Improve debug views: add 2D graph, fix flipped axes, hide invisible landmarks, show data pre-calibration

- Delete REFACTOR.md, add PAUSE_SIGNAL.md for pause gesture rework planning
- Fix vertically flipped FRONT/SIDE 3D views (remove incorrect Y negation)
- Hide landmarks with visibility < 0.3 and exclude from auto-scale
- Show step graphs before calibration completes (use raw position as baseline)
- Add 4th debug view showing 2D normalized landmarks
- Expose landmarks2D from usePoseLoop
- Flip step graph Y axis to match intuitive direction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
main
name 2026-04-15 20:35:32 +00:00
parent ce7cf3a042
commit 5007dd9440
7 changed files with 77 additions and 386 deletions

32
PAUSE_SIGNAL.md 100644
View File

@ -0,0 +1,32 @@
# Pause Signal Rework
## Problem
The current pause gesture is **arms-out (T-pose)**: both wrists at shoulder height.
Issues:
- **Accidental triggers** — players raise arms during gameplay and inadvertently pause
- **Unintuitive** — new players don't know or remember to hold a T-pose to pause
## Current Implementation
- `armsOutDetector.ts` — checks both wrists within `ARMS_Y_TOLERANCE` (0.15) of shoulder Y
- 2-second cooldown after arms drop before it can re-trigger
- Arms-out is dual-purpose: it both **pauses gameplay** and is the **calibration-ready signal**
## Alternative Signals
| Gesture | Pros | Cons |
|---|---|---|
| **Hands on head** (both wrists above head) | Very intentional, hard to do by accident | Might be tiring if held |
| **Crossed arms** (wrists near opposite shoulders) | Familiar "stop" posture, unlikely during dance | Harder to detect reliably with occlusion |
| **Hand over face** (one wrist covering nose) | Simple, quick | Could conflict with wiping sweat |
| **Both hands raised high** (surrender pose) | Easy to detect, distinct from dance moves | Could overlap with jump celebrations |
| **Timeout / stillness** (no movement for N seconds) | No specific pose needed | Slow, frustrating to wait |
| **Palm facing camera** (open hand toward webcam) | Intuitive "stop" signal | Requires hand landmark model, more compute |
## Considerations
- Whatever replaces T-pose for pause must not conflict with the calibration flow (which also uses arms-out to signal "ready")
- Should be easy to explain in the tutorial overlay
- Must be distinct from stepping, jumping, and natural dance movement

View File

@ -1,357 +0,0 @@
# Refactor Proposal: Modular MediaPipe + Gesture Callbacks
## Current State
| File | Lines | Responsibility |
|------|-------|---------------|
| `gestureDetector.ts` | 511 | State machine + all gesture detection + step detection + graph sampling + logging |
| `App.tsx` | 457 | MediaPipe init, video management, animation loop, canvas drawing, gesture wiring, UI |
| `Settings.tsx` | 254 | Settings panel (fine as-is) |
| `StepGraphs.tsx` | 153 | Debug visualization (fine as-is) |
| `arScene.ts` | 64 | Three.js setup (fine as-is) |
| `types.ts` | 49 | Settings types + persistence |
| `worldToCamera.ts` | 61 | Unused (dead code) |
| `DebugGraph.tsx` | 136 | Unused (dead code) |
| `debugLog.ts` | 16 | In-memory log buffer |
### Problems
1. **gestureDetector.ts is a 511-line monolith** -- calibration state machine, jump detection, leg raises, palms, arms-out, step detection, graph sampling, and baseline adaptation all live in one function.
2. **App.tsx does too much** -- MediaPipe lifecycle, webcam/file source management, the animation loop, canvas drawing, and React UI state are all in one component with 7 refs.
3. **No callback/event system** -- gesture signals pass through mutable `GestureState` + return values. There's no way to subscribe to "a step was detected" without polling the ref every frame.
4. **Dead code** -- `worldToCamera.ts` and `DebugGraph.tsx` are imported nowhere.
---
## Proposed Architecture
```
src/
types.ts # All shared types (expanded)
debugLog.ts # Unchanged
pose/
PoseDetector.ts # MediaPipe lifecycle (init, detect, dispose)
VideoSource.ts # Webcam/file source: attach -> loadeddata -> play, debug.webm probe
usePoseLoop.ts # Hook: owns rAF loop, calls engine.update() + arScene directly (no React in hot path)
gesture/
types.ts # GestureSignal, CalibrationStatus, per-detector state, callbacks
CalibrationMachine.ts # Framing -> waiting -> calibrating -> pos-center -> ready/paused
GestureTelemetry.ts # Graph/log sampling — subscribes to callbacks, collects GraphSample/StepGraphSample/StepLogEntry
detectors/
jumpDetector.ts # Jump detection (both ankles rise)
stepDetector.ts # Step detection (toe XZ displacement + baseline adaptation)
palmsDetector.ts # Palms together (wrist distance)
armsOutDetector.ts # Arms out (wrist at shoulder height)
GestureEngine.ts # Orchestrates calibration + detectors, fires callbacks. Config via constructor + setConfig()
useGesture.ts # Hook: creates engine, exposes read-only state + callbacks to React. Owns display timer (not engine)
ar/
arScene.ts # Three.js setup (remove unused resizeARScene export)
components/
App.tsx # Slim orchestrator: wires hooks together, renders UI
Settings.tsx # Unchanged
StepGraphs.tsx # Unchanged
GestureOverlay.tsx # Gesture label + calibration hints (extracted from App). Owns 600ms display timer
TransportBar.tsx # Video playback controls (extracted from App)
PoseCanvas.tsx # Canvas drawing: landmarks, connectors, framing box, debug text
```
---
## Key Design Decisions
### 1. Callback-based gesture events
The core ask. Instead of polling a mutable ref, consumers register callbacks:
```ts
// gesture/types.ts
export interface GestureCallbacks {
onGesture?: (signal: GestureSignal) => void // any gesture fires (immediate, no display timer)
onStep?: (direction: StepDirection, foot: 'left' | 'right') => void
onJump?: () => void
onPalms?: () => void
onArmsOut?: () => void
onCalibrationChange?: (status: CalibrationStatus) => void
onPause?: () => void
onResume?: () => void
}
export type StepDirection = 'left' | 'right' | 'forward' | 'back'
```
The `GestureEngine` accepts callbacks at construction and fires them when gestures trigger. The `useGesture` hook exposes this to React:
```ts
// Usage in App.tsx
const gesture = useGesture(poseResults, settings, {
onStep: (dir, foot) => console.log(`Step ${dir} with ${foot} foot`),
onJump: () => console.log('Jump!'),
onCalibrationChange: (status) => console.log(`Calibration: ${status}`),
})
```
### 2. PoseDetector class wraps MediaPipe lifecycle
Extracts the MediaPipe init/detect/dispose cycle out of App.tsx into a plain class:
```ts
// pose/PoseDetector.ts
export class PoseDetector {
static async create(settings: PoseSettings): Promise<PoseDetector>
detectFrame(video: HTMLVideoElement, timestamp: number): PoseResult
dispose(): void
}
export interface PoseResult {
landmarks2D: NormalizedLandmark[][]
worldLandmarks: Landmark[][]
}
```
### 3. VideoSource class wraps webcam/file management
Owns the `MediaStream`, video element setup, and recording logic:
```ts
// pose/VideoSource.ts
export class VideoSource {
static async create(
video: HTMLVideoElement,
mode: 'webcam' | 'file',
fileUrl?: string
): Promise<VideoSource>
get stream(): MediaStream | null
dispose(): void
}
```
### 4. usePoseLoop hook ties it together
Single hook that manages the animation frame loop:
```ts
// pose/usePoseLoop.ts
export function usePoseLoop(
videoRef: RefObject<HTMLVideoElement>,
settings: PoseSettings,
fileUrl: string | null,
onFrame: (result: PoseResult, fps: number) => void,
): { loading: boolean; error: string | null }
```
### 5. Individual detectors are pure functions
Each detector is a small, focused, testable function that owns its own state type:
```ts
// gesture/detectors/jumpDetector.ts
export interface JumpState { wasAirborne: boolean }
export function createJumpState(): JumpState
export function detectJump(
state: JumpState,
leftRise: number,
rightRise: number,
): boolean // true = just detected
```
```ts
// gesture/detectors/stepDetector.ts
export interface StepState {
activeStep: GestureSignal
steppedFoot: 'left' | 'right' | null
heelBaseL: Point2D | null
heelBaseR: Point2D | null
}
export function createStepState(): StepState
export function detectStep(...): StepResult | null
export function checkStepReset(...): boolean
export function adaptBaseline(base: Point2D, current: Point2D, alpha?: number): Point2D
```
```ts
// gesture/detectors/palmsDetector.ts
export interface PalmsState { wasPalms: boolean }
export function createPalmsState(): PalmsState
export function detectPalms(state: PalmsState, worldLandmarks: Landmark3D[]): boolean
// gesture/detectors/armsOutDetector.ts
export interface ArmsOutState { wasArmsOut: boolean; armsDownSince: number | null }
export function createArmsOutState(): ArmsOutState
export function detectArmsOut(state: ArmsOutState, worldLandmarks: Landmark3D[]): boolean
```
### 6. CalibrationMachine is a standalone state machine
Separated from gesture detection. Owns ankle baseline collection, heel calibration samples, and foot spread measurement:
```ts
// gesture/CalibrationMachine.ts
export interface CalibratedBaselines {
leftY: number // 2D ankle Y baseline
rightY: number
heelBaseL: Point2D // hip-relative XZ for step detection
heelBaseR: Point2D
footSpread: number // meters
}
export class CalibrationMachine {
get status(): CalibrationStatus
get framingHint(): string | null
get baselines(): CalibratedBaselines | null
/** Call each frame. Returns true if state changed. */
update(
landmarks2D: NormalizedLandmark[],
worldLandmarks: Landmark3D[],
): boolean
reset(): void
}
```
### 7. GestureEngine orchestrates everything
Replaces the current `detectGesture()` monolith. Owns the calibration machine and runs all detectors in sequence. Config passed at construction, updated via setter:
```ts
// gesture/GestureEngine.ts
export class GestureEngine {
constructor(config: StepConfig, callbacks?: GestureCallbacks)
/** Call each frame with latest pose data. Returns current signal for display. */
update(
landmarks2D: NormalizedLandmark[],
worldLandmarks: Landmark3D[],
): GestureSignal
/** Read-only access for synchronous canvas drawing (e.g. calibrationStatus for red box) */
get state(): Readonly<GestureState>
setConfig(config: StepConfig): void
setCallbacks(callbacks: GestureCallbacks): void
}
```
---
## What Stays the Same
- All detection algorithms (thresholds, hysteresis, baseline adaptation)
- Calibration flow (framing -> waiting -> calibrating -> pos-center -> ready/paused)
- Settings panel and persistence
- Three.js AR overlay
- Step graph visualization
- Debug logging
- Recording functionality
---
## What Gets Deleted
- `worldToCamera.ts` -- unused dead code
- `DebugGraph.tsx` -- unused dead code
---
## Migration Order
The refactor is incremental — app stays working at each step:
1. **Delete dead code** -- `worldToCamera.ts`, `DebugGraph.tsx`, unused `resizeARScene` export, `left-leg`/`right-leg` from `GestureSignal` type + App.tsx UI references, `wasLeftRaised`/`wasRightRaised` from `GestureState`
2. **Extract types** -- move gesture types from `gestureDetector.ts` to `gesture/types.ts`, add `GestureCallbacks` interface, per-detector state types
3. **Extract individual detectors** -- pull `jumpDetector`, `stepDetector`, `palmsDetector`, `armsOutDetector` out as pure functions with own state types
4. **Extract CalibrationMachine** -- pull calibration state logic + baseline collection into its own module
5. **Build GestureEngine as thin wrapper** -- initially delegates to existing `detectGesture()` to keep things working, then incrementally migrate logic to use calibration machine + detectors + callback dispatch
6. **Extract GestureTelemetry** -- move graph/log sampling out of engine, subscribe to callbacks
7. **Move display timer to React** -- remove `signalShowUntil` from engine, let `useGesture`/`GestureOverlay` own the 600ms display timer
8. **Extract PoseDetector** -- wrap MediaPipe init/detect/dispose
9. **Extract VideoSource** -- wrap webcam/file source management including attach -> loadeddata -> play lifecycle and debug.webm auto-probe
10. **Build usePoseLoop hook** -- owns rAF loop, calls `gestureEngine.update()` + `arScene.update()` directly (no React in hot path), batched setState for UI
11. **Build useGesture hook** -- wraps GestureEngine for React, exposes read-only state + callbacks
12. **Extract UI components** -- `GestureOverlay`, `TransportBar`, `PoseCanvas`
13. **Slim down App.tsx** -- wire hooks and components together
---
## Expected Result
| Before | After |
|--------|-------|
| `App.tsx` 457 lines | `App.tsx` ~100 lines |
| `gestureDetector.ts` 511 lines | `GestureEngine.ts` ~80 lines + 4 detectors ~40-60 lines each + `CalibrationMachine.ts` ~120 lines + `GestureTelemetry.ts` ~60 lines |
| No callback system | `GestureCallbacks` interface with per-gesture hooks |
| 2 dead files | Deleted |
| All logic in 2 big files | 15+ small, focused modules |
---
## Review Notes
### Issues Found
**1. Leg raise detection doesn't exist yet — nothing to extract**
The proposal lists `legRaiseDetector.ts` as something to extract, but `emit('left-leg')` / `emit('right-leg')` is never called in the current code. The `wasLeftRaised`/`wasRightRaised` state fields are declared but never written to. The `leftRaised`/`rightRaised` variables are computed (line 406-407) but only used to gate baseline adaptation (lines 503-508), not to fire a gesture signal. The type union and App.tsx UI reference `left-leg`/`right-leg`, so the intent was there, but the implementation was never finished. The refactor should either implement it or remove the dead types/UI — not list it as an extraction.
**2. Graph/log sampling has no home**
`GraphSample`, `StepGraphSample`, and `StepLogEntry` collection is ~40% of the current `detectGesture()` code. The proposal doesn't say which module owns this. It can't live in the individual detectors (they're supposed to be pure). It shouldn't live in `GestureEngine` (that's orchestration, not observability). Suggest a dedicated `gesture/GestureTelemetry.ts` that subscribes to the callback system, or have `GestureEngine` expose a `samples` getter and do collection internally as a separate concern.
**3. `signalShowUntil` mixes UI and detection**
The current code gates all detection behind `if (now < state.signalShowUntil) return state.activeSignal` — a 600ms display timer that suppresses new gestures. This is a UI concern (how long to show a label) leaking into detection logic. The proposal doesn't address where this lives. Suggestion: remove it from the engine entirely. Let `GestureEngine` fire callbacks immediately and let the React layer (`useGesture` or `GestureOverlay`) own the display timer.
**4. `arScene.ts` has dead code too**
The proposal says arScene.ts is "fine as-is", but `resizeARScene` (line 49) is exported and never called. Minor, but should be included in the dead code cleanup.
**5. `stepConfig` passed per-frame is noisy**
`GestureEngine.update()` takes `stepConfig` every frame, but it only changes when the user edits settings. Pass it at construction and add a `setConfig()` method instead. Same applies to any other settings the engine needs.
**6. `VideoSource` API is too thin**
The proposed `VideoSource.create(video, mode, fileUrl?)` doesn't account for:
- The `loadeddata` promise needed before `video.play()`
- Auto-probing `/debug.webm` on startup
- Calling `video.play()` after source attachment
The real lifecycle is: attach source → wait for loadeddata → play. `VideoSource` needs to own all three steps or the caller ends up reimplementing the complexity.
**7. Data flow between hooks is undefined**
`usePoseLoop` emits `(result, fps)` via `onFrame`. `useGesture` takes `poseResults`. How do they connect? If `onFrame` calls into `useGesture`, then `useGesture` can't take `poseResults` as a prop — it needs to be a callback target. If App.tsx wires them via state, you get a frame of latency and re-render per frame. The proposal should specify: does `usePoseLoop` call `gestureEngine.update()` directly inside the animation loop, or does React mediate? The former is better for performance (no React in the hot path).
**8. Step 5 in migration is a risky big-bang swap**
"Build GestureEngine — compose calibration machine + detectors + callback dispatch, replacing `detectGesture()`" replaces the working monolith in one shot. Safer: first build `GestureEngine` as a thin wrapper that delegates to the existing `detectGesture()`, then incrementally migrate logic out of it.
### Suggestions
**A. Split `GestureState` per-detector**
The current 20+ field bag makes it hard to reason about what state belongs to which detector. Each detector should own its state:
```ts
interface JumpState { wasAirborne: boolean }
interface StepState { activeStep: GestureSignal; steppedFoot: 'left' | 'right' | null; heelBaseL: Point2D | null; ... }
interface PalmsState { wasPalms: boolean }
// etc.
```
`GestureEngine` composes these. This makes the state boundaries explicit and each detector truly self-contained.
**B. Decide on frame-loop ownership**
The cleanest architecture: `usePoseLoop` owns the `requestAnimationFrame` loop and calls `gestureEngine.update()` + `arScene.update()` directly — no React state in the hot path. React only gets notified for UI updates (gesture label, calibration status) via batched setState. The `onFrame` callback is for the canvas drawing, not for gesture detection.
**C. Callbacks as the primary interface**
Callbacks are the right call — they make the API push-based and decouple consumers from engine internals. Just make sure the engine still exposes a read-only `state` getter for the few cases where the animation loop needs synchronous access (e.g. canvas drawing checks `calibrationStatus` to decide whether to draw the red box). Callbacks notify, state answers "what's true right now".

View File

@ -66,7 +66,7 @@ export function DebugApp() {
const {
loading, error, fps, gesture, calibrationStatus, framingHint,
stepGraphSamples, worldLandmarks, telemetry,
stepGraphSamples, landmarks2D, worldLandmarks, telemetry,
} = usePoseLoop(videoRef, canvasRef, arCanvasRef, settings, null, {
onStep: (direction) => arrowGameRef.current?.handleStep(direction),
})
@ -144,7 +144,7 @@ export function DebugApp() {
thresholdZ={settings.stepThresholdZ}
reset={settings.stepReset}
/>
<Pose3DView landmarks={worldLandmarks} />
<Pose3DView landmarks={worldLandmarks} landmarks2D={landmarks2D} />
<button
type="button"
className="copy-log-btn"

View File

@ -37,7 +37,7 @@ function drawGraph(
const allVals = samples.flatMap((s) => [getLeft(s), getRight(s)])
const maxAbs = Math.max(threshold * 1.5, ...allVals.map(Math.abs))
const toY = (v: number) => h / 2 - (v / maxAbs) * (h / 2 - 4)
const toY = (v: number) => h / 2 + (v / maxAbs) * (h / 2 - 4)
const toX = (i: number) => (i / (samples.length - 1)) * w
// Highlight active step segments

View File

@ -1,9 +1,10 @@
import { useEffect, useRef } from 'react'
import type { Landmark3D } from '../gesture/types'
import type { Landmark2D, Landmark3D } from '../gesture/types'
import { PoseLandmarker } from '@mediapipe/tasks-vision'
interface Props {
landmarks: Landmark3D[]
landmarks2D?: Landmark2D[]
size?: number
}
@ -14,8 +15,8 @@ type View = {
}
const VIEWS: View[] = [
{ title: 'FRONT (X\u00b7Y)', getX: (l) => l.x, getY: (l) => -l.y },
{ title: 'SIDE (Z\u00b7Y)', getX: (l) => l.z, getY: (l) => -l.y },
{ title: 'FRONT (X\u00b7Y)', getX: (l) => l.x, getY: (l) => l.y },
{ title: 'SIDE (Z\u00b7Y)', getX: (l) => l.z, getY: (l) => l.y },
{ title: 'TOP (X\u00b7Z)', getX: (l) => l.x, getY: (l) => l.z },
]
@ -74,11 +75,13 @@ function drawView(
return
}
const VIS_THRESHOLD = 0.3
const visible = landmarks.map((l) => (l.visibility ?? 1) >= VIS_THRESHOLD)
const projected = landmarks.map((l) => ({ x: view.getX(l), y: view.getY(l) }))
// Auto-scale: fit skeleton into canvas with padding
const xs = projected.map((p) => p.x)
const ys = projected.map((p) => p.y)
// Auto-scale: fit only visible landmarks into canvas with padding
const xs = projected.filter((_, i) => visible[i]).map((p) => p.x)
const ys = projected.filter((_, i) => visible[i]).map((p) => p.y)
const minX = Math.min(...xs), maxX = Math.max(...xs)
const minY = Math.min(...ys), maxY = Math.max(...ys)
const range = Math.max(maxX - minX, maxY - minY) || 0.001
@ -105,6 +108,7 @@ function drawView(
// Connections
ctx.lineWidth = 1.5
for (const conn of PoseLandmarker.POSE_CONNECTIONS) {
if (!visible[conn.start] || !visible[conn.end]) continue
const a = toScreen(projected[conn.start])
const b = toScreen(projected[conn.end])
ctx.strokeStyle = connectionColor(conn.start, conn.end)
@ -116,15 +120,13 @@ function drawView(
// Landmarks
for (let i = 0; i < projected.length; i++) {
if (!visible[i]) continue
const p = toScreen(projected[i])
const vis = landmarks[i].visibility ?? 1
ctx.globalAlpha = vis < 0.3 ? 0.2 : 1
ctx.fillStyle = landmarkColor(i)
ctx.beginPath()
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2)
ctx.fill()
}
ctx.globalAlpha = 1
// Title
ctx.font = 'bold 14px monospace'
@ -132,10 +134,13 @@ function drawView(
ctx.fillText(view.title, 8, 20)
}
export function Pose3DView({ landmarks, size = 220 }: Props) {
const VIEW_2D: View = { title: '2D (X\u00b7Y)', getX: (l) => l.x, getY: (l) => l.y }
export function Pose3DView({ landmarks, landmarks2D, size = 220 }: Props) {
const ref0 = useRef<HTMLCanvasElement>(null)
const ref1 = useRef<HTMLCanvasElement>(null)
const ref2 = useRef<HTMLCanvasElement>(null)
const ref3 = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const refs = [ref0, ref1, ref2]
@ -145,7 +150,12 @@ export function Pose3DView({ landmarks, size = 220 }: Props) {
const ctx = canvas.getContext('2d')!
drawView(ctx, size, size, landmarks, VIEWS[i])
}
}, [landmarks, size])
if (ref3.current && landmarks2D) {
const lm3d = landmarks2D.map((l) => ({ x: l.x, y: l.y, z: 0, visibility: l.visibility }))
drawView(ref3.current.getContext('2d')!, size, size, lm3d, VIEW_2D)
}
}, [landmarks, landmarks2D, size])
return (
<div style={{
@ -159,6 +169,7 @@ export function Pose3DView({ landmarks, size = 220 }: Props) {
<canvas ref={ref0} width={size} height={size} />
<canvas ref={ref1} width={size} height={size} />
<canvas ref={ref2} width={size} height={size} />
<canvas ref={ref3} width={size} height={size} />
</div>
)
}

View File

@ -14,23 +14,25 @@ export class GestureTelemetry {
sample(engine: GestureEngine, worldLandmarks: Landmark3D[]): void {
const now = performance.now()
// Step graph + log (only during gameplay with baselines)
if (engine.calibrationStatus === 'ready' && worldLandmarks?.length >= 33) {
// Step graph + log (sample whenever we have landmarks)
if (worldLandmarks?.length >= 33) {
const heelBaseL = engine.heelBaseL
const heelBaseR = engine.heelBaseR
if (heelBaseL && heelBaseR) {
const lt = worldLandmarks[31], rt = worldLandmarks[32]
const sample: StepGraphSample = {
time: now,
leftX: lt.x - heelBaseL.x,
rightX: rt.x - heelBaseR.x,
leftZ: lt.z - heelBaseL.z,
rightZ: rt.z - heelBaseR.z,
activeStep: engine.activeStep,
}
this.stepGraph.push(sample)
if (this.stepGraph.length > GRAPH_HISTORY) this.stepGraph.shift()
const lt = worldLandmarks[31], rt = worldLandmarks[32]
const baseL = heelBaseL ?? { x: lt.x, z: lt.z }
const baseR = heelBaseR ?? { x: rt.x, z: rt.z }
const sample: StepGraphSample = {
time: now,
leftX: lt.x - baseL.x,
rightX: rt.x - baseR.x,
leftZ: lt.z - baseL.z,
rightZ: rt.z - baseR.z,
activeStep: engine.activeStep,
}
this.stepGraph.push(sample)
if (this.stepGraph.length > GRAPH_HISTORY) this.stepGraph.shift()
if (heelBaseL && heelBaseR) {
this.stepLog.push({
t: now,
ltX: lt.x, ltZ: lt.z, rtX: rt.x, rtZ: rt.z,

View File

@ -18,6 +18,7 @@ export interface PoseLoopState {
calibrationStatus: CalibrationStatus
framingHint: string | null
stepGraphSamples: StepGraphSample[]
landmarks2D: Landmark2D[]
worldLandmarks: Landmark3D[]
engine: GestureEngine | null
telemetry: GestureTelemetry | null
@ -52,6 +53,7 @@ export function usePoseLoop(
const [calibrationStatus, setCalibrationStatus] = useState<CalibrationStatus>('framing')
const [framingHint, setFramingHint] = useState<string | null>(null)
const [stepGraphSamples, setStepGraphSamples] = useState<StepGraphSample[]>([])
const [landmarks2DState, setLandmarks2DState] = useState<Landmark2D[]>([])
const [worldLandmarksState, setWorldLandmarksState] = useState<Landmark3D[]>([])
const engineRef = useRef<GestureEngine | null>(null)
@ -217,6 +219,7 @@ export function usePoseLoop(
setFramingHint(engine.framingHint)
if (frameCount % 3 === 0) {
setStepGraphSamples([...telemetry.stepGraph])
if (landmarks2D.length >= 33) setLandmarks2DState([...landmarks2D])
if (worldLandmarks.length >= 33) setWorldLandmarksState([...worldLandmarks])
}
@ -251,7 +254,7 @@ export function usePoseLoop(
}, [settings, fileUrl]) // eslint-disable-line react-hooks/exhaustive-deps
return {
loading, error, fps, gesture, calibrationStatus, framingHint, stepGraphSamples, worldLandmarks: worldLandmarksState,
loading, error, fps, gesture, calibrationStatus, framingHint, stepGraphSamples, landmarks2D: landmarks2DState, worldLandmarks: worldLandmarksState,
engine: engineRef.current,
telemetry: telemetryRef.current,
videoSource: videoSourceRef.current,