import assert from 'node:assert/strict'; import { existsSync, readFileSync } from 'node:fs'; import { test } from 'node:test'; import vm from 'node:vm'; import ts from 'typescript'; import * as THREE from 'three'; const path = new URL('../src/input/SceneZoomController.ts', import.meta.url); assert.ok(existsSync(path), 'anchored scene projection controller is required'); const exports = {}; vm.runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText, { exports, require: () => THREE }); const { SceneZoomController } = exports; const close = (actual, expected, label = '') => assert.ok(Math.abs(actual - expected) < 1e-10, `${label}: ${actual} != ${expected}`); const setup = () => { const camera = new THREE.PerspectiveCamera(40, 1.6, .1, 100); camera.position.set(.2, -.1, 3); camera.lookAt(0, 0, 0); camera.updateMatrixWorld(true); return { camera, zoom: new SceneZoomController(camera) }; }; const pointAt = (camera, anchor, depth = .3) => new THREE.Vector3(anchor.x, anchor.y, depth).unproject(camera); const expectAt = (camera, point, anchor) => { const p = point.clone().project(camera); close(p.x, anchor.x, 'x anchor'); close(p.y, anchor.y, 'y anchor'); }; test('all depths stay below an off-center cursor under the common projection', () => { for (const anchor of [{ x: .6, y: .2 }, { x: -1, y: 1 }, { x: 1, y: -1 }, { x: 0, y: 0 }]) { const { camera, zoom } = setup(); const position = camera.position.clone(), orientation = camera.quaternion.clone(); const points = [-.7, .2, .99].map(depth => pointAt(camera, anchor, depth)); zoom.setZoom(2.4, anchor); points.forEach(point => expectAt(camera, point, anchor)); assert.deepEqual(camera.position, position); assert.deepEqual(camera.quaternion.toArray(), orientation.toArray()); assert.equal(camera.zoom, 2.4); } }); test('moving pinch midpoint carries its content to the new midpoint', () => { const { camera, zoom } = setup(); zoom.setZoom(2); const from = { x: -.2, y: .1 }, to = { x: .2, y: -.15 }, point = pointAt(camera, from); zoom.setZoom(2.6, from, to); expectAt(camera, point, to); }); test('pinch translation works at fixed zoom and at the maximum clamp', () => { for (const requested of [3, 300]) { const { camera, zoom } = setup(); zoom.setZoom(3); const from = { x: 0, y: 0 }, to = { x: .2, y: .3 }, point = pointAt(camera, from); zoom.setZoom(requested, from, to); expectAt(camera, point, to); assert.equal(zoom.value, 3); } }); test('clamping zoom uses the actual ratio rather than an overshooting request', () => { const { camera, zoom } = setup(); zoom.setZoom(2); const anchor = { x: .7, y: -.6 }, point = pointAt(camera, anchor); zoom.setZoom(80, anchor); expectAt(camera, point, anchor); }); test('zoom and offset are bounded, and zoom1 restores the full centered view', () => { const { camera, zoom } = setup(); zoom.setZoom(3, { x: 0, y: 0 }, { x: 99, y: -99 }); assert.deepEqual(zoom.offset.toArray(), [2, -2]); zoom.setZoom(.1, { x: -.2, y: .3 }); assert.equal(zoom.value, 1); assert.deepEqual(zoom.offset.toArray(), [0, 0]); const neutral = new THREE.PerspectiveCamera(40, 1.6, .1, 100); assert.deepEqual(camera.projectionMatrix.elements, neutral.projectionMatrix.elements); }); test('repeated anchored in/out stays stable with no accumulated drift', () => { const { camera, zoom } = setup(), anchor = { x: .8, y: -.7 }, point = pointAt(camera, anchor); for (let i = 0; i < 100; i++) { zoom.setZoom(2.7, anchor); expectAt(camera, point, anchor); zoom.setZoom(1, anchor); expectAt(camera, point, anchor); } assert.deepEqual(zoom.offset.toArray(), [0, 0]); }); test('resize reapplies normalized offset and refreshes the inverse matrix', () => { const { camera, zoom } = setup(); zoom.setZoom(2, { x: .5, y: -.25 }); const offset = zoom.offset.clone(); camera.aspect = .5; zoom.updateProjection(); assert.deepEqual(zoom.offset, offset); close(camera.projectionMatrix.elements[8], -offset.x); close(camera.projectionMatrix.elements[9], -offset.y); const product = camera.projectionMatrix.clone().multiply(camera.projectionMatrixInverse); product.elements.forEach((value, i) => close(value, i % 5 === 0 ? 1 : 0)); }); test('nonfinite requests and anchors cannot corrupt projection state', () => { const { camera, zoom } = setup(); zoom.setZoom(2, { x: .3, y: .2 }); const matrix = camera.projectionMatrix.clone(); for (const value of [NaN, Infinity, -Infinity]) zoom.setZoom(value); zoom.setZoom(2.5, { x: NaN, y: 0 }); zoom.setZoom(2.5, { x: 0, y: 0 }, { x: Infinity, y: 0 }); assert.deepEqual(camera.projectionMatrix, matrix); });