1import { Box3, Mesh, PerspectiveCamera, Texture, Vector3 } from "three";
2import { describe, expect, it } from "vite-plus/test";
3import { createAndroidFoldScene } from "./androidFoldScene.ts";
4import { phoneDisplayLayout } from "./phoneScene.ts";
5
6describe("Android fold scene", () => {
7 it("moves one physical half around the hinge while preserving both screen halves", () => {
8 const texture = new Texture();
9 const scene = createAndroidFoldScene(
10 texture,
11 phoneDisplayLayout({ width: 2200, height: 1840, orientation: "landscape_left" }, 2200, 1840),
12 180,
13 );
14 const moving = scene.orientation.children[0]!;
15 const fixed = scene.orientation.children[1]!;
16 const openWidth = new Box3().setFromObject(scene.root).getSize(new Vector3()).x;
17 const leftScreen = scene.root.getObjectByName("left-inner-screen") as Mesh;
18 const rightScreen = scene.root.getObjectByName("right-inner-screen") as Mesh;
19 leftScreen.geometry.computeBoundingBox();
20 rightScreen.geometry.computeBoundingBox();
21 const creaseWidth =
22 rightScreen.geometry.boundingBox!.min.x - leftScreen.geometry.boundingBox!.max.x;
23 expect(creaseWidth).toBeLessThan(0.01);
24 const continuousScreen = scene.root.getObjectByName("continuous-inner-screen") as Mesh;
25 const positions = continuousScreen.geometry.getAttribute("position");
26 expect(continuousScreen.geometry.index).not.toBeNull();
27 expect(Array.from({ length: positions.count }, (_, i) => positions.getX(i))).toContain(0);
28 scene.setAngle(90);
29 expect(moving.rotation.y).toBeCloseTo(Math.PI / 2);
30 expect(fixed.rotation.y).toBe(0);
31 scene.setAngle(0);
32 const closedWidth = new Box3().setFromObject(scene.root).getSize(new Vector3()).x;
33 expect(closedWidth).toBeLessThan(openWidth * 0.7);
34 expect(scene.root.getObjectByName("cover-screen")?.visible).toBe(true);
35 scene.dispose();
36 texture.dispose();
37 });
38
39 it("maps touches on each open half and the closed cover to the live frame", () => {
40 const texture = new Texture();
41 const scene = createAndroidFoldScene(texture, phoneDisplayLayout(null, 2200, 1840), 180);
42 const camera = new PerspectiveCamera(32, 1, 0.1, 30);
43 camera.position.z = 6;
44 camera.updateMatrixWorld(true);
45 const project = (x: number) => {
46 const point = new Vector3(x, 0, 0.041).project(camera);
47 return scene.screenPoint((point.x + 1) / 2, (1 - point.y) / 2, camera);
48 };
49 expect(project(-0.52)?.x).toBeCloseTo(0.25, 1);
50 expect(project(0.52)?.x).toBeCloseTo(0.75, 1);
51 expect(scene.screenPoint(0.99, 0.5, camera, true)?.x).toBe(1);
52 scene.setAngle(0);
53 expect(project(0.52)?.x).toBeCloseTo(0.5, 1);
54 scene.dispose();
55 texture.dispose();
56 });
57
58 it("shapes the inner display to the raw frame, portrait or landscape", () => {
59 const texture = new Texture();
60 for (const [width, height] of [
61 [2076, 2152],
62 [2208, 1840],
63 ] as const) {
64 const scene = createAndroidFoldScene(
65 texture,
66 phoneDisplayLayout(null, width, height),
67 180,
68 width / height,
69 );
70 const screen = scene.root.getObjectByName("continuous-inner-screen") as Mesh;
71 screen.geometry.computeBoundingBox();
72 const size = screen.geometry.boundingBox!.getSize(new Vector3());
73 expect(size.x / size.y).toBeCloseTo(width / height, 2);
74 scene.dispose();
75 }
76 texture.dispose();
77 });
78});