diff --git a/packages/model-viewer/src/features/animation.ts b/packages/model-viewer/src/features/animation.ts index 35fdf22d21..9f0a0e8fbe 100644 --- a/packages/model-viewer/src/features/animation.ts +++ b/packages/model-viewer/src/features/animation.ts @@ -75,14 +75,14 @@ export const AnimationMixin = >( */ get availableAnimations(): Array { if (this.loaded) { - return this[$scene].animationNames; + return this[$scene].modelData.animationNames; } return []; } get duration(): number { - return this[$scene].duration; + return this[$scene].modelData.duration; } get paused(): boolean { diff --git a/packages/model-viewer/src/model-viewer-base.ts b/packages/model-viewer/src/model-viewer-base.ts index 3077912a1d..002abb0de6 100644 --- a/packages/model-viewer/src/model-viewer-base.ts +++ b/packages/model-viewer/src/model-viewer-base.ts @@ -393,7 +393,7 @@ export default class ModelViewerElementBase extends ReactiveElement { this[$loaded] = false; this[$loadedTime] = 0; this[$scene].reset(); - } else if (this.src !== this[$scene].url) { + } else if (this.src !== this[$scene].modelData.url) { this[$loaded] = false; this[$loadedTime] = 0; this[$updateSource](); @@ -594,7 +594,7 @@ export default class ModelViewerElementBase extends ReactiveElement { async[$updateSource]() { const scene = this[$scene]; if (this.loaded || !this[$shouldAttemptPreload]() || - this.src === scene.url) { + this.src === scene.modelData.url) { return; } diff --git a/packages/model-viewer/src/test/features/animation-spec.ts b/packages/model-viewer/src/test/features/animation-spec.ts index 0c840e2bb0..8897705991 100644 --- a/packages/model-viewer/src/test/features/animation-spec.ts +++ b/packages/model-viewer/src/test/features/animation-spec.ts @@ -42,8 +42,8 @@ const animationIsPlaying = (element: any, animationName?: string): boolean => { const animationWithIndexIsPlaying = (element: any, animationIndex = 0): boolean => { - const {currentAnimationAction} = element[$scene]; - const {_currentGLTF} = element[$scene]; + const {currentAnimationAction, modelData} = element[$scene]; + const {_currentGLTF} = modelData.currentGLTF if (currentAnimationAction != null && animationIndex >= 0 && animationIndex < _currentGLTF.animations.length && diff --git a/packages/model-viewer/src/test/model-viewer-base-spec.ts b/packages/model-viewer/src/test/model-viewer-base-spec.ts index 2db6684192..e9045bf886 100644 --- a/packages/model-viewer/src/test/model-viewer-base-spec.ts +++ b/packages/model-viewer/src/test/model-viewer-base-spec.ts @@ -92,7 +92,7 @@ suite('ModelViewerElementBase', () => { element.src = assetPath('models/Horse.glb'); await waitForEvent(element, 'load'); - expect(element[$scene].url) + expect(element[$scene].modelData.url) .to.be.equal(assetPath('models/Horse.glb')); }); @@ -102,7 +102,7 @@ suite('ModelViewerElementBase', () => { element.src = assetPath('models/Horse.glb'); await waitForEvent(element, 'load'); - expect(element[$scene].url) + expect(element[$scene].modelData.url) .to.be.equal(assetPath('models/Horse.glb')); }); }); diff --git a/packages/model-viewer/src/three-components/ModelData.ts b/packages/model-viewer/src/three-components/ModelData.ts new file mode 100644 index 0000000000..6c9b8ba78e --- /dev/null +++ b/packages/model-viewer/src/three-components/ModelData.ts @@ -0,0 +1,108 @@ +import {AnimationAction, AnimationClip, LoopPingPong, Object3D} from 'three'; + +import {$renderer} from '../model-viewer-base.js'; +import {ModelViewerElement} from '../model-viewer.js'; + +import {ModelViewerGLTFInstance} from './gltf-instance/ModelViewerGLTFInstance.js'; + +/** + * A THREE.Scene object that takes a Model and CanvasHTMLElement and + * constructs a framed scene based off of the canvas dimensions. + * Provides lights and cameras to be used in a renderer. + */ +export class ModelData extends Object3D { + // ModelScene is going to have child of this types + public url: string|null = null; + + public currentGLTF: ModelViewerGLTFInstance|null = null; + + private cancelPendingSourceChange: (() => void)|null = null; + + + // Animations + public animationNames: Array = []; + public animationsByName: Map = new Map(); + public currentAnimationAction: AnimationAction|null = null; + + + constructor() { + super(); + } + + async loadModel( + url: string, element: ModelViewerElement, + progressCallback: (progress: number) => void): + Promise { + // If we have pending work due to a previous source change in progress, + // cancel it so that we do not incur a race condition: + if (this.cancelPendingSourceChange != null) { + this.cancelPendingSourceChange!(); + this.cancelPendingSourceChange = null; + } + + let gltf: ModelViewerGLTFInstance; + + try { + gltf = await new Promise( + async (resolve, reject) => { + this.cancelPendingSourceChange = () => reject(); + try { + const result = await element[$renderer].loader.load( + url, element, progressCallback); + resolve(result); + } catch (error) { + reject(error); + } + }); + } finally { + this.cancelPendingSourceChange = null; + } + + return gltf; + } + + setUpAnimations() { + const {animations} = this.currentGLTF!; + console.log('Samaneh:animation', animations); + const animationsByName = new Map(); + const animationNames = []; + + for (const animation of animations) { + animationsByName.set(animation.name, animation); + animationNames.push(animation.name); + } + + this.animations = animations; + this.animationsByName = animationsByName; + this.animationNames = animationNames; + } + + + get animationTime(): number { + if (this.currentAnimationAction != null) { + const loopCount = + Math.max((this.currentAnimationAction as any)._loopCount, 0); + if (this.currentAnimationAction.loop === LoopPingPong && + (loopCount & 1) === 1) { + return this.duration - this.currentAnimationAction.time + } else { + return this.currentAnimationAction.time; + } + } + + return 0; + } + + get duration(): number { + if (this.currentAnimationAction != null && + this.currentAnimationAction.getClip()) { + return this.currentAnimationAction.getClip().duration; + } + + return 0; + } + + get hasActiveAnimation(): boolean { + return this.currentAnimationAction != null; + } +} \ No newline at end of file diff --git a/packages/model-viewer/src/three-components/ModelScene.ts b/packages/model-viewer/src/three-components/ModelScene.ts index 1a16135409..9bc8bd8350 100644 --- a/packages/model-viewer/src/three-components/ModelScene.ts +++ b/packages/model-viewer/src/three-components/ModelScene.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import {ACESFilmicToneMapping, AnimationAction, AnimationActionLoopStyles, AnimationClip, AnimationMixer, AnimationMixerEventMap, Box3, Camera, Euler, Event as ThreeEvent, LoopPingPong, LoopRepeat, Material, Matrix3, Mesh, Object3D, PerspectiveCamera, Raycaster, Scene, Sphere, Texture, ToneMapping, Triangle, Vector2, Vector3, WebGLRenderer, XRTargetRaySpace} from 'three'; +import {ACESFilmicToneMapping, AnimationActionLoopStyles, AnimationMixer, AnimationMixerEventMap, Box3, Camera, Euler, Event as ThreeEvent, LoopRepeat, Material, Matrix3, Mesh, Object3D, PerspectiveCamera, Raycaster, Scene, Sphere, Texture, ToneMapping, Triangle, Vector2, Vector3, WebGLRenderer, XRTargetRaySpace} from 'three'; import {CSS2DRenderer} from 'three/examples/jsm/renderers/CSS2DRenderer.js'; import {reduceVertices} from 'three/examples/jsm/utils/SceneUtils.js'; @@ -28,6 +28,7 @@ import {Damper, SETTLING_TIME} from './Damper.js'; import {ModelViewerGLTFInstance} from './gltf-instance/ModelViewerGLTFInstance.js'; import {GroundedSkybox} from './GroundedSkybox.js'; import {Hotspot} from './Hotspot.js'; +import {ModelData} from './ModelData.js'; import {Shadow} from './Shadow.js'; export const GROUNDED_SKYBOX_SIZE = 10; @@ -65,6 +66,7 @@ const ndc = new Vector2(); * Provides lights and cameras to be used in a renderer. */ export class ModelScene extends Scene { + public modelData: ModelData; public element: ModelViewerElement; public canvas: HTMLCanvasElement; public annotationRenderer = new CSS2DRenderer(); @@ -82,10 +84,10 @@ export class ModelScene extends Scene { public camera = new PerspectiveCamera(45, 1, 0.1, 100); public xrCamera: Camera|null = null; - public url: string|null = null; + public pivot = new Object3D(); public target = new Object3D(); - public animationNames: Array = []; + public boundingBox = new Box3(); public boundingSphere = new Sphere(); public size = new Vector3(); @@ -108,12 +110,8 @@ export class ModelScene extends Scene { private targetDamperY = new Damper(); private targetDamperZ = new Damper(); - private _currentGLTF: ModelViewerGLTFInstance|null = null; private _model: Object3D|null = null; private mixer: AnimationMixer; - private cancelPendingSourceChange: (() => void)|null = null; - private animationsByName: Map = new Map(); - private currentAnimationAction: AnimationAction|null = null; private groundedSkybox = new GroundedSkybox(); @@ -149,6 +147,8 @@ export class ModelScene extends Scene { style.top = '0'; this.element.shadowRoot!.querySelector('.default')!.appendChild(domElement); + this.modelData = new ModelData(); + this.schemaElement.setAttribute('type', 'application/ld+json'); } @@ -194,6 +194,8 @@ export class ModelScene extends Scene { await this.setupScene(); } + + /** * Sets the model via URL. */ @@ -201,14 +203,17 @@ export class ModelScene extends Scene { async setSource( url: string|null, progressCallback: (progress: number) => void = () => {}) { - if (!url || url === this.url) { + console.log('SK: ModelScene:SetSource Started'); + if (!url || url === this.modelData.url) { + console.log('SK: ModelScene:SetSource no change'); progressCallback(1); return; } this.reset(); - this.url = url; + if (this.externalRenderer != null) { + console.log('SK: ModelScene:SetSource externalRendere.load'); const framingInfo = await this.externalRenderer.load(progressCallback); this.boundingSphere.radius = framingInfo.framedRadius; @@ -216,58 +221,33 @@ export class ModelScene extends Scene { return; } - // If we have pending work due to a previous source change in progress, - // cancel it so that we do not incur a race condition: - if (this.cancelPendingSourceChange != null) { - this.cancelPendingSourceChange!(); - this.cancelPendingSourceChange = null; - } - let gltf: ModelViewerGLTFInstance; try { - gltf = await new Promise( - async (resolve, reject) => { - this.cancelPendingSourceChange = () => reject(); - try { - const result = await this.element[$renderer].loader.load( - url, this.element, progressCallback); - resolve(result); - } catch (error) { - reject(error); - } - }); + gltf = + await this.modelData.loadModel(url, this.element, progressCallback); } catch (error) { if (error == null) { // Loading was cancelled, so silently return return; } - throw error; } - this.cancelPendingSourceChange = null; this.reset(); - this.url = url; - this._currentGLTF = gltf; + this.modelData.url = url; + console.log('SK: ModelScene:SetSource gltf value', gltf); + + this.modelData.currentGLTF = gltf; if (gltf != null) { this._model = gltf.scene; this.target.add(gltf.scene); } - const {animations} = gltf!; - const animationsByName = new Map(); - const animationNames = []; - for (const animation of animations) { - animationsByName.set(animation.name, animation); - animationNames.push(animation.name); - } - this.animations = animations; - this.animationsByName = animationsByName; - this.animationNames = animationNames; + this.modelData.setUpAnimations(); await this.setupScene(); } @@ -285,7 +265,7 @@ export class ModelScene extends Scene { } reset() { - this.url = null; + this.modelData.url = null; this.renderCount = 0; this.queueRender(); if (this.shadow != null) { @@ -299,15 +279,15 @@ export class ModelScene extends Scene { this._model = null; } - const gltf = this._currentGLTF; + const gltf = this.modelData.currentGLTF; if (gltf != null) { gltf.dispose(); - this._currentGLTF = null; + this.modelData.currentGLTF = null; } - if (this.currentAnimationAction != null) { - this.currentAnimationAction.stop(); - this.currentAnimationAction = null; + if (this.modelData.currentAnimationAction != null) { + this.modelData.currentAnimationAction.stop(); + this.modelData.currentAnimationAction = null; } this.mixer.stopAllAction(); @@ -326,7 +306,7 @@ export class ModelScene extends Scene { } get currentGLTF() { - return this._currentGLTF; + return this.modelData.currentGLTF; } /** @@ -669,20 +649,7 @@ export class ModelScene extends Scene { this.queueShadowRender(); } - get animationTime(): number { - if (this.currentAnimationAction != null) { - const loopCount = - Math.max((this.currentAnimationAction as any)._loopCount, 0); - if (this.currentAnimationAction.loop === LoopPingPong && - (loopCount & 1) === 1) { - return this.duration - this.currentAnimationAction.time - } else { - return this.currentAnimationAction.time; - } - } - return 0; - } set animationTimeScale(value: number) { this.mixer.timeScale = value; @@ -692,18 +659,7 @@ export class ModelScene extends Scene { return this.mixer.timeScale; } - get duration(): number { - if (this.currentAnimationAction != null && - this.currentAnimationAction.getClip()) { - return this.currentAnimationAction.getClip().duration; - } - - return 0; - } - get hasActiveAnimation(): boolean { - return this.currentAnimationAction != null; - } /** * Plays an animation if there are any associated with the current model. @@ -715,10 +671,10 @@ export class ModelScene extends Scene { name: string|null = null, crossfadeTime: number = 0, loopMode: AnimationActionLoopStyles = LoopRepeat, repetitionCount: number = Infinity) { - if (this._currentGLTF == null) { + if (this.modelData.currentGLTF == null) { return; } - const {animations} = this; + const {animations} = this.modelData; if (animations == null || animations.length === 0) { return; } @@ -726,7 +682,7 @@ export class ModelScene extends Scene { let animationClip = null; if (name != null) { - animationClip = this.animationsByName.get(name); + animationClip = this.modelData.animationsByName.get(name); if (animationClip == null) { const parsedAnimationIndex = parseInt(name); @@ -743,10 +699,10 @@ export class ModelScene extends Scene { } try { - const {currentAnimationAction: lastAnimationAction} = this; + const {currentAnimationAction: lastAnimationAction} = this.modelData; const action = this.mixer.clipAction(animationClip, this); - this.currentAnimationAction = action; + this.modelData.currentAnimationAction = action; if (this.element.paused) { this.mixer.stopAllAction(); @@ -756,7 +712,7 @@ export class ModelScene extends Scene { action.crossFadeFrom(lastAnimationAction, crossfadeTime, false); } else if ( this.animationTimeScale > 0 && - this.animationTime == this.duration) { + this.animationTime == this.modelData.duration) { // This is a workaround for what I believe is a three.js bug. this.animationTime = 0; } @@ -774,7 +730,7 @@ export class ModelScene extends Scene { } stopAnimation() { - this.currentAnimationAction = null; + this.modelData.currentAnimationAction = null; this.mixer.stopAllAction(); } @@ -820,7 +776,7 @@ export class ModelScene extends Scene { */ setShadowIntensity(shadowIntensity: number) { this.shadowIntensity = shadowIntensity; - if (this._currentGLTF == null) { + if (this.modelData.currentGLTF == null) { return; } this.setBakedShadowVisibility(); diff --git a/packages/model-viewer/src/three-components/Shadow.ts b/packages/model-viewer/src/three-components/Shadow.ts index f344f0fc03..7bf7a4b8cb 100644 --- a/packages/model-viewer/src/three-components/Shadow.ts +++ b/packages/model-viewer/src/three-components/Shadow.ts @@ -121,7 +121,7 @@ export class Shadow extends Object3D { setScene(scene: ModelScene, softness: number, side: Side) { const {boundingBox, size, rotation, position} = this; - this.isAnimated = scene.animationNames.length > 0; + this.isAnimated = scene.modelData.animationNames.length > 0; this.boundingBox.copy(scene.boundingBox); this.size.copy(scene.size); this.maxDimension = Math.max(size.x, size.y, size.z) *