import assert from 'node:assert/strict'; import test from 'node:test'; import { applyHeadPositionLock, applySbsTextureWindow, hideContentMeshes, isLeftEyeCamera, positionPlaneForPresentation, resetHeadPositionLockedContent, shouldLockContentToHeadPosition, showActiveContentMesh } from '../vr180player/rendering/projection.js'; function createMaterial() { return { map: { offset: { x: 99, y: 99 }, repeat: { x: 99, y: 99 } } }; } function createRenderer({ isPresenting = false, xrCamera = null } = {}) { return { xr: { getCamera: () => xrCamera, isPresenting } }; } function createCamera(x, projectionOffset = 0) { return { matrixWorldInverse: { elements: new Array(16).fill(0).with(12, x) }, matrixWorld: { elements: new Array(16).fill(0).with(12, x).with(13, 1.7).with(14, 0.25) }, projectionMatrix: { elements: new Array(16).fill(0).with(8, projectionOffset) } }; } function createPositionedMesh() { const calls = []; return { calls, position: { set: (...args) => calls.push(args) } }; } test('applySbsTextureWindow uses left eye only in non-XR 2D fallback', () => { const material = createMaterial(); applySbsTextureWindow(createRenderer(), createCamera(0), material, true); assert.equal(material.map.offset.x, 0); assert.equal(material.map.repeat.x, 0.5); assert.equal(material.map.offset.y, 0); assert.equal(material.map.repeat.y, 1); }); test('applySbsTextureWindow keeps the full texture outside XR when not in 2D fallback', () => { const material = createMaterial(); applySbsTextureWindow(createRenderer(), createCamera(0), material, false); assert.equal(material.map.offset.x, 0); assert.equal(material.map.repeat.x, 1); assert.equal(material.map.offset.y, 0); assert.equal(material.map.repeat.y, 1); }); test('applySbsTextureWindow selects the right SBS half for the right XR eye', () => { const leftCamera = createCamera(-0.03); const rightCamera = createCamera(0.03); const renderer = createRenderer({ isPresenting: true, xrCamera: { cameras: [leftCamera, rightCamera] } }); const material = createMaterial(); applySbsTextureWindow(renderer, rightCamera, material, false); assert.equal(material.map.offset.x, 0.5); assert.equal(material.map.repeat.x, 0.5); }); test('isLeftEyeCamera falls back to projection matrix offset when XR cameras are unavailable', () => { assert.equal(isLeftEyeCamera(createRenderer({ isPresenting: true }), createCamera(0, -0.1)), true); assert.equal(isLeftEyeCamera(createRenderer({ isPresenting: true }), createCamera(0, 0.1)), false); }); test('showActiveContentMesh hides inactive meshes before showing the active mesh', () => { const vr180Mesh = { visible: true }; const planeMesh = { visible: true }; showActiveContentMesh(vr180Mesh, planeMesh, planeMesh); assert.equal(vr180Mesh.visible, false); assert.equal(planeMesh.visible, true); hideContentMeshes(vr180Mesh, planeMesh); assert.equal(vr180Mesh.visible, false); assert.equal(planeMesh.visible, false); }); test('positionPlaneForPresentation uses the fallback camera depth in 2D plane mode', () => { const calls = []; const planeMesh = { position: { set: (...args) => calls.push(args) } }; positionPlaneForPresentation(planeMesh, { position: { z: 0.1 } }, true, 3, 1.2); positionPlaneForPresentation(planeMesh, { position: { z: 0.1 } }, false, 3, 1.2); assert.deepEqual(calls[0], [0, 1.6, -1.0999999999999999]); assert.deepEqual(calls[1], [0, 1.6, -3]); }); test('shouldLockContentToHeadPosition defaults to VR180 only in auto mode', () => { assert.equal(shouldLockContentToHeadPosition('auto', 'vr180'), true); assert.equal(shouldLockContentToHeadPosition('auto', 'plane'), false); assert.equal(shouldLockContentToHeadPosition('position', 'plane'), true); assert.equal(shouldLockContentToHeadPosition('none', 'vr180'), false); }); test('applyHeadPositionLock centers VR180 content on the XR camera position', () => { const mesh = createPositionedMesh(); applyHeadPositionLock(mesh, createCamera(0.4), 'vr180', true, 3); assert.deepEqual(mesh.calls[0], [0.4, 1.7, 0.25]); }); test('applyHeadPositionLock keeps opt-in plane content in front of the XR camera position', () => { const mesh = createPositionedMesh(); applyHeadPositionLock(mesh, createCamera(-0.25), 'plane', true, 3); assert.deepEqual(mesh.calls[0], [-0.25, 1.7, -2.75]); }); test('applyHeadPositionLock leaves content untouched when disabled', () => { const mesh = createPositionedMesh(); applyHeadPositionLock(mesh, createCamera(0.4), 'vr180', false, 3); assert.deepEqual(mesh.calls, []); }); test('resetHeadPositionLockedContent restores default mesh positions', () => { const vr180Mesh = createPositionedMesh(); const planeMesh = createPositionedMesh(); resetHeadPositionLockedContent(vr180Mesh, planeMesh, 3); assert.deepEqual(vr180Mesh.calls[0], [0, 0, 0]); assert.deepEqual(planeMesh.calls[0], [0, 1.6, -3]); });