@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
162 lines (136 loc) • 4.91 kB
text/typescript
/* @license
* Copyright 2020 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 {Group, Mesh, Object3D} from 'three';
import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js';
import {SkeletonUtils} from 'three/examples/jsm/utils/SkeletonUtils.js';
import {Constructor} from '../utilities.js';
export const $prepared = Symbol('prepared');
export interface PreparedGLTF extends GLTF {
[$prepared]?: boolean;
}
export const $prepare = Symbol('prepare');
export const $preparedGLTF = Symbol('preparedGLTF');
export const $clone = Symbol('clone');
/**
* Represents the preparation and enhancement of the output of a Three.js
* GLTFLoader (a Three.js-flavor "GLTF"), to make it suitable for optimal,
* correct viewing in a given presentation context and also make the cloning
* process more explicit and legible.
*
* A GLTFInstance is API-compatible with a Three.js-flavor "GLTF", so it should
* be considered to be interchangeable with the loaded result of a GLTFLoader.
*
* This basic implementation only implements trivial preparation and enhancement
* of a GLTF. These operations are intended to be enhanced by inheriting
* classes.
*/
export class GLTFInstance implements GLTF {
/**
* Prepares a given GLTF for presentation and future cloning. A GLTF that is
* prepared can safely have this method invoked on it multiple times; it will
* only be prepared once, including after being cloned.
*/
static prepare(source: GLTF): PreparedGLTF {
if (source.scene == null) {
throw new Error('Model does not have a scene');
}
if ((source as PreparedGLTF)[$prepared]) {
return source;
}
const prepared = this[$prepare](source) as Partial<PreparedGLTF>;
// NOTE: ES5 Symbol polyfill is not compatible with spread operator
// so {...prepared, [$prepared]: true} does not work
prepared[$prepared] = true;
return prepared as PreparedGLTF;
}
/**
* Override in an inheriting class to apply specialty one-time preparations
* for a given input GLTF.
*/
protected static[$prepare](source: GLTF): GLTF {
// TODO(#195,#1003): We don't currently support multiple scenes, so we don't
// bother preparing extra scenes for now:
const {scene} = source;
const scenes = [scene];
return {...source, scene, scenes};
}
protected[$preparedGLTF]: PreparedGLTF;
get parser() {
return this[$preparedGLTF].parser;
}
get animations() {
return this[$preparedGLTF].animations;
}
get scene() {
return this[$preparedGLTF].scene;
}
get scenes() {
return this[$preparedGLTF].scenes;
}
get cameras() {
return this[$preparedGLTF].cameras;
}
get asset() {
return this[$preparedGLTF].asset;
}
get userData() {
return this[$preparedGLTF].userData;
}
constructor(preparedGLTF: PreparedGLTF) {
this[$preparedGLTF] = preparedGLTF;
}
/**
* Creates and returns a copy of this instance.
*/
clone<T extends GLTFInstance>(): T {
const GLTFInstanceConstructor = this.constructor as Constructor<T>;
const clonedGLTF = this[$clone]();
return new GLTFInstanceConstructor(clonedGLTF);
}
/**
* Cleans up any retained memory that might not otherwise be released when
* this instance is done being used.
*/
dispose(): void {
this.scenes.forEach((scene: Group) => {
scene.traverse((object: Object3D) => {
if (!(object as Mesh).isMesh) {
return;
}
const mesh = object as Mesh;
const materials =
Array.isArray(mesh.material) ? mesh.material : [mesh.material];
materials.forEach(material => {
material.dispose();
});
mesh.geometry.dispose();
});
});
}
/**
* Override in an inheriting class to implement specialized cloning strategies
*/
protected[$clone](): PreparedGLTF {
const source = this[$preparedGLTF];
// TODO(#195,#1003): We don't currently support multiple scenes, so we don't
// bother cloning extra scenes for now:
const scene = SkeletonUtils.clone(this.scene) as Group;
const scenes = [scene];
const userData = source.userData ? {...source.userData} : {};
return {...source, scene, scenes, userData};
}
}
export type GLTFInstanceConstructor =
Constructor<GLTFInstance, {prepare: typeof GLTFInstance['prepare']}>;