From 4bccbc15801225f7512f342d2d529cadca2aaac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:40:33 +0200 Subject: [PATCH] feat(pose): decouple pinch from air-piano gate --- data_only_viz/action_head_pub.py | 10 ++++- .../tests/test_action_head_finger_emit.py | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/data_only_viz/action_head_pub.py b/data_only_viz/action_head_pub.py index 5a5941c..33256fb 100644 --- a/data_only_viz/action_head_pub.py +++ b/data_only_viz/action_head_pub.py @@ -172,6 +172,11 @@ class ActionHeadPublisher(threading.Thread): self._finger_enabled = os.environ.get("FINGER_PIANO", "0") not in ( "0", "", "false", "False", ) + # Pinch detection is gated independently of the air-piano finger-strike: + # the matrix global actions want pinches WITHOUT the finger-strike "piano". + self._pinch_enabled = os.environ.get("PINCH_ENABLE", "0") not in ( + "0", "", "false", "False", + ) vel = float(os.environ.get("FINGER_STRIKE_VEL", "0.02")) refr = float(os.environ.get("FINGER_STRIKE_REFRACTORY_MS", "120")) self._finger_det = FingerStrikeDetector( @@ -220,8 +225,9 @@ class ActionHeadPublisher(threading.Thread): self.bridge.send_finger(ev) def _emit_pinch(self, t_now: float) -> None: - """Detect thumb-to-finger pinches (clip toggles) and emit /pose/pinch.""" - if not getattr(self, "_finger_enabled", False): + """Detect thumb-to-finger pinches (matrix global actions) and emit + /pose/pinch. Gated by PINCH_ENABLE, independently of the air-piano.""" + if not getattr(self, "_pinch_enabled", False): return hands, used, _ip = self._pick_hands() for ev in self._pinch_det.step(hands, t_now): diff --git a/data_only_viz/tests/test_action_head_finger_emit.py b/data_only_viz/tests/test_action_head_finger_emit.py index 5226237..ca4b9f2 100644 --- a/data_only_viz/tests/test_action_head_finger_emit.py +++ b/data_only_viz/tests/test_action_head_finger_emit.py @@ -52,3 +52,47 @@ def test_emit_fingers_noop_when_disabled(monkeypatch): pub.state.persons_hands = [_hand(0.46)] pub._emit_fingers(0.04) assert not pub.bridge.send_finger.called + + +def _pub_envs(monkeypatch, finger_piano: str, pinch_enable: str): + """Build a publisher with both gates set independently.""" + monkeypatch.setenv("FINGER_PIANO", finger_piano) + monkeypatch.setenv("PINCH_ENABLE", pinch_enable) + monkeypatch.setenv("FINGER_STRIKE_VEL", "0.02") + monkeypatch.setenv("FINGER_STRIKE_REFRACTORY_MS", "120") + from data_only_viz.action_head_pub import ActionHeadPublisher + pub = ActionHeadPublisher.__new__(ActionHeadPublisher) + pub.state = _FakeState() + pub.bridge = MagicMock() + pub._init_finger_piano() + return pub + + +def test_emit_pinch_fires_when_pinch_enabled(monkeypatch): + # PINCH_ENABLE on, FINGER_PIANO off -> pinches still emit (decoupled). + pub = _pub_envs(monkeypatch, finger_piano="0", pinch_enable="1") + pub._pinch_det = MagicMock() + pub._pinch_det.step.return_value = [MagicMock(hand=0, finger=1)] + pub.state.persons_hands = [_hand(0.40)] + pub._emit_pinch(0.0) + assert pub.bridge.send_pinch.called + + +def test_emit_pinch_off_when_pinch_disabled_even_with_finger_piano(monkeypatch): + # Decouple: FINGER_PIANO on but PINCH_ENABLE off -> no pinch emitted. + pub = _pub_envs(monkeypatch, finger_piano="1", pinch_enable="0") + pub._pinch_det = MagicMock() + pub._pinch_det.step.return_value = [MagicMock(hand=0, finger=1)] + pub.state.persons_hands = [_hand(0.40)] + pub._emit_pinch(0.0) + assert not pub.bridge.send_pinch.called + + +def test_emit_fingers_off_when_only_pinch_enabled(monkeypatch): + # Decouple: PINCH_ENABLE on, FINGER_PIANO off -> air-piano stays silent. + pub = _pub_envs(monkeypatch, finger_piano="0", pinch_enable="1") + pub.state.persons_hands = [_hand(0.40)] + pub._emit_fingers(0.00) + pub.state.persons_hands = [_hand(0.46)] + pub._emit_fingers(0.04) + assert not pub.bridge.send_finger.called