SC's mod cache keeps the last received value when a hand vanishes, but
the gauge markers and wrist cross disappeared — the screen no longer
showed what the matrix was hearing. Markers now hold the last known
position (dim conf 0.45) and go full-bright when the hand is live.
Metal lines are 1px and the skeleton was barely visible in
performance. Each ARKit bone is now drawn in concentric offset passes
(SKEL_LINE_WIDTH, default 4 approx px; 1 restores the thin look).
The X/Y voice mods were anchored to the 21-landmark centroid, so
opening or pinching the fingers shifted the mod values (gesture/mod
cross-talk). cx/cy now track the WRIST (kp 0). The renderer draws a
cross marker at each wrist on the video whenever features exist, so
the control point stays visible even for far hands the overlay
filters out (user request live 2026-07-02).
m now stops the auto data-synth scene AND forces the openpos viz
(matrix unaffected — it is not a scene). Repairs the two test pins the
frozen pinch defaults broke (ext_min 1) and makes the skeleton path
safe for __new__-built renderers (hand gate getattr).
Live session outcomes: (1) pinch thresholds validated for inner
fingers frozen as defaults (ratio_on 0.50, margin 0.05, ext_min 1);
(2) panel frames + X/Y gauges now permanent (no early-return when
nothing is detected); (3) wxcvbn source-bundle shortcuts gated off by
default (VIZ_SOURCE_KEYS=1 re-enables) — a stray key mid-performance
switched the audio scene.
Add gesture_quality() pure helper (hand_display.py) that maps a slot's
plausibility, facing, and proximity to a score in [0,1]. Renderer uses
it to modulate panel frame brightness (0.25+0.75*q) and stroke count
(1..3 passes at q≥0, ≥0.5, ≥0.85 / status==3). Status still drives
hue (pid 7/8/9). New gesture_slot_quality field in State written
alongside gesture_slot_status each tick.
Add gauge_segments() for X/Y position gauges around each panel: a
horizontal rail below and a vertical rail on the outer side, each with
a bold two-tick notch at the hand's normalised cx/cy (mirror-aware for
X). Dim rails (conf 0.25) when slot absent. Renderer reads hand_feats
L/R directly from state.
11 new tests (6 gesture_quality + 5 gauge_segments); suite 430 passed.
Add gesture_slot_status [0..3] to State (absent/detected/armed/engaged).
PinchDetector.engaged_slots() exposes per-slot pinch state. Publisher
writes status after each tick via _update_gesture_slot_status().
Renderer draws panel frame always: dim (pid7 conf=0.3) when absent,
normal (pid7) detected, pid8 armed, pid9 double-stroke engaged.
Portrait mode validated live: VIDEO_ROTATE now also rotates the
iPhone 2D landmarks (skeleton2D + Vision hands) at the decode layer so
overlays/panels/gestures stay aligned with the rotated video. ARKit
per-finger joints are masked from the wireframe (unreliable 2D, seen
as false hands); the display shows the filtered Vision hands instead.
The body-mesh triangles (solid cyan blob reported live) were the one
surface no filter covered: under iphone-usb persons_body is the ARKit
conversion with c=1.0, so garbage/stale joints painted full triangles.
Skip BODY_TRIANGLES whenever the ARKit topology is present (webcam
MediaPipe path unchanged); the body stays the freshness-gated ARKit
wireframe.
renderer.py: read ARKIT_FULL_SKELETON via VizConfig at
initWithState_ instead of module-level os.environ; store as
self._arkit_full, use getattr fallback for __new__ test bypass.
main.py: pass cfg.iphone_osc_port to IphoneOSCListener
instead of hardcoded 57128; log actual port.
hand_slots.py: import _finite/_coord/_clamp from hand_features
at module level (acyclic: hand_features imports hand_slots only
lazily inside step()); delete the duplicate copies.
Add data_only_viz/config.py: frozen dataclass VizConfig with typed
fields for all 56 env reads in data_only_viz. from_env() reads
os.environ at call time — no module-level singleton, test-compat.
Migrate action_head_pub._init_finger_piano and renderer.initWithState_
to consume VizConfig. Keep renderer.py ARKIT_FULL module-level (test
uses MetalRenderer.__new__ without calling initWithState_).
HAND_SWAP_LR default changed False (was True): Vision chirality
validated correct live 2026-07-02; swap knob now defaults off.
HandPersistenceGate (min_frames=3, radius=0.15) suppresses ghost
hands that flash for fewer than min_frames consecutive frames.
Gate is called once per frame in the renderer on persons_hands;
result filters both the video overlay loop and the side panels.
hand_plausible now rejects wrists outside [-0.1, 1.1] (out-of-frame
ghost anchor check). panel_segments gains a conf_min param.
Env knobs read at renderer init (single parse, no per-frame cost):
HAND_CONF_MIN (default 0.45, raised from 0.3)
HAND_PERSIST_FRAMES (default 3)
ARKIT_BONE_MAX (default 0.5, passed to arkit_segments)
New tests: 12 gate tests + 4 wrist-bounds tests.
persons_arkit_2d_t was written by IphoneUSBSource but never
read. A USB drop left the frozen skeleton on screen indefinitely.
- Add arkit_2d_fresh(ts_by_pid, now, max_age=1.0) pure helper
in hand_display.py (8 TDD tests, all green)
- renderer._update_skeleton: use_arkit now requires at least one
pid younger than 1 s (replaces bool check)
- Per-pid freshness gate in draw loop skips stale pids
Add panel_rect/panel_segments/panel_frame helpers to hand_display.py
(PANEL_SIDE=0.30, PANEL_MARGIN=0.02, PANEL_INNER=0.10). Segments use
uniform pixel-space scale so the hand aspect ratio is preserved
regardless of view aspect. mirror=True flips X within the panel.
In renderer._update_skeleton add push_panel (no mirror flip) and a
panels block before the segs==0 epilogue: routes persons_hands to
left/right panel via chirality list when aligned, screen-cx fallback
otherwise. Draws frame (pid 7) + wireframe (pid 5 left / 6 right).
Runs on every path including use_arkit. Guards: _mp_bones None, empty
hands, misaligned chirality, height==0.
26 tests pass (10 new for panel helpers).
Replace per-frame floats list + struct.pack in _update_skeleton
with a preallocated numpy.float32 staging buffer (_skel_cpu_buf),
eliminating the dynamic allocation on every draw call (~60/s).
New standalone visualizer: OSC listener, pose bridge,
Apple Vision / CoreML / YOLO pose backends, Euro filter,
fine analysis, mesh topology, holistic renderer.
Metal shader (scene.metal) for GPU-accelerated drawing.