Spaces:
Running
Running
| // Pure math helpers. Kept SDK-free so every layer except robot/real.ts | |
| // (the single SDK-touching file) can use them in any test environment. | |
| // Conventions match the SDK/daemon wire format: rotation is intrinsic | |
| // ZYX (yaw about Z, then pitch about Y, then roll about X), matrices are | |
| // row-major 4×4 homogeneous, angles on the wire are radians. A unit test | |
| // cross-validates rpyToMatrixDeg/matrixToRpyDeg against the SDK's own | |
| // rpyToMatrix/matrixToRpy. | |
| export function degToRad(deg: number): number { | |
| return (deg * Math.PI) / 180; | |
| } | |
| export function radToDeg(rad: number): number { | |
| return (rad * 180) / Math.PI; | |
| } | |
| export function clamp(v: number, lo: number, hi: number): number { | |
| return Math.min(hi, Math.max(lo, v)); | |
| } | |
| export type Flat16 = number[]; | |
| /** Accepts a nested 4×4 (recorded-move JSON) or an already-flat array. */ | |
| export function flat16(m: number[][] | number[]): Flat16 { | |
| if (typeof m[0] === "number") return m as number[]; | |
| return (m as number[][]).flat(); | |
| } | |
| export function nest16(m: number[][] | number[]): number[][] { | |
| if (typeof m[0] !== "number") return m as number[][]; | |
| const f = m as number[]; | |
| return [f.slice(0, 4), f.slice(4, 8), f.slice(8, 12), f.slice(12, 16)]; | |
| } | |
| /** Degrees → flat row-major 4×4 rotation matrix (ZYX intrinsic). */ | |
| export function rpyToMatrixDeg(rollDeg: number, pitchDeg: number, yawDeg: number): Flat16 { | |
| const r = degToRad(rollDeg); | |
| const p = degToRad(pitchDeg); | |
| const y = degToRad(yawDeg); | |
| const cr = Math.cos(r), sr = Math.sin(r); | |
| const cp = Math.cos(p), sp = Math.sin(p); | |
| const cy = Math.cos(y), sy = Math.sin(y); | |
| return [ | |
| cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr, 0, | |
| sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr, 0, | |
| -sp, cp * sr, cp * cr, 0, | |
| 0, 0, 0, 1, | |
| ]; | |
| } | |
| export interface RpyDeg { | |
| roll: number; | |
| pitch: number; | |
| yaw: number; | |
| } | |
| /** Flat or nested 4×4 rotation matrix → degrees (ZYX intrinsic). */ | |
| export function matrixToRpyDeg(m: number[][] | number[]): RpyDeg { | |
| const f = flat16(m); | |
| const m00 = f[0]!, m10 = f[4]!, m20 = f[8]!, m21 = f[9]!, m22 = f[10]!; | |
| return { | |
| roll: radToDeg(Math.atan2(m21, m22)), | |
| pitch: radToDeg(-Math.asin(clamp(m20, -1, 1))), | |
| yaw: radToDeg(Math.atan2(m10, m00)), | |
| }; | |
| } | |
| /** | |
| * The daemon's head matrix is world-frame: to keep the head pointing | |
| * "straight ahead relative to the body" while the body turns, the head | |
| * matrix must carry body yaw + local head yaw. | |
| */ | |
| export function worldHeadYawDeg(bodyYawDeg: number, headYawLocalDeg: number): number { | |
| return bodyYawDeg + headYawLocalDeg; | |
| } | |