UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

289 lines (237 loc) 8.62 kB
/* @license * Copyright 2019 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {expect} from '@esm-bundle/chai'; import {Matrix4, PerspectiveCamera, Vector2, Vector3} from 'three'; import {$scene} from '../../model-viewer-base.js'; import {ModelViewerElement} from '../../model-viewer.js'; import {ARRenderer} from '../../three-components/ARRenderer.js'; import {ModelScene} from '../../three-components/ModelScene.js'; import {Renderer} from '../../three-components/Renderer.js'; import {waitForEvent} from '../../utilities.js'; import {assetPath} from '../helpers.js'; class MockXRFrame implements XRFrame { constructor(public session: XRSession) { } readonly predictedDisplayTime = 0; // We don't use nor test the returned XRPose other than its existence. getPose(_xrSpace: XRSpace, _frameOfRef: XRReferenceSpace) { return {} as XRPose; } getViewerPose(_referenceSpace?: XRReferenceSpace): XRViewerPose { // Rotate 180 degrees on Y (so it's not the default) // and angle 45 degrees towards the ground, like a phone. const matrix = new Matrix4() .identity() .makeRotationAxis(new Vector3(0, 1, 0), Math.PI) .multiply(new Matrix4().makeRotationAxis( new Vector3(1, 0, 0), -Math.PI / 4)); matrix.setPosition(10, 2, 3); const transform: XRRigidTransform = { matrix: matrix.elements as unknown as Float32Array, position: {} as DOMPointReadOnly, orientation: {} as DOMPointReadOnly, inverse: {} as XRRigidTransform }; const camera = new PerspectiveCamera(); const view: XRView = { eye: {} as XREye, projectionMatrix: camera.projectionMatrix.elements as unknown as Float32Array, transform: transform, requestViewportScale: (_scale: number|null) => {} }; const viewerPos: XRViewerPose = { transform: transform, views: [view], emulatedPosition: false }; return viewerPos; } getHitTestResults(_xrHitTestSource: XRHitTestSource) { return []; } getHitTestResultsForTransientInput(_hitTestSource: XRTransientInputHitTestSource) { return []; } } suite('ARRenderer', () => { let element: ModelViewerElement; let arRenderer: ARRenderer; let xrSession: XRSession; let inputSources: Array<XRInputSource> = []; const setInputSources = (sources: Array<XRInputSource>) => { inputSources = sources; }; const stubWebXrInterface = (arRenderer: ARRenderer) => { arRenderer.resolveARSession = async () => { class FakeSession extends EventTarget implements XRSession { public renderState: XRRenderState = { baseLayer: { getViewport: () => { return {x: 0, y: 0, width: 320, height: 240} as XRViewport } } as unknown as XRLayer } as XRRenderState; public hitTestSources: Set<XRHitTestSource> = new Set<XRHitTestSource>(); async updateRenderState(_object: any) { } requestFrameOfReference() { return {}; } async requestReferenceSpace(_type: XRReferenceSpaceType): Promise<XRReferenceSpace> { return {} as XRReferenceSpace; } get inputSources(): Array<XRInputSource> { return inputSources; } async requestHitTestSource(_options: XRHitTestOptionsInit): Promise<XRHitTestSource> { const result = {cancel: () => {}}; this.hitTestSources.add(result); return result; } async requestHitTestSourceForTransientInput( _options: XRTransientInputHitTestOptionsInit) { const result = {cancel: () => {}}; this.hitTestSources.add(result); return result; } requestAnimationFrame() { return 1; } cancelAnimationFrame() { } async end() { this.dispatchEvent(new CustomEvent('end')); } readonly environmentBlendMode = {} as XREnvironmentBlendMode; readonly visibilityState = {} as XRVisibilityState; readonly isSystemKeyboardSupported = false; async updateTargetFrameRate(_rate: number) { return; } onend() { } oninputsourceschange() { } onselect() { } onselectstart() { } onselectend() { } onsqueeze() { } onsqueezestart() { } onsqueezeend() { } onvisibilitychange() { } onframeratechange() { } } xrSession = new FakeSession(); return xrSession; }; }; setup(() => { element = new ModelViewerElement(); document.body.insertBefore(element, document.body.firstChild); arRenderer = Renderer.singleton.arRenderer; }); teardown(() => { if (element.parentNode != null) { element.parentNode.removeChild(element); } }); test('is not presenting if present has not been invoked', () => { expect(arRenderer.isPresenting).to.be.equal(false); }); suite('when presenting a scene', () => { let modelScene: ModelScene; let oldXRRay: any; setup(async () => { const sourceLoads = waitForEvent(element, 'poster-dismissed'); element.src = assetPath('models/Astronaut.glb'); await sourceLoads; modelScene = element[$scene]; stubWebXrInterface(arRenderer); setInputSources([]); oldXRRay = (window as any).XRRay; (window as any).XRRay = class MockXRRay implements XRRay { readonly origin = new DOMPointReadOnly; readonly direction = new DOMPointReadOnly; matrix = new Float32Array; constructor(_origin: DOMPointInit, _direction: DOMPointInit) { } } await arRenderer.present(modelScene); }); teardown(async () => { (window as any).XRRay = oldXRRay; await arRenderer.stopPresenting().catch(() => {}); }); test('presents the model at its natural scale', () => { const scale = modelScene.target.getWorldScale(new Vector3()); expect(scale.x).to.be.equal(1); expect(scale.y).to.be.equal(1); expect(scale.z).to.be.equal(1); }); suite('presentation ends', () => { setup(async () => { await arRenderer.stopPresenting(); }); test('restores the model to its natural scale', () => { const scale = modelScene.target.getWorldScale(new Vector3()); expect(scale.x).to.be.equal(1); expect(scale.y).to.be.equal(1); expect(scale.z).to.be.equal(1); }); test('restores original camera', () => { expect(modelScene.camera).to.be.equal(modelScene.camera); }); test('restores scene size', () => { expect(modelScene.width).to.be.equal(300); expect(modelScene.height).to.be.equal(150); }); }); // We're going to need to mock out XRFrame more so it can set the camera // in order to properly test this. suite('after initial placement', () => { let yaw: number; setup(async () => { arRenderer.onWebXRFrame(0, new MockXRFrame(arRenderer.currentSession!)); yaw = modelScene.yaw; }); test('places the model oriented to the camera', () => { const epsilon = 0.0001; const {target, position, camera} = modelScene; const cameraPosition = camera.position; const cameraToHit = new Vector2( position.x - cameraPosition.x, position.z - cameraPosition.z); const forward = target.getWorldDirection(new Vector3()); const forwardProjection = new Vector2(forward.x, forward.z); expect(forward.y).to.be.equal(0); expect(cameraToHit.cross(forwardProjection)).to.be.closeTo(0, epsilon); expect(cameraToHit.dot(forwardProjection)).to.be.lessThan(0); expect(modelScene.yaw).to.be.equal(yaw); }); }); }); });