@@ -23,6 +23,8 @@ import { LassoOverlay } from './components/LassoOverlay'
2323import { SelectionActions } from './components/SelectionActions'
2424import { TagCloud } from './components/TagCloud'
2525import { useHandLockAndGrab } from './hooks/useHandLockAndGrab'
26+ import { useHandRecording , downloadRecording , listSavedRecordings , loadRecordingFromStorage } from './hooks/useHandRecording'
27+ import { useHandPlayback } from './hooks/useHandPlayback'
2628import { useTagCloud } from './hooks/useTagCloud'
2729import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'
2830import { 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