diff --git a/data_only_viz/action_head_pub.py b/data_only_viz/action_head_pub.py index 674649d..d08a46f 100644 --- a/data_only_viz/action_head_pub.py +++ b/data_only_viz/action_head_pub.py @@ -165,8 +165,11 @@ class ActionHeadPublisher(threading.Thread): rkp = getattr(self.state, "right_hand_kp", None) hands = [h for h in (lkp, rkp) if h is not None and len(h) > 0] chirality = [] + stab = getattr(self, "_stab", None) + fist_enabled = stab.active_flags() if stab is not None else (True, True) feats = self._hand_ext.step(hands, chirality or None, - swap=getattr(self, "_hand_swap_lr", False)) + swap=getattr(self, "_hand_swap_lr", False), + fist_enabled=fist_enabled) with self.state.lock(): self.state.hand_feats = feats self.bridge.send_hands(feats, t_now) diff --git a/data_only_viz/hand_features.py b/data_only_viz/hand_features.py index e47c095..491aaa1 100644 --- a/data_only_viz/hand_features.py +++ b/data_only_viz/hand_features.py @@ -79,7 +79,8 @@ class HandFeatureExtractor: buf.append((cx, cy)) return _clamp(spd, 0.0, 1.0) - def step(self, hands: list, chirality=None, swap: bool = False) -> dict: + def step(self, hands: list, chirality=None, swap: bool = False, + fist_enabled: tuple = (True, True)) -> dict: from data_only_viz.hand_slots import route_hands # swap must mirror the gesture path (HAND_SWAP_LR) or per-voice # L/R mods contradict pinch/strike in the same performance. @@ -95,6 +96,8 @@ class HandFeatureExtractor: if left_lm is not None: lf = self._features(left_lm) + if not fist_enabled[0]: + lf["fist"] = 0.0 lf["speed"] = self._speed(0, lf["cx"], lf["cy"]) out_l = lf else: @@ -102,6 +105,8 @@ class HandFeatureExtractor: if right_lm is not None: rf = self._features(right_lm) + if not fist_enabled[1]: + rf["fist"] = 0.0 rf["speed"] = self._speed(1, rf["cx"], rf["cy"]) out_r = rf else: diff --git a/data_only_viz/tests/test_hand_features.py b/data_only_viz/tests/test_hand_features.py index 2033fb6..bf8dfeb 100644 --- a/data_only_viz/tests/test_hand_features.py +++ b/data_only_viz/tests/test_hand_features.py @@ -153,3 +153,53 @@ def test_no_swap_keeps_source_chirality(): out = ext.step([a, b], [1, 0], swap=False) assert out["L"]["cx"] == pytest.approx(0.70, abs=0.01) # chir 0 -> L assert out["R"]["cx"] == pytest.approx(0.30, abs=0.01) + + +# --------------------------------------------------------------------------- +# fist_enabled gate +# --------------------------------------------------------------------------- + +def test_fist_enabled_false_zeroes_fist_preserves_openness(): + """fist_enabled=(False, False) forces fist=0 but leaves openness intact.""" + ext = HandFeatureExtractor() + h = _fist_hand(0.20) # all curled -> fist > 0.9 normally + out = ext.step([h], fist_enabled=(False, False)) + hand = out["L"] or out["R"] + assert hand["fist"] == 0.0, "fist must be zeroed when disabled" + assert hand["openness"] < 0.2, "openness unaffected" + + +def test_fist_enabled_true_computes_fist(): + """fist_enabled=(True, True) leaves fist computation unchanged.""" + ext = HandFeatureExtractor() + h = _fist_hand(0.20) + out = ext.step([h], fist_enabled=(True, True)) + hand = out["L"] or out["R"] + assert hand["fist"] > 0.9 + + +def test_fist_enabled_per_slot_independence(): + """fist_enabled masks only the specified slot.""" + ext = HandFeatureExtractor() + # Build two 21-kp hands with distinct wrist positions for L/R routing + # Left hand: cx~0.2, all fingers curled (fist) + h_fist = _fist_hand(0.20) + h_fist[0] = LM(0.2, 0.6) + h_fist[9] = LM(0.2, 0.5) + for mcp in (5, 13, 17): + h_fist[mcp] = LM(0.2, 0.5) + for tip in (8, 12, 16, 20): + h_fist[tip] = LM(0.2 + 0.20 * 0.10, 0.5) + # Right hand: cx~0.8, fingers extended (open) + h_open = _fist_hand(0.80) + h_open[0] = LM(0.8, 0.6) + h_open[9] = LM(0.8, 0.5) + for mcp in (5, 13, 17): + h_open[mcp] = LM(0.8, 0.5) + for tip in (8, 12, 16, 20): + h_open[tip] = LM(0.8 + 0.80 * 0.10, 0.5) + out = ext.step([h_fist, h_open], fist_enabled=(False, True)) + # L slot (cx~0.2) should have fist=0 even though it's curled + assert out["L"]["fist"] == 0.0 + # R slot (cx~0.8) should compute normally (open -> fist < 0.1) + assert out["R"]["fist"] < 0.1