import assert from "node:assert/strict"; import { after, before, test } from "node:test"; import { createServer } from "vite"; let server, createRecordedRerunCameraJournal, initialEye; before(async () => { server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true } }); const module = await server.ssrLoadModule( "/src/components/rerun/recordedRerunCameraJournal.ts"); createRecordedRerunCameraJournal = module.createRecordedRerunCameraJournal; initialEye = module.RECORDED_RERUN_ORBITAL_EYE; }); after(async () => { await server?.close(); }); class FakeEvent { constructor(type, init = {}) { this.type = type; Object.assign(this, init); } } class FakeTarget { listeners = new Map(); emitted = []; addEventListener(type, listener) { const listeners = this.listeners.get(type) ?? []; listeners.push(listener); this.listeners.set(type, listeners); } removeEventListener(type, listener) { this.listeners.set(type, (this.listeners.get(type) ?? []).filter(item => item !== listener)); } dispatchEvent(event) { this.emitted.push(event); for (const listener of this.listeners.get(event.type) ?? []) listener(event); return true; } } const radius = (eye) => Math.hypot( eye.position[0] - eye.lookTarget[0], eye.position[1] - eye.lookTarget[1], eye.position[2] - eye.lookTarget[2], ); test("recorded orbital eye tracks only 3D viewport navigation", () => { const scope = new FakeTarget(); const timers = []; Object.assign(scope, { WheelEvent: FakeEvent, requestAnimationFrame(callback) { callback(); return 1; }, setTimeout(callback) { timers.push(callback); return timers.length; }, }); const canvas = new FakeTarget(); Object.assign(canvas, { clientHeight: 200, isConnected: true, getBoundingClientRect: () => ({ left: 100, right: 500, top: 50, bottom: 250, width: 400, height: 200, }), }); const journal = createRecordedRerunCameraJournal(canvas, scope); journal.configure(initialEye, 0.46); const pointer = (type, x, y, buttons) => new FakeEvent(type, { clientX: x, clientY: y, button: 0, buttons, pointerId: 7, pointerType: "mouse", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, }); canvas.dispatchEvent(pointer("pointerdown", 200, 100, 1)); scope.dispatchEvent(pointer("pointermove", 240, 120, 1)); scope.dispatchEvent(pointer("pointerup", 240, 120, 0)); assert.deepEqual(journal.current(), initialEye); canvas.dispatchEvent(pointer("pointerdown", 380, 100, 1)); scope.dispatchEvent(pointer("pointermove", 420, 120, 1)); scope.dispatchEvent(pointer("pointerup", 420, 120, 0)); const rotated = journal.current(); assert.notDeepEqual(rotated.position, initialEye.position); assert.ok(Math.abs(radius(rotated) - radius(initialEye)) < 1e-9); journal.setMaxOrbitalRadius(40); scope.dispatchEvent(new FakeEvent("wheel", { clientX: 380, clientY: 130, deltaX: 0, deltaY: 2_000, deltaMode: 0, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, })); assert.ok(Math.abs(radius(journal.current()) - 40) < 1e-9); journal.configure(rotated, 0.46); scope.dispatchEvent(new FakeEvent("wheel", { clientX: 0, clientY: 0, deltaX: 0, deltaY: -20, deltaMode: 0, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, })); const zoomed = journal.current(); assert.ok(Math.abs(radius(zoomed) - radius(rotated) * Math.exp(-20 / 200)) < 1e-9); const snapshot = journal.current(); journal.setSpatialViewportStart(0.8); scope.dispatchEvent(new FakeEvent("wheel", { clientX: 380, clientY: 130, deltaX: 0, deltaY: -20, deltaMode: 0, })); assert.deepEqual(journal.current(), snapshot); journal.dispose(); });