@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
256 lines • 8.97 kB
JavaScript
/*
* Copyright 2018 Google Inc. 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.
*/
var _a;
import { AnimationMixer, Box3, Object3D, Vector3 } from 'three';
import { $releaseFromCache, CachingGLTFLoader } from './CachingGLTFLoader.js';
import { moveChildren } from './ModelUtils.js';
const $cancelPendingSourceChange = Symbol('cancelPendingSourceChange');
const $currentScene = Symbol('currentScene');
/**
* An Object3D that can swap out its underlying
* model.
*
* @extends THREE.Object3D
*/
export default class Model extends Object3D {
/**
* Creates a model.
*/
constructor() {
super();
this[_a] = null;
this.loader = new CachingGLTFLoader();
this.mixer = new AnimationMixer(null);
this.animations = [];
this.animationsByName = new Map();
this.currentAnimationAction = null;
this.modelContainer = new Object3D();
this.animationNames = [];
this.boundingBox = new Box3();
this.size = new Vector3();
this.userData = { url: null };
this.url = null;
this.name = 'Model';
this.modelContainer.name = 'ModelContainer';
this.add(this.modelContainer);
}
/**
* Returns a boolean indicating whether or not there is a
* loaded model attached.
*
* @return {Boolean}
*/
hasModel() {
return !!this.modelContainer.children.length;
}
applyEnvironmentMap(map) {
// Note that unlit models (using MeshBasicMaterial) should not apply
// an environment map, even though `map` is the currently configured
// environment map.
this.modelContainer.traverse((obj) => {
// There are some cases where `obj.material` is
// an array of materials.
const mesh = obj;
if (Array.isArray(mesh.material)) {
for (let material of mesh.material) {
if (material.isMeshBasicMaterial) {
continue;
}
material.envMap = map;
material.needsUpdate = true;
}
}
else if (mesh.material && !mesh.material.isMeshBasicMaterial) {
mesh.material.envMap = map;
mesh.material.needsUpdate = true;
}
});
this.dispatchEvent({ type: 'envmap-change', value: map });
}
setEnvironmentMapIntensity(intensity) {
const intensityIsNumber = typeof intensity === 'number' && !self.isNaN(intensity);
if (!intensityIsNumber) {
intensity = 1.0;
}
this.modelContainer.traverse(object => {
if (object && object.isMesh && object.material) {
const { material } = object;
if (Array.isArray(material)) {
material.forEach(material => material.envMapIntensity =
intensity);
}
else {
object.material.envMapIntensity =
intensity;
}
}
});
}
/**
* Pass in a THREE.Object3D to be controlled
* by this model.
*
* @param {THREE.Object3D}
*/
setObject(model) {
this.clear();
this.modelContainer.add(model);
this.updateBoundingBox();
this.dispatchEvent({ type: 'model-load' });
}
/**
* @param {String?} url
* @param {Function?} progressCallback
*/
async setSource(url, progressCallback) {
if (!url || url === this.url) {
if (progressCallback) {
progressCallback(1);
}
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;
}
this.url = url;
let scene = null;
try {
scene = await new Promise(async (resolve, reject) => {
this[$cancelPendingSourceChange] = () => reject();
try {
const result = await this.loader.load(url, progressCallback);
resolve(result);
}
catch (error) {
reject(error);
}
});
}
catch (error) {
if (error == null) {
return;
}
throw error;
}
this.clear();
this[$currentScene] = scene;
if (scene != null) {
moveChildren(scene, this.modelContainer);
}
this.modelContainer.traverse(obj => {
if (obj && obj.type === 'Mesh') {
obj.castShadow = true;
}
});
const animations = scene ? scene.userData.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;
this.userData.url = url;
this.updateBoundingBox();
this.dispatchEvent({ type: 'model-load', url });
}
set animationTime(value) {
if (this.currentAnimationAction != null) {
this.currentAnimationAction.time = value;
}
}
get animationTime() {
if (this.currentAnimationAction != null) {
return this.currentAnimationAction.time;
}
return 0;
}
get hasActiveAnimation() {
return this.currentAnimationAction != null;
}
/**
* Plays an animation if there are any associated with the current model.
* Accepts an optional string name of an animation to play. If no name is
* provided, or if no animation is found by the given name, always falls back
* to playing the first animation.
*/
playAnimation(name = null, crossfadeTime = 0) {
const { animations } = this;
if (animations == null || animations.length === 0) {
console.warn(`Cannot play animation (model does not have any animations)`);
return;
}
let animationClip = null;
if (name != null) {
animationClip = this.animationsByName.get(name);
}
if (animationClip == null) {
animationClip = animations[0];
}
try {
const { currentAnimationAction: lastAnimationAction } = this;
this.currentAnimationAction =
this.mixer.clipAction(animationClip, this).play();
this.currentAnimationAction.enabled = true;
if (lastAnimationAction != null &&
this.currentAnimationAction !== lastAnimationAction) {
this.currentAnimationAction.crossFadeFrom(lastAnimationAction, crossfadeTime, false);
}
}
catch (error) {
console.error(error);
}
}
stopAnimation() {
if (this.currentAnimationAction != null) {
this.currentAnimationAction.stop();
this.currentAnimationAction.reset();
this.currentAnimationAction = null;
}
this.mixer.stopAllAction();
}
updateAnimation(step) {
this.mixer.update(step);
}
clear() {
this.url = null;
this.userData = { url: null };
// Remove all current children
if (this[$currentScene] != null) {
moveChildren(this.modelContainer, this[$currentScene]);
this[$currentScene][$releaseFromCache]();
this[$currentScene] = null;
}
if (this.currentAnimationAction != null) {
this.currentAnimationAction.stop();
this.currentAnimationAction = null;
}
this.mixer.stopAllAction();
this.mixer.uncacheRoot(this);
}
updateBoundingBox() {
this.remove(this.modelContainer);
this.boundingBox.setFromObject(this.modelContainer);
this.boundingBox.getSize(this.size);
this.add(this.modelContainer);
}
}
_a = $currentScene;
//# sourceMappingURL=Model.js.map