import { parseFreeMoCapCsv } from '../src/studio/freemocapImport.js'; import { done, ok, section } from './harness.mjs'; const points = [ 'left_ear', 'right_ear', 'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow', 'left_wrist', 'right_wrist', 'left_hip', 'right_hip', 'left_knee', 'right_knee', 'left_ankle', 'right_ankle', 'left_heel', 'right_heel', 'left_foot_index', 'right_foot_index', ]; section('FreeMoCap tidy XYZ data imports into MediaPipe landmark order'); { const lines = ['frame,keypoint,x,y,z,model,trajectory,reprojection_error']; for (const trajectory of ['3d_xyz', 'rigid_3d_xyz']) { for (const frame of [0, 1]) { for (const [index, point] of points.entries()) { const base = trajectory === 'rigid_3d_xyz' ? 200 : 100; lines.push(`${frame},${point},${base + index},20,300,mediapipe_body,${trajectory},0.5`); } } } // Non-body rows in the tidy all-data file must not contaminate the pose. lines.push('0,left_wrist,999,999,999,mediapipe_left_hand,rigid_3d_xyz,0.5'); const result = parseFreeMoCapCsv(lines.join('\n')); ok(result.frames.length === 2, 'all complete body frames import'); ok(result.trajectory === 'rigid_3d_xyz', 'rigid body trajectory is preferred when available'); ok(result.tracker === 'MediaPipe', 'tracker metadata is reported'); const leftEar = result.frames[0].landmarks[7]; ok(leftEar.x === 200 && leftEar.y === -300 && leftEar.z === -20, 'FreeMoCap Z-up axes convert for the Tilt retargeter'); } section('FreeMoCap per-trajectory CSV and RTMPose toe names are accepted'); { const rtmposePoints = points.map((point) => point.replace('foot_index', 'big_toe')); const lines = ['frame,keypoint,x,y,z']; for (const [index, point] of rtmposePoints.entries()) lines.push(`4,${point},${index},10,20`); const result = parseFreeMoCapCsv(lines.join('\r\n')); ok(result.frames[0].frame === 4, 'source frame numbers survive import'); ok(result.frames[0].landmarks[31].visibility === 1, 'RTMPose big toe maps to the Tilt foot-index landmark'); } done('FreeMoCap import');