@google/model-viewer
Advanced tools
Comparing version 2.0.0-rc3 to 2.0.0
@@ -32,3 +32,3 @@ /* @license | ||
const deserializeARModes = enumerationDeserializer(['quick-look', 'scene-viewer', 'webxr', 'none']); | ||
const DEFAULT_AR_MODES = 'webxr scene-viewer'; | ||
const DEFAULT_AR_MODES = 'webxr scene-viewer quick-look'; | ||
const ARMode = { | ||
@@ -35,0 +35,0 @@ QUICK_LOOK: 'quick-look', |
@@ -22,3 +22,3 @@ /* @license | ||
import { property } from 'lit/decorators.js'; | ||
import { $needsRender, $onModelLoad, $progressTracker, $renderer, $scene, $shouldAttemptPreload } from '../model-viewer-base.js'; | ||
import { $needsRender, $progressTracker, $renderer, $scene, $shouldAttemptPreload } from '../model-viewer-base.js'; | ||
import { clamp, deserializeUrl } from '../utilities.js'; | ||
@@ -33,5 +33,4 @@ export const BASE_OPACITY = 0.5; | ||
const $cancelEnvironmentUpdate = Symbol('cancelEnvironmentUpdate'); | ||
const $onPreload = Symbol('onPreload'); | ||
export const EnvironmentMixin = (ModelViewerElement) => { | ||
var _a, _b, _c, _d; | ||
var _a, _b, _c; | ||
class EnvironmentModelViewerElement extends ModelViewerElement { | ||
@@ -48,16 +47,3 @@ constructor() { | ||
this[_c] = null; | ||
this[_d] = (event) => { | ||
if (event.element === this) { | ||
this[$updateEnvironment](); | ||
} | ||
}; | ||
} | ||
connectedCallback() { | ||
super.connectedCallback(); | ||
this[$renderer].loader.addEventListener('preload', this[$onPreload]); | ||
} | ||
disconnectedCallback() { | ||
super.disconnectedCallback(); | ||
this[$renderer].loader.removeEventListener('preload', this[$onPreload]); | ||
} | ||
updated(changedProperties) { | ||
@@ -86,7 +72,3 @@ super.updated(changedProperties); | ||
} | ||
[(_a = $currentEnvironmentMap, _b = $currentBackground, _c = $cancelEnvironmentUpdate, _d = $onPreload, $onModelLoad)]() { | ||
super[$onModelLoad](); | ||
this[$scene].setEnvironmentAndSkybox(this[$currentEnvironmentMap], this[$currentBackground]); | ||
} | ||
async [$updateEnvironment]() { | ||
async [(_a = $currentEnvironmentMap, _b = $currentBackground, _c = $cancelEnvironmentUpdate, $updateEnvironment)]() { | ||
const { skyboxImage, environmentImage } = this; | ||
@@ -103,3 +85,3 @@ if (this[$cancelEnvironmentUpdate] != null) { | ||
try { | ||
const { environmentMap, skybox } = await textureUtils.generateEnvironmentMapAndSkybox(deserializeUrl(skyboxImage), environmentImage, (progress) => updateEnvProgress(clamp(progress, 0, 1) * 0.99)); | ||
const { environmentMap, skybox } = await textureUtils.generateEnvironmentMapAndSkybox(deserializeUrl(skyboxImage), environmentImage, (progress) => updateEnvProgress(clamp(progress, 0, 1))); | ||
if (this[$currentEnvironmentMap] !== environmentMap) { | ||
@@ -128,7 +110,3 @@ this[$currentEnvironmentMap] = environmentMap; | ||
finally { | ||
requestAnimationFrame(() => { | ||
requestAnimationFrame(() => { | ||
updateEnvProgress(1.0); | ||
}); | ||
}); | ||
updateEnvProgress(1.0); | ||
} | ||
@@ -135,0 +113,0 @@ } |
@@ -162,4 +162,3 @@ import { ReactiveElement } from 'lit'; | ||
* Parses the element for an appropriate source URL and | ||
* sets the views to use the new model based off of the `preload` | ||
* attribute. | ||
* sets the views to use the new model based. | ||
*/ | ||
@@ -166,0 +165,0 @@ [$updateSource](): Promise<void>; |
@@ -25,2 +25,3 @@ /* @license | ||
import { HAS_INTERSECTION_OBSERVER, HAS_RESIZE_OBSERVER } from './constants.js'; | ||
import { $updateEnvironment } from './features/environment.js'; | ||
import { makeTemplate } from './template.js'; | ||
@@ -30,3 +31,3 @@ import { $evictionPolicy, CachingGLTFLoader } from './three-components/CachingGLTFLoader.js'; | ||
import { Renderer } from './three-components/Renderer.js'; | ||
import { clamp, debounce, timePasses } from './utilities.js'; | ||
import { clamp, debounce } from './utilities.js'; | ||
import { dataUrlToBlob } from './utilities/data-conversion.js'; | ||
@@ -164,9 +165,2 @@ import { ProgressTracker } from './utilities/progress-tracker.js'; | ||
new ModelScene({ canvas: this[$canvas], element: this, width, height }); | ||
this[$scene].addEventListener('model-load', async (event) => { | ||
this[$markLoaded](); | ||
this[$onModelLoad](); | ||
// Give loading async tasks a chance to complete. | ||
await timePasses(); | ||
this.dispatchEvent(new CustomEvent('load', { detail: { url: event.url } })); | ||
}); | ||
// Update initial size on microtask timing so that subclasses have a | ||
@@ -458,11 +452,12 @@ // chance to initialize | ||
* Parses the element for an appropriate source URL and | ||
* sets the views to use the new model based off of the `preload` | ||
* attribute. | ||
* sets the views to use the new model based. | ||
*/ | ||
async [(_o = $onContextLost, $updateSource)]() { | ||
if (this.loaded || !this[$shouldAttemptPreload]()) { | ||
const scene = this[$scene]; | ||
if (this.loaded || !this[$shouldAttemptPreload]() || | ||
this.src === scene.url) { | ||
return; | ||
} | ||
if (this.generateSchema) { | ||
this[$scene].updateSchema(this.src); | ||
scene.updateSchema(this.src); | ||
} | ||
@@ -474,9 +469,20 @@ this[$updateStatus]('Loading'); | ||
// throw exceptions and/or behave in unexpected ways: | ||
this[$scene].stopAnimation(); | ||
scene.stopAnimation(); | ||
const updateSourceProgress = this[$progressTracker].beginActivity(); | ||
const source = this.src; | ||
try { | ||
await this[$scene].setSource(source, (progress) => updateSourceProgress(clamp(progress, 0, 1) * 0.95)); | ||
const detail = { url: source }; | ||
this.dispatchEvent(new CustomEvent('preload', { detail })); | ||
const srcUpdated = scene.setSource(source, (progress) => updateSourceProgress(clamp(progress, 0, 1) * 0.95)); | ||
const envUpdated = this[$updateEnvironment](); | ||
await Promise.all([srcUpdated, envUpdated]); | ||
this[$markLoaded](); | ||
this[$onModelLoad](); | ||
// Wait for shaders to compile and pixels to be drawn. | ||
await new Promise(resolve => { | ||
requestAnimationFrame(() => { | ||
requestAnimationFrame(() => { | ||
this.dispatchEvent(new CustomEvent('load', { detail: { url: source } })); | ||
resolve(); | ||
}); | ||
}); | ||
}); | ||
} | ||
@@ -487,7 +493,3 @@ catch (error) { | ||
finally { | ||
requestAnimationFrame(() => { | ||
requestAnimationFrame(() => { | ||
updateSourceProgress(1.0); | ||
}); | ||
}); | ||
updateSourceProgress(1.0); | ||
} | ||
@@ -494,0 +496,0 @@ } |
@@ -47,11 +47,6 @@ /* @license | ||
let loadDispatched = false; | ||
let preloadDispatched = false; | ||
const loadHandler = () => { | ||
loadDispatched = true; | ||
}; | ||
const preloadHandler = () => { | ||
preloadDispatched = true; | ||
}; | ||
element.addEventListener('load', loadHandler); | ||
element.addEventListener('preload', preloadHandler); | ||
element.style.display = 'none'; | ||
@@ -64,5 +59,3 @@ // Give IntersectionObserver a chance to notify. In Chrome, this takes | ||
element.removeEventListener('load', loadHandler); | ||
element.removeEventListener('preload', preloadHandler); | ||
expect(loadDispatched).to.be.false; | ||
expect(preloadDispatched).to.be.false; | ||
}); | ||
@@ -135,18 +128,12 @@ suite('load', () => { | ||
suite('src changes quickly', () => { | ||
test('eventually notifies that current src is preloaded', async () => { | ||
test('eventually notifies that current src is loaded', async () => { | ||
element.loading = 'eager'; | ||
element.src = CUBE_GLB_PATH; | ||
const loadCubeEvent = waitForEvent(element, 'load'); | ||
await timePasses(); | ||
let preloadEvent = null; | ||
const onPreload = (event) => { | ||
if (event.detail.url === HORSE_GLB_PATH) { | ||
preloadEvent = event; | ||
} | ||
}; | ||
element.addEventListener('preload', onPreload); | ||
element.src = HORSE_GLB_PATH; | ||
await until(() => element.loaded); | ||
await timePasses(); | ||
element.removeEventListener('preload', onPreload); | ||
expect(preloadEvent).to.be.ok; | ||
const loadCube = await loadCubeEvent; | ||
const loadHorse = await waitForEvent(element, 'load'); | ||
expect(loadCube.detail.url).to.be.eq(CUBE_GLB_PATH); | ||
expect(loadHorse.detail.url).to.be.eq(HORSE_GLB_PATH); | ||
}); | ||
@@ -153,0 +140,0 @@ }); |
@@ -37,10 +37,2 @@ /* @license | ||
}); | ||
suite('setModelSource', () => { | ||
test('fires a model-load event when loaded', async function () { | ||
let fired = false; | ||
scene.addEventListener('model-load', () => fired = true); | ||
await scene.setSource(assetPath('models/Astronaut.glb')); | ||
expect(fired).to.be.ok; | ||
}); | ||
}); | ||
suite('with a model', () => { | ||
@@ -47,0 +39,0 @@ setup(async () => { |
@@ -222,3 +222,3 @@ /* @license | ||
this.oldTarget.copy(scene.getTarget()); | ||
scene.addEventListener('model-load', this.onUpdateScene); | ||
scene.element.addEventListener('load', this.onUpdateScene); | ||
const radians = HIT_ANGLE_DEG * Math.PI / 180; | ||
@@ -316,3 +316,3 @@ const ray = this.placeOnWall === true ? | ||
scene.xrCamera = null; | ||
scene.removeEventListener('model-load', this.onUpdateScene); | ||
scene.element.removeEventListener('load', this.onUpdateScene); | ||
scene.orientHotspots(0); | ||
@@ -319,0 +319,0 @@ element.requestUpdate('cameraTarget'); |
@@ -150,3 +150,2 @@ /* @license | ||
this.idealAspect = framingInfo.fieldOfViewAspect; | ||
this.dispatchEvent({ type: 'model-load', url: this.url }); | ||
return; | ||
@@ -205,3 +204,2 @@ } | ||
this.setShadowIntensity(this.shadowIntensity); | ||
this.dispatchEvent({ type: 'model-load', url: this.url }); | ||
} | ||
@@ -208,0 +206,0 @@ reset() { |
@@ -54,9 +54,2 @@ /// <reference types="webxr" /> | ||
constructor(options: RendererOptions); | ||
/** | ||
* Updates the renderer's size based on the largest scene and any changes to | ||
* device pixel ratio. | ||
*/ | ||
private updateRendererSize; | ||
private updateRendererScale; | ||
dispatchRenderScale(scene: ModelScene): void; | ||
registerScene(scene: ModelScene): void; | ||
@@ -72,2 +65,9 @@ unregisterScene(scene: ModelScene): void; | ||
private countVisibleScenes; | ||
/** | ||
* Updates the renderer's size based on the largest scene and any changes to | ||
* device pixel ratio. | ||
*/ | ||
private updateRendererSize; | ||
private updateRendererScale; | ||
private shouldRender; | ||
private rescaleCanvas; | ||
@@ -74,0 +74,0 @@ private sceneSize; |
@@ -26,5 +26,5 @@ /* @license | ||
const DURATION_DECAY = 0.2; | ||
const LOW_FRAME_DURATION_MS = 18; | ||
const HIGH_FRAME_DURATION_MS = 26; | ||
const MAX_AVG_CHANGE_MS = 2; | ||
const LOW_FRAME_DURATION_MS = 40; | ||
const HIGH_FRAME_DURATION_MS = 60; | ||
const MAX_AVG_CHANGE_MS = 5; | ||
const SCALE_STEPS = [1, 0.79, 0.62, 0.5, 0.4, 0.31, 0.25]; | ||
@@ -137,3 +137,59 @@ const DEFAULT_LAST_STEP = 3; | ||
} | ||
registerScene(scene) { | ||
this.scenes.add(scene); | ||
scene.forceRescale(); | ||
if (this.canRender && this.scenes.size > 0) { | ||
this.threeRenderer.setAnimationLoop((time, frame) => this.render(time, frame)); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.addScene(scene); | ||
} | ||
} | ||
unregisterScene(scene) { | ||
this.scenes.delete(scene); | ||
if (this.canvas3D.parentElement === scene.canvas.parentElement) { | ||
scene.canvas.parentElement.removeChild(this.canvas3D); | ||
} | ||
if (this.canRender && this.scenes.size === 0) { | ||
this.threeRenderer.setAnimationLoop(null); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.removeScene(scene); | ||
} | ||
} | ||
displayCanvas(scene) { | ||
return this.multipleScenesVisible ? scene.element[$canvas] : this.canvas3D; | ||
} | ||
/** | ||
* The function enables an optimization, where when there is only a single | ||
* <model-viewer> element, we can use the renderer's 3D canvas directly for | ||
* display. Otherwise we need to use the element's 2D canvas and copy the | ||
* renderer's result into it. | ||
*/ | ||
countVisibleScenes() { | ||
const { canvas3D } = this; | ||
let visibleScenes = 0; | ||
let canvas3DScene = null; | ||
for (const scene of this.scenes) { | ||
const { element } = scene; | ||
if (element.modelIsVisible && scene.externalRenderer == null) { | ||
++visibleScenes; | ||
} | ||
if (canvas3D.parentElement === scene.canvas.parentElement) { | ||
canvas3DScene = scene; | ||
} | ||
} | ||
const multipleScenesVisible = visibleScenes > 1; | ||
if (canvas3DScene != null) { | ||
const newlyMultiple = multipleScenesVisible && !this.multipleScenesVisible; | ||
const disappearing = !canvas3DScene.element.modelIsVisible; | ||
if (newlyMultiple || disappearing) { | ||
const { width, height } = this.sceneSize(canvas3DScene); | ||
this.copyPixels(canvas3DScene, width, height); | ||
canvas3D.parentElement.removeChild(canvas3D); | ||
} | ||
} | ||
this.multipleScenesVisible = multipleScenesVisible; | ||
} | ||
/** | ||
* Updates the renderer's size based on the largest scene and any changes to | ||
@@ -196,4 +252,30 @@ * device pixel ratio. | ||
} | ||
dispatchRenderScale(scene) { | ||
shouldRender(scene) { | ||
if (!scene.shouldRender()) { | ||
// The first frame we stop rendering the scene (because it stops moving), | ||
// trigger one extra render at full scale. | ||
if (scene.scaleStep != 0) { | ||
scene.scaleStep = 0; | ||
this.rescaleCanvas(scene); | ||
} | ||
else { | ||
return false; | ||
} | ||
} | ||
else if (scene.scaleStep != this.scaleStep) { | ||
// Update render scale | ||
scene.scaleStep = this.scaleStep; | ||
this.rescaleCanvas(scene); | ||
} | ||
return true; | ||
} | ||
rescaleCanvas(scene) { | ||
const scale = SCALE_STEPS[scene.scaleStep]; | ||
const width = Math.ceil(this.width / scale); | ||
const height = Math.ceil(this.height / scale); | ||
const { style } = scene.canvas; | ||
style.width = `${width}px`; | ||
style.height = `${height}px`; | ||
this.canvas3D.style.width = `${width}px`; | ||
this.canvas3D.style.height = `${height}px`; | ||
const renderedDpr = this.dpr * scale; | ||
@@ -214,93 +296,2 @@ const reason = scale < 1 ? 'GPU throttling' : | ||
} | ||
registerScene(scene) { | ||
this.scenes.add(scene); | ||
scene.forceRescale(); | ||
if (this.canRender && this.scenes.size > 0) { | ||
this.threeRenderer.setAnimationLoop((time, frame) => this.render(time, frame)); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.addScene(scene); | ||
} | ||
} | ||
unregisterScene(scene) { | ||
this.scenes.delete(scene); | ||
if (this.canvas3D.parentElement === scene.canvas.parentElement) { | ||
scene.canvas.parentElement.removeChild(this.canvas3D); | ||
} | ||
if (this.canRender && this.scenes.size === 0) { | ||
this.threeRenderer.setAnimationLoop(null); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.removeScene(scene); | ||
} | ||
} | ||
displayCanvas(scene) { | ||
return this.multipleScenesVisible ? scene.element[$canvas] : this.canvas3D; | ||
} | ||
/** | ||
* The function enables an optimization, where when there is only a single | ||
* <model-viewer> element, we can use the renderer's 3D canvas directly for | ||
* display. Otherwise we need to use the element's 2D canvas and copy the | ||
* renderer's result into it. | ||
*/ | ||
countVisibleScenes() { | ||
const { canvas3D } = this; | ||
let visibleScenes = 0; | ||
let canvas3DScene = null; | ||
for (const scene of this.scenes) { | ||
const { element } = scene; | ||
if (element.modelIsVisible && scene.externalRenderer == null) { | ||
++visibleScenes; | ||
} | ||
if (canvas3D.parentElement === scene.canvas.parentElement) { | ||
canvas3DScene = scene; | ||
} | ||
} | ||
const multipleScenesVisible = visibleScenes > 1; | ||
if (canvas3DScene != null) { | ||
const newlyMultiple = multipleScenesVisible && !this.multipleScenesVisible; | ||
const disappearing = !canvas3DScene.element.modelIsVisible; | ||
if (newlyMultiple || disappearing) { | ||
const { width, height } = this.sceneSize(canvas3DScene); | ||
this.copyPixels(canvas3DScene, width, height); | ||
canvas3D.parentElement.removeChild(canvas3D); | ||
} | ||
} | ||
this.multipleScenesVisible = multipleScenesVisible; | ||
} | ||
rescaleCanvas(scene) { | ||
const { style } = scene.canvas; | ||
if (!scene.shouldRender()) { | ||
// The first frame we stop rendering the scene (because it stops moving), | ||
// trigger one extra render at full scale. | ||
if (scene.scaleStep != 0) { | ||
scene.scaleStep = 0; | ||
style.width = `${this.width}px`; | ||
style.height = `${this.height}px`; | ||
this.dispatchRenderScale(scene); | ||
if (!this.multipleScenesVisible) { | ||
this.canvas3D.style.width = `${this.width}px`; | ||
this.canvas3D.style.height = `${this.height}px`; | ||
} | ||
} | ||
else { | ||
return true; // Skip rendering | ||
} | ||
} | ||
else if (scene.scaleStep != this.scaleStep) { | ||
// Update render scale | ||
scene.scaleStep = this.scaleStep; | ||
const scale = this.scaleFactor; | ||
const width = Math.ceil(this.width / scale); | ||
const height = Math.ceil(this.height / scale); | ||
style.width = `${width}px`; | ||
style.height = `${height}px`; | ||
if (!this.multipleScenesVisible) { | ||
this.canvas3D.style.width = `${width}px`; | ||
this.canvas3D.style.height = `${height}px`; | ||
} | ||
this.dispatchRenderScale(scene); | ||
} | ||
return false; // Perform rendering | ||
} | ||
sceneSize(scene) { | ||
@@ -377,3 +368,3 @@ const { dpr } = this; | ||
this.preRender(scene, t, delta); | ||
if (this.rescaleCanvas(scene)) { | ||
if (!this.shouldRender(scene)) { | ||
continue; | ||
@@ -380,0 +371,0 @@ } |
{ | ||
"name": "@google/model-viewer", | ||
"version": "2.0.0-rc3", | ||
"version": "2.0.0", | ||
"description": "Easily display interactive 3D models on the web and in AR!", | ||
@@ -111,2 +111,2 @@ "repository": "https://github.com/google/model-viewer", | ||
} | ||
} | ||
} |
@@ -35,3 +35,3 @@ /* @license | ||
const DEFAULT_AR_MODES = 'webxr scene-viewer'; | ||
const DEFAULT_AR_MODES = 'webxr scene-viewer quick-look'; | ||
@@ -38,0 +38,0 @@ const ARMode: {[index: string]: ARMode} = { |
@@ -17,6 +17,5 @@ /* @license | ||
import {property} from 'lit/decorators.js'; | ||
import {Event as ThreeEvent, Texture} from 'three'; | ||
import {Texture} from 'three'; | ||
import ModelViewerElementBase, {$needsRender, $onModelLoad, $progressTracker, $renderer, $scene, $shouldAttemptPreload} from '../model-viewer-base.js'; | ||
import {PreloadEvent} from '../three-components/CachingGLTFLoader.js'; | ||
import ModelViewerElementBase, {$needsRender, $progressTracker, $renderer, $scene, $shouldAttemptPreload} from '../model-viewer-base.js'; | ||
import {clamp, Constructor, deserializeUrl} from '../utilities.js'; | ||
@@ -33,3 +32,2 @@ | ||
const $cancelEnvironmentUpdate = Symbol('cancelEnvironmentUpdate'); | ||
const $onPreload = Symbol('onPreload'); | ||
@@ -70,18 +68,2 @@ export declare interface EnvironmentInterface { | ||
private[$onPreload] = (event: ThreeEvent) => { | ||
if ((event as PreloadEvent).element === this) { | ||
this[$updateEnvironment](); | ||
} | ||
}; | ||
connectedCallback() { | ||
super.connectedCallback(); | ||
this[$renderer].loader.addEventListener('preload', this[$onPreload]); | ||
} | ||
disconnectedCallback() { | ||
super.disconnectedCallback(); | ||
this[$renderer].loader.removeEventListener('preload', this[$onPreload]); | ||
} | ||
updated(changedProperties: Map<string|number|symbol, unknown>) { | ||
@@ -116,9 +98,2 @@ super.updated(changedProperties); | ||
[$onModelLoad]() { | ||
super[$onModelLoad](); | ||
this[$scene].setEnvironmentAndSkybox( | ||
this[$currentEnvironmentMap], this[$currentBackground]); | ||
} | ||
async[$updateEnvironment]() { | ||
@@ -145,4 +120,3 @@ const {skyboxImage, environmentImage} = this; | ||
environmentImage, | ||
(progress: number) => | ||
updateEnvProgress(clamp(progress, 0, 1) * 0.99)); | ||
(progress: number) => updateEnvProgress(clamp(progress, 0, 1))); | ||
@@ -171,7 +145,3 @@ if (this[$currentEnvironmentMap] !== environmentMap) { | ||
} finally { | ||
requestAnimationFrame(() => { | ||
requestAnimationFrame(() => { | ||
updateEnvProgress(1.0); | ||
}); | ||
}); | ||
updateEnvProgress(1.0); | ||
} | ||
@@ -178,0 +148,0 @@ } |
@@ -21,2 +21,3 @@ /* @license | ||
import {HAS_INTERSECTION_OBSERVER, HAS_RESIZE_OBSERVER} from './constants.js'; | ||
import {$updateEnvironment} from './features/environment.js'; | ||
import {makeTemplate} from './template.js'; | ||
@@ -26,3 +27,3 @@ import {$evictionPolicy, CachingGLTFLoader} from './three-components/CachingGLTFLoader.js'; | ||
import {ContextLostEvent, Renderer} from './three-components/Renderer.js'; | ||
import {clamp, debounce, timePasses} from './utilities.js'; | ||
import {clamp, debounce} from './utilities.js'; | ||
import {dataUrlToBlob} from './utilities/data-conversion.js'; | ||
@@ -262,13 +263,2 @@ import {ProgressTracker} from './utilities/progress-tracker.js'; | ||
this[$scene].addEventListener('model-load', async (event) => { | ||
this[$markLoaded](); | ||
this[$onModelLoad](); | ||
// Give loading async tasks a chance to complete. | ||
await timePasses(); | ||
this.dispatchEvent( | ||
new CustomEvent('load', {detail: {url: (event as any).url}})); | ||
}); | ||
// Update initial size on microtask timing so that subclasses have a | ||
@@ -585,7 +575,8 @@ // chance to initialize | ||
* Parses the element for an appropriate source URL and | ||
* sets the views to use the new model based off of the `preload` | ||
* attribute. | ||
* sets the views to use the new model based. | ||
*/ | ||
async[$updateSource]() { | ||
if (this.loaded || !this[$shouldAttemptPreload]()) { | ||
const scene = this[$scene]; | ||
if (this.loaded || !this[$shouldAttemptPreload]() || | ||
this.src === scene.url) { | ||
return; | ||
@@ -595,3 +586,3 @@ } | ||
if (this.generateSchema) { | ||
this[$scene].updateSchema(this.src); | ||
scene.updateSchema(this.src); | ||
} | ||
@@ -603,3 +594,3 @@ this[$updateStatus]('Loading'); | ||
// throw exceptions and/or behave in unexpected ways: | ||
this[$scene].stopAnimation(); | ||
scene.stopAnimation(); | ||
@@ -609,3 +600,3 @@ const updateSourceProgress = this[$progressTracker].beginActivity(); | ||
try { | ||
await this[$scene].setSource( | ||
const srcUpdated = scene.setSource( | ||
source, | ||
@@ -615,4 +606,19 @@ (progress: number) => | ||
const detail = {url: source}; | ||
this.dispatchEvent(new CustomEvent('preload', {detail})); | ||
const envUpdated = (this as any)[$updateEnvironment](); | ||
await Promise.all([srcUpdated, envUpdated]); | ||
this[$markLoaded](); | ||
this[$onModelLoad](); | ||
// Wait for shaders to compile and pixels to be drawn. | ||
await new Promise<void>(resolve => { | ||
requestAnimationFrame(() => { | ||
requestAnimationFrame(() => { | ||
this.dispatchEvent( | ||
new CustomEvent('load', {detail: {url: source}})); | ||
resolve(); | ||
}); | ||
}); | ||
}); | ||
} catch (error) { | ||
@@ -622,9 +628,5 @@ this.dispatchEvent(new CustomEvent( | ||
} finally { | ||
requestAnimationFrame(() => { | ||
requestAnimationFrame(() => { | ||
updateSourceProgress(1.0); | ||
}); | ||
}); | ||
updateSourceProgress(1.0); | ||
} | ||
} | ||
} |
@@ -56,12 +56,7 @@ /* @license | ||
let loadDispatched = false; | ||
let preloadDispatched = false; | ||
const loadHandler = () => { | ||
loadDispatched = true; | ||
}; | ||
const preloadHandler = () => { | ||
preloadDispatched = true; | ||
}; | ||
element.addEventListener('load', loadHandler); | ||
element.addEventListener('preload', preloadHandler); | ||
@@ -79,6 +74,4 @@ element.style.display = 'none'; | ||
element.removeEventListener('load', loadHandler); | ||
element.removeEventListener('preload', preloadHandler); | ||
expect(loadDispatched).to.be.false; | ||
expect(preloadDispatched).to.be.false; | ||
}); | ||
@@ -167,25 +160,18 @@ | ||
suite('src changes quickly', () => { | ||
test('eventually notifies that current src is preloaded', async () => { | ||
test('eventually notifies that current src is loaded', async () => { | ||
element.loading = 'eager'; | ||
element.src = CUBE_GLB_PATH; | ||
const loadCubeEvent = | ||
waitForEvent(element, 'load') as Promise<CustomEvent>; | ||
await timePasses(); | ||
let preloadEvent = null; | ||
const onPreload = (event: CustomEvent) => { | ||
if (event.detail.url === HORSE_GLB_PATH) { | ||
preloadEvent = event; | ||
} | ||
}; | ||
element.addEventListener<any>('preload', onPreload); | ||
element.src = HORSE_GLB_PATH; | ||
await until(() => element.loaded); | ||
const loadCube = await loadCubeEvent; | ||
const loadHorse = await waitForEvent(element, 'load') as CustomEvent; | ||
await timePasses(); | ||
element.removeEventListener<any>('preload', onPreload); | ||
expect(preloadEvent).to.be.ok; | ||
expect(loadCube.detail.url).to.be.eq(CUBE_GLB_PATH); | ||
expect(loadHorse.detail.url).to.be.eq(HORSE_GLB_PATH); | ||
}); | ||
@@ -192,0 +178,0 @@ }); |
@@ -47,11 +47,2 @@ /* @license | ||
suite('setModelSource', () => { | ||
test('fires a model-load event when loaded', async function() { | ||
let fired = false; | ||
scene.addEventListener('model-load', () => fired = true); | ||
await scene.setSource(assetPath('models/Astronaut.glb')); | ||
expect(fired).to.be.ok; | ||
}); | ||
}); | ||
suite('with a model', () => { | ||
@@ -58,0 +49,0 @@ setup(async () => { |
@@ -234,3 +234,3 @@ /* @license | ||
scene.addEventListener('model-load', this.onUpdateScene); | ||
scene.element.addEventListener('load', this.onUpdateScene); | ||
@@ -354,3 +354,3 @@ const radians = HIT_ANGLE_DEG * Math.PI / 180; | ||
scene.removeEventListener('model-load', this.onUpdateScene); | ||
scene.element.removeEventListener('load', this.onUpdateScene); | ||
scene.orientHotspots(0); | ||
@@ -357,0 +357,0 @@ element.requestUpdate('cameraTarget'); |
@@ -207,4 +207,2 @@ /* @license | ||
this.idealAspect = framingInfo.fieldOfViewAspect; | ||
this.dispatchEvent({type: 'model-load', url: this.url}); | ||
return; | ||
@@ -276,3 +274,2 @@ } | ||
this.setShadowIntensity(this.shadowIntensity); | ||
this.dispatchEvent({type: 'model-load', url: this.url}); | ||
} | ||
@@ -279,0 +276,0 @@ |
@@ -42,5 +42,5 @@ /* @license | ||
const DURATION_DECAY = 0.2; | ||
const LOW_FRAME_DURATION_MS = 18; | ||
const HIGH_FRAME_DURATION_MS = 26; | ||
const MAX_AVG_CHANGE_MS = 2; | ||
const LOW_FRAME_DURATION_MS = 40; | ||
const HIGH_FRAME_DURATION_MS = 60; | ||
const MAX_AVG_CHANGE_MS = 5; | ||
const SCALE_STEPS = [1, 0.79, 0.62, 0.5, 0.4, 0.31, 0.25]; | ||
@@ -176,3 +176,72 @@ const DEFAULT_LAST_STEP = 3; | ||
registerScene(scene: ModelScene) { | ||
this.scenes.add(scene); | ||
scene.forceRescale(); | ||
if (this.canRender && this.scenes.size > 0) { | ||
this.threeRenderer.setAnimationLoop( | ||
(time: number, frame?: any) => this.render(time, frame)); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.addScene(scene); | ||
} | ||
} | ||
unregisterScene(scene: ModelScene) { | ||
this.scenes.delete(scene); | ||
if (this.canvas3D.parentElement === scene.canvas.parentElement) { | ||
scene.canvas.parentElement!.removeChild(this.canvas3D); | ||
} | ||
if (this.canRender && this.scenes.size === 0) { | ||
this.threeRenderer.setAnimationLoop(null); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.removeScene(scene); | ||
} | ||
} | ||
displayCanvas(scene: ModelScene): HTMLCanvasElement { | ||
return this.multipleScenesVisible ? scene.element[$canvas] : this.canvas3D; | ||
} | ||
/** | ||
* The function enables an optimization, where when there is only a single | ||
* <model-viewer> element, we can use the renderer's 3D canvas directly for | ||
* display. Otherwise we need to use the element's 2D canvas and copy the | ||
* renderer's result into it. | ||
*/ | ||
private countVisibleScenes() { | ||
const {canvas3D} = this; | ||
let visibleScenes = 0; | ||
let canvas3DScene = null; | ||
for (const scene of this.scenes) { | ||
const {element} = scene; | ||
if (element.modelIsVisible && scene.externalRenderer == null) { | ||
++visibleScenes; | ||
} | ||
if (canvas3D.parentElement === scene.canvas.parentElement) { | ||
canvas3DScene = scene; | ||
} | ||
} | ||
const multipleScenesVisible = visibleScenes > 1; | ||
if (canvas3DScene != null) { | ||
const newlyMultiple = | ||
multipleScenesVisible && !this.multipleScenesVisible; | ||
const disappearing = !canvas3DScene.element.modelIsVisible; | ||
if (newlyMultiple || disappearing) { | ||
const {width, height} = this.sceneSize(canvas3DScene); | ||
this.copyPixels(canvas3DScene, width, height); | ||
canvas3D.parentElement!.removeChild(canvas3D); | ||
} | ||
} | ||
this.multipleScenesVisible = multipleScenesVisible; | ||
} | ||
/** | ||
* Updates the renderer's size based on the largest scene and any changes to | ||
@@ -247,4 +316,31 @@ * device pixel ratio. | ||
dispatchRenderScale(scene: ModelScene) { | ||
private shouldRender(scene: ModelScene): boolean { | ||
if (!scene.shouldRender()) { | ||
// The first frame we stop rendering the scene (because it stops moving), | ||
// trigger one extra render at full scale. | ||
if (scene.scaleStep != 0) { | ||
scene.scaleStep = 0; | ||
this.rescaleCanvas(scene); | ||
} else { | ||
return false; | ||
} | ||
} else if (scene.scaleStep != this.scaleStep) { | ||
// Update render scale | ||
scene.scaleStep = this.scaleStep; | ||
this.rescaleCanvas(scene); | ||
} | ||
return true; | ||
} | ||
private rescaleCanvas(scene: ModelScene) { | ||
const scale = SCALE_STEPS[scene.scaleStep]; | ||
const width = Math.ceil(this.width / scale); | ||
const height = Math.ceil(this.height / scale); | ||
const {style} = scene.canvas; | ||
style.width = `${width}px`; | ||
style.height = `${height}px`; | ||
this.canvas3D.style.width = `${width}px`; | ||
this.canvas3D.style.height = `${height}px`; | ||
const renderedDpr = this.dpr * scale; | ||
@@ -266,107 +362,2 @@ const reason = scale < 1 ? 'GPU throttling' : | ||
registerScene(scene: ModelScene) { | ||
this.scenes.add(scene); | ||
scene.forceRescale(); | ||
if (this.canRender && this.scenes.size > 0) { | ||
this.threeRenderer.setAnimationLoop( | ||
(time: number, frame?: any) => this.render(time, frame)); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.addScene(scene); | ||
} | ||
} | ||
unregisterScene(scene: ModelScene) { | ||
this.scenes.delete(scene); | ||
if (this.canvas3D.parentElement === scene.canvas.parentElement) { | ||
scene.canvas.parentElement!.removeChild(this.canvas3D); | ||
} | ||
if (this.canRender && this.scenes.size === 0) { | ||
this.threeRenderer.setAnimationLoop(null); | ||
} | ||
if (this.debugger != null) { | ||
this.debugger.removeScene(scene); | ||
} | ||
} | ||
displayCanvas(scene: ModelScene): HTMLCanvasElement { | ||
return this.multipleScenesVisible ? scene.element[$canvas] : this.canvas3D; | ||
} | ||
/** | ||
* The function enables an optimization, where when there is only a single | ||
* <model-viewer> element, we can use the renderer's 3D canvas directly for | ||
* display. Otherwise we need to use the element's 2D canvas and copy the | ||
* renderer's result into it. | ||
*/ | ||
private countVisibleScenes() { | ||
const {canvas3D} = this; | ||
let visibleScenes = 0; | ||
let canvas3DScene = null; | ||
for (const scene of this.scenes) { | ||
const {element} = scene; | ||
if (element.modelIsVisible && scene.externalRenderer == null) { | ||
++visibleScenes; | ||
} | ||
if (canvas3D.parentElement === scene.canvas.parentElement) { | ||
canvas3DScene = scene; | ||
} | ||
} | ||
const multipleScenesVisible = visibleScenes > 1; | ||
if (canvas3DScene != null) { | ||
const newlyMultiple = | ||
multipleScenesVisible && !this.multipleScenesVisible; | ||
const disappearing = !canvas3DScene.element.modelIsVisible; | ||
if (newlyMultiple || disappearing) { | ||
const {width, height} = this.sceneSize(canvas3DScene); | ||
this.copyPixels(canvas3DScene, width, height); | ||
canvas3D.parentElement!.removeChild(canvas3D); | ||
} | ||
} | ||
this.multipleScenesVisible = multipleScenesVisible; | ||
} | ||
private rescaleCanvas(scene: ModelScene): boolean { | ||
const {style} = scene.canvas; | ||
if (!scene.shouldRender()) { | ||
// The first frame we stop rendering the scene (because it stops moving), | ||
// trigger one extra render at full scale. | ||
if (scene.scaleStep != 0) { | ||
scene.scaleStep = 0; | ||
style.width = `${this.width}px`; | ||
style.height = `${this.height}px`; | ||
this.dispatchRenderScale(scene); | ||
if (!this.multipleScenesVisible) { | ||
this.canvas3D.style.width = `${this.width}px`; | ||
this.canvas3D.style.height = `${this.height}px`; | ||
} | ||
} else { | ||
return true; // Skip rendering | ||
} | ||
} else if (scene.scaleStep != this.scaleStep) { | ||
// Update render scale | ||
scene.scaleStep = this.scaleStep; | ||
const scale = this.scaleFactor; | ||
const width = Math.ceil(this.width / scale); | ||
const height = Math.ceil(this.height / scale); | ||
style.width = `${width}px`; | ||
style.height = `${height}px`; | ||
if (!this.multipleScenesVisible) { | ||
this.canvas3D.style.width = `${width}px`; | ||
this.canvas3D.style.height = `${height}px`; | ||
} | ||
this.dispatchRenderScale(scene); | ||
} | ||
return false; // Perform rendering | ||
} | ||
private sceneSize(scene: ModelScene) { | ||
@@ -461,3 +452,3 @@ const {dpr} = this; | ||
if (this.rescaleCanvas(scene)) { | ||
if (!this.shouldRender(scene)) { | ||
continue; | ||
@@ -464,0 +455,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
0
21033582
125353