import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { test } from 'node:test'; import ts from 'typescript'; import * as THREE from 'three'; import {loadTypescript} from './support/load-typescript.mjs'; const {CameraController}=await loadTypescript(new URL('../src/input/CameraController.ts',import.meta.url)); test('camera absolute input rejects invalid readings, clamps, and smooths at default .75', () => { const camera = new THREE.PerspectiveCamera(), controller = new CameraController(camera); controller.setView(3, -3); assert.deepEqual(controller.target.toArray(), [1, -1]); controller.setView(NaN, 0); assert.deepEqual(controller.target.toArray(), [1, -1]); const first = controller.update(1 / 60, 1); assert.ok(first.x > 0 && first.x < 1); for (let i = 0; i < 120; i++) controller.update(1 / 60, 2); assert.equal(camera.position.x, .75); assert.equal(camera.position.y, -.75); assert.equal(camera.position.z, 3); const before = controller.current.toArray(); controller.hold(); controller.update(.1, 2); assert.deepEqual(controller.current.toArray(), before); controller.setMaxOffset(NaN); controller.update(.1, 2); assert.equal(camera.position.x, .75); }); test('the existing camera smoothing is frame-rate independent', () => { const results = [30, 60, 120].map(fps => { const c = new CameraController(new THREE.PerspectiveCamera()); c.setView(.75, -.25); for (let i = 0; i < fps; i++) c.update(1 / fps, 1); return c.current.x; }); assert.ok(Math.max(...results) - Math.min(...results) < 1e-12); }); test('integration keeps scene zoom separate and freezes camera at pinch start', async () => { const main = await readFile(new URL('../src/main.ts', import.meta.url), 'utf8'); const html = await readFile(new URL('../index.html', import.meta.url), 'utf8'); assert.match(main, /if \(pinching\)\s*\{?\s*cameraController\.hold\(\)/); assert.match(main, /gyro\.setPaused\(experience\.isBlocked\(\) \|\| experience\.isPhotoAdjusting\(\) \|\| pointer\.isInteracting\)/); assert.match(main, /gyroButton\.onclick = \(\) => \{ if \(mobileTilt && !experience\.isBlocked\(\) && !experience\.isPhotoAdjusting\(\)\) void gyro\.toggle\(\); \}/); assert.match(main, /gyroRecenter\.addEventListener\('click', \(\) => \{ if\(experience\.isBlocked\(\)\|\|experience\.isPhotoAdjusting\(\)\)return;cameraController\.reset\(\); gyro\.reset\(\); \}/); assert.match(main, /gyro\.syncManualView\(\)/); assert.match(html, /id="gyro"[^>]*aria-pressed="false"[^>]*>[\s\S]*?id="gyro-label">기울기 연결/); assert.match(html, /id="gyro-status"[^>]*role="status"/); assert.match(html, /센서값은 이 기기 안에서만/); assert.match(main, /gyro\.dispose\(\)/); }); test('real pointer, gyro and camera controllers freeze pending smoothing and resume manual view', async () => { async function controllerModule(name) { const text = await readFile(new URL(`../src/input/${name}.ts`, import.meta.url), 'utf8'); const output = ts.transpileModule(text, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ES2020 } }).outputText; return import(`data:text/javascript;base64,${Buffer.from(output).toString('base64')}`); } const { PointerController } = await controllerModule('PointerController'); const { GyroController } = await controllerModule('GyroController'); class Events { handlers = new Map(); captures = new Set(); classList = { add() {}, remove() {} }; addEventListener(type, fn) { if (!this.handlers.has(type)) this.handlers.set(type, new Set()); this.handlers.get(type).add(fn); } removeEventListener(type, fn) { this.handlers.get(type)?.delete(fn); } emit(type, init = {}) { for (const fn of [...(this.handlers.get(type) ?? [])]) fn({ pointerType: 'touch', pointerId: 1, clientX: 0, clientY: 0, ...init }); } setPointerCapture(id) { this.captures.add(id); } hasPointerCapture(id) { return this.captures.has(id); } releasePointerCapture(id) { this.captures.delete(id); } getBoundingClientRect() { return { left: 0, top: 0, width: 200, height: 100 }; } } const win = new Events(), doc = new Events(), element = new Events(), orientation = new Events(); const timers = new Map(); let timer = 0; Object.assign(win, { screen: { orientation }, DeviceOrientationEvent: {}, isSecureContext: true, setTimeout: fn => { timers.set(++timer, fn); return timer; }, clearTimeout: id => timers.delete(id) }); orientation.angle = 0; doc.hidden = false; globalThis.window = win; globalThis.document = doc; const camera = new THREE.PerspectiveCamera(), controller = new CameraController(camera); const gyro = new GyroController((x, y) => controller.setView(x, y), () => {}, () => ({ x: controller.target.x, y: controller.target.y })); let zoom = 1; const pointer = new PointerController(element, (x, y) => { controller.add(x, y); gyro.syncManualView(); }, undefined, { getZoom: () => zoom, setZoom: value => { zoom = value; }, onGestureChange: (active, pinching) => { if (pinching) controller.hold(); gyro.setPaused(active); }, }); const reading = (beta, gamma) => win.emit('deviceorientation', { alpha: 0, beta, gamma }); await gyro.toggle(); reading(0, 0); reading(0, 30); controller.update(1 / 60, 1); assert.ok(controller.target.x > controller.current.x, 'smoothing was still in flight'); element.emit('pointerdown'); element.emit('pointerdown', { pointerId: 2, clientX: 100 }); const held = camera.position.clone(); reading(45, -30); element.emit('pointermove', { pointerId: 2, clientX: 150 }); assert.equal(zoom, 1.5); for (let i = 0; i < 20; i++) controller.update(1 / 60, 1); assert.deepEqual(camera.position.toArray(), held.toArray(), 'pinch freezes real camera, including remaining smoothing'); element.emit('pointerup', { pointerId: 2 }); element.emit('pointermove', { clientX: 20 }); const manual = controller.target.clone(); assert.ok(manual.x > held.x / .75); element.emit('pointerup'); reading(45, -30); assert.deepEqual(controller.target.toArray(), manual.toArray(), 'first sensor sample preserves surviving-finger drag'); controller.add(-.06, 0); gyro.syncManualView(); const keyboard = controller.target.clone(); reading(50, -25); assert.deepEqual(controller.target.toArray(), keyboard.toArray(), 'first sensor sample preserves keyboard view'); pointer.dispose(); gyro.dispose(); assert.equal(timers.size, 0); });