Skip to content

Commit 4d4a990

Browse files
committed
feat: Add hand recording and playback for automated testing
Introduces hooks for recording and replaying hand gesture data, enabling automated testing and playback of hand interactions. Updates gesture state to include pinchPoint for direct pinch selection, refactors selection logic to use pinchPoint, and adds visual feedback for pinch pre-select. UI now displays recording and playback indicators, and exposes global automation APIs in test mode.
1 parent 2a7e956 commit 4d4a990

9 files changed

Lines changed: 1483 additions & 146 deletions

src/App.tsx

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { LassoOverlay } from './components/LassoOverlay'
2323
import { SelectionActions } from './components/SelectionActions'
2424
import { TagCloud } from './components/TagCloud'
2525
import { useHandLockAndGrab } from './hooks/useHandLockAndGrab'
26+
import { useHandRecording, downloadRecording, listSavedRecordings, loadRecordingFromStorage } from './hooks/useHandRecording'
27+
import { useHandPlayback } from './hooks/useHandPlayback'
2628
import { useTagCloud } from './hooks/useTagCloud'
2729
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'
2830
import { useBookmarks, type Bookmark } from './hooks/useBookmarks'
@@ -58,6 +60,7 @@ const DEFAULT_GESTURE_STATE: GestureState = {
5860
pointDirection: null,
5961
pinchStrength: 0,
6062
grabStrength: 0,
63+
pinchPoint: null,
6164
leftPinchRay: null,
6265
rightPinchRay: null,
6366
activePinchRay: null,
@@ -163,6 +166,56 @@ export default function App() {
163166
}, [])
164167

165168
const [gestureState, setGestureState] = useState<GestureState>(DEFAULT_GESTURE_STATE)
169+
170+
// Test mode - check URL param for automated testing
171+
const [isTestMode] = useState(() => {
172+
const params = new URLSearchParams(window.location.search)
173+
return params.get('test') === 'true'
174+
})
175+
176+
// Hand recording/playback for automated testing
177+
const recording = useHandRecording({ autoDownload: false })
178+
const playback = useHandPlayback({
179+
logEvents: true,
180+
exposeGlobal: true,
181+
onGestureChange: (state) => {
182+
// When playing back, use the playback gesture state
183+
if (playback.isPlaying) {
184+
setGestureState(state)
185+
}
186+
},
187+
})
188+
189+
// Expose recording controls globally for automation
190+
useEffect(() => {
191+
if (isTestMode) {
192+
const api = {
193+
// Recording
194+
startRecording: recording.startRecording,
195+
stopRecording: () => {
196+
const rec = recording.stopRecording()
197+
if (rec) downloadRecording(rec)
198+
return rec
199+
},
200+
isRecording: () => recording.isRecording,
201+
// Playback
202+
loadRecording: playback.loadRecording,
203+
play: playback.play,
204+
pause: playback.pause,
205+
stop: playback.stop,
206+
seek: playback.seek,
207+
setSpeed: playback.setSpeed,
208+
// Utilities
209+
listRecordings: listSavedRecordings,
210+
loadFromStorage: loadRecordingFromStorage,
211+
getGestureState: () => gestureState,
212+
}
213+
;(window as unknown as Record<string, unknown>).__handTest = api
214+
console.log('[TEST MODE] Enabled. Use window.__handTest for automation')
215+
console.log('[TEST MODE] Commands: startRecording(), stopRecording(), listRecordings(), loadFromStorage(key), play(), pause()')
216+
}
217+
}, [isTestMode, recording, playback, gestureState])
218+
166219
// Tracking source - check URL param on mount, then allow UI toggle
167220
const [trackingSource, setTrackingSource] = useState<'mediapipe' | 'iphone'>(() => {
168221
const params = new URLSearchParams(window.location.search)
@@ -233,7 +286,11 @@ export default function App() {
233286

234287
const handleGestureStateChange = useCallback((state: GestureState) => {
235288
setGestureState(state)
236-
}, [])
289+
// Record frame if recording is active
290+
if (recording.isRecording) {
291+
recording.recordFrame(state, trackingInfo.hasLiDAR)
292+
}
293+
}, [recording.isRecording, recording.recordFrame, trackingInfo.hasLiDAR])
237294

238295
const { lock: handLock } = useHandLockAndGrab(gestureState, gestureControlEnabled)
239296

@@ -672,6 +729,33 @@ export default function App() {
672729
</button>
673730
)}
674731

732+
{/* Recording Indicator (when recording) */}
733+
{recording.isRecording && (
734+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-red-500/20 border border-red-500/50">
735+
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
736+
<span className="text-red-400 text-sm font-medium">
737+
REC {Math.floor(recording.duration / 1000)}s ({recording.frameCount})
738+
</span>
739+
</div>
740+
)}
741+
742+
{/* Playback Indicator (when playing) */}
743+
{playback.isPlaying && (
744+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/20 border border-green-500/50">
745+
<div className="w-2 h-2 rounded-full bg-green-500" />
746+
<span className="text-green-400 text-sm font-medium">
747+
PLAY {Math.floor(playback.currentTime / 1000)}s / {Math.floor(playback.duration / 1000)}s
748+
</span>
749+
</div>
750+
)}
751+
752+
{/* Test Mode Indicator */}
753+
{isTestMode && !recording.isRecording && !playback.isPlaying && (
754+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/50">
755+
<span className="text-yellow-400 text-sm font-medium">TEST MODE</span>
756+
</div>
757+
)}
758+
675759
{/* Settings Panel Toggle */}
676760
<button
677761
onClick={() => setSettingsPanelOpen(!settingsPanelOpen)}

0 commit comments

Comments
 (0)