1import { expect, it, vi } from "vite-plus/test";
2import { Quaternion, Vector3 } from "three";
3import { createDeviceMotion } from "./deviceMotion.ts";
4import { nearestDeviceView } from "./deviceViewSnap.ts";
5
6const snaps = ["portrait", "landscape_left", "portrait_upside_down", "landscape_right"].map(
7 (orientation, index) => ({
8 orientation,
9 rotation: new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), (-index * Math.PI) / 2),
10 yawLimit: Math.PI / 3,
11 }),
12);
13const rotation = (x: number, y: number, z = 0) =>
14 new Quaternion().setFromAxisAngle(new Vector3(x, y, z).normalize(), Math.hypot(x, y, z));
15
16it("selects the nearest quarter turn, retains a nearby yaw, and treats quaternion signs identically", () => {
17 const side = new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), -Math.PI / 2);
18 const released = side.clone().premultiply(rotation(0, 0.4));
19 const nearest = nearestDeviceView(released, snaps)!;
20 expect(nearest.orientation).toBe("landscape_left");
21 expect(nearest.rotation.angleTo(released)).toBeLessThan(1e-6);
22 const negative = released.clone().set(-released.x, -released.y, -released.z, -released.w);
23 expect(nearestDeviceView(negative, snaps)!.rotation.angleTo(nearest.rotation)).toBeLessThan(1e-6);
24 expect(
25 nearestDeviceView(rotation(0, 3), snaps)!.rotation.angleTo(new Quaternion()),
26 ).toBeLessThanOrEqual(Math.PI / 3 + 1e-6);
27});
28
29it("a click without dragging resumes the interrupted resting view instead of stranding the device", () => {
30 const choose = vi.fn(() => new Quaternion());
31 const motion = createDeviceMotion({ choose });
32 const rest = rotation(0, 0.8);
33 motion.setPose(rest, 0);
34 motion.advance(50);
35 motion.dragActive(true, 50);
36 motion.advance(60);
37 motion.dragActive(false, 60);
38 motion.advance(2100);
39 expect(motion.rotation.angleTo(rest)).toBeLessThan(1e-6);
40 expect(choose).not.toHaveBeenCalled();
41 expect(motion.needsFrame()).toBe(false);
42});