File size: 2,537 Bytes
bac98f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Frame sampling over a REAL emotion from the library (committed fixture)
// — proves the recorded-move JSON → puppet trajectory pipeline on
// production data.

import { describe, expect, it } from "vitest";
import { sampleFrames } from "../../src/emotions/loader";
import type { EmotionMotion } from "../../src/robot/adapter";
import cheerful1 from "../fixtures/cheerful1.json";

const motion = cheerful1 as unknown as EmotionMotion;

describe("sampleFrames on cheerful1", () => {
  const frames = sampleFrames(motion);

  it("keeps every recorded frame with monotonic timestamps", () => {
    expect(frames).toHaveLength(motion.time.length);
    expect(frames[0]!.t).toBeCloseTo(0, 1);
    for (let i = 1; i < frames.length; i++) {
      expect(frames[i]!.t).toBeGreaterThanOrEqual(frames[i - 1]!.t);
    }
    // A real move lasts a few seconds.
    expect(frames[frames.length - 1]!.t).toBeGreaterThan(1);
    expect(frames[frames.length - 1]!.t).toBeLessThan(30);
  });

  it("produces plausible logical degrees", () => {
    for (const f of frames) {
      expect(Math.abs(f.headRoll)).toBeLessThanOrEqual(90);
      expect(Math.abs(f.headPitch)).toBeLessThanOrEqual(90);
      expect(Math.abs(f.headYaw)).toBeLessThanOrEqual(120);
      expect(Math.abs(f.bodyYaw)).toBeLessThanOrEqual(180);
      expect(Math.abs(f.antennaRight)).toBeLessThanOrEqual(180);
      expect(Math.abs(f.antennaLeft)).toBeLessThanOrEqual(180);
      for (const v of Object.values(f)) expect(Number.isFinite(v)).toBe(true);
    }
  });

  it("actually moves (not a degenerate constant trajectory)", () => {
    const pitches = frames.map((f) => f.headPitch);
    const span = Math.max(...pitches) - Math.min(...pitches);
    const antSpan =
      Math.max(...frames.map((f) => f.antennaLeft)) - Math.min(...frames.map((f) => f.antennaLeft));
    expect(span + antSpan).toBeGreaterThan(3);
  });

  it("holds previous values when a channel is missing", () => {
    const sparse: EmotionMotion = {
      time: [0, 0.1, 0.2],
      set_target_data: [
        { head: motion.set_target_data[0]!.head, antennas: [0.1, -0.1], body_yaw: 0.2 },
        {}, // nothing recorded this tick
        { antennas: [0.3, -0.3] },
      ],
    };
    const out = sampleFrames(sparse);
    expect(out[1]).toMatchObject({
      headRoll: out[0]!.headRoll,
      antennaRight: out[0]!.antennaRight,
      bodyYaw: out[0]!.bodyYaw,
    });
    expect(out[2]!.antennaRight).not.toBe(out[1]!.antennaRight);
    expect(out[2]!.headPitch).toBe(out[1]!.headPitch);
  });
});