@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
218 lines (177 loc) • 6.34 kB
text/typescript
/* @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 {Event as ThreeEvent, EventDispatcher, WebGLRenderer} from 'three';
import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader.js';
import {GLTF, GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js';
import {KTX2Loader} from 'three/examples/jsm/loaders/KTX2Loader';
import ModelViewerElementBase from '../model-viewer-base.js';
import {CacheEvictionPolicy} from '../utilities/cache-eviction-policy.js';
import {GLTFInstance, GLTFInstanceConstructor} from './GLTFInstance.js';
export type ProgressCallback = (progress: number) => void;
export interface PreloadEvent extends ThreeEvent {
type: 'preload';
element: ModelViewerElementBase;
src: String;
}
/**
* A helper to Promise-ify a Three.js GLTFLoader
*/
export const loadWithLoader =
(url: string,
loader: GLTFLoader,
progressCallback: ProgressCallback = () => {}) => {
const onProgress = (event: ProgressEvent) => {
const fraction = event.loaded / event.total;
progressCallback!
(Math.max(0, Math.min(1, isFinite(fraction) ? fraction : 1)));
};
return new Promise<GLTF>((resolve, reject) => {
loader.load(url, resolve, onProgress, reject);
});
};
const cache = new Map<string, Promise<GLTFInstance>>();
const preloaded = new Map<string, boolean>();
let dracoDecoderLocation: string;
const dracoLoader = new DRACOLoader();
let ktx2TranscoderLocation: string;
const ktx2Loader = new KTX2Loader();
export const $loader = Symbol('loader');
export const $evictionPolicy = Symbol('evictionPolicy');
const $GLTFInstance = Symbol('GLTFInstance');
export class CachingGLTFLoader<T extends GLTFInstanceConstructor =
GLTFInstanceConstructor> extends
EventDispatcher {
static setDRACODecoderLocation(url: string) {
dracoDecoderLocation = url;
dracoLoader.setDecoderPath(url);
}
static getDRACODecoderLocation() {
return dracoDecoderLocation;
}
static setKTX2TranscoderLocation(url: string) {
ktx2TranscoderLocation = url;
ktx2Loader.setTranscoderPath(url);
}
static getKTX2TranscoderLocation() {
return ktx2TranscoderLocation;
}
static initializeKTX2Loader(renderer: WebGLRenderer) {
ktx2Loader.detectSupport(renderer);
}
static[$evictionPolicy]: CacheEvictionPolicy =
new CacheEvictionPolicy(CachingGLTFLoader);
static get cache() {
return cache;
}
/** @nocollapse */
static clearCache() {
cache.forEach((_value, url) => {
this.delete(url);
});
this[$evictionPolicy].reset();
}
static has(url: string) {
return cache.has(url);
}
/** @nocollapse */
static async delete(url: string) {
if (!this.has(url)) {
return;
}
const gltfLoads = cache.get(url);
preloaded.delete(url);
cache.delete(url);
const gltf = await gltfLoads;
// Dispose of the cached glTF's materials and geometries:
gltf!.dispose();
}
/**
* Returns true if the model that corresponds to the specified url is
* available in our local cache.
*/
static hasFinishedLoading(url: string) {
return !!preloaded.get(url);
}
constructor(GLTFInstance: T) {
super();
this[$GLTFInstance] = GLTFInstance;
this[$loader].setDRACOLoader(dracoLoader);
this[$loader].setKTX2Loader(ktx2Loader);
}
protected[$loader]: GLTFLoader = new GLTFLoader();
protected[$GLTFInstance]: T;
protected get[$evictionPolicy](): CacheEvictionPolicy {
return (this.constructor as typeof CachingGLTFLoader)[$evictionPolicy];
}
/**
* Preloads a glTF, populating the cache. Returns a promise that resolves
* when the cache is populated.
*/
async preload(
url: string, element: ModelViewerElementBase,
progressCallback: ProgressCallback = () => {}) {
this.dispatchEvent(
{type: 'preload', element: element, src: url} as PreloadEvent);
if (!cache.has(url)) {
const rawGLTFLoads =
loadWithLoader(url, this[$loader], (progress: number) => {
progressCallback(progress * 0.8);
});
const GLTFInstance = this[$GLTFInstance];
const gltfInstanceLoads = rawGLTFLoads
.then((rawGLTF) => {
return GLTFInstance.prepare(rawGLTF);
})
.then((preparedGLTF) => {
progressCallback(0.9);
return new GLTFInstance(preparedGLTF);
});
cache.set(url, gltfInstanceLoads);
}
await cache.get(url);
preloaded.set(url, true);
if (progressCallback) {
progressCallback(1.0);
}
}
/**
* Loads a glTF from the specified url and resolves a unique clone of the
* glTF. If the glTF has already been loaded, makes a clone of the cached
* copy.
*/
async load(
url: string, element: ModelViewerElementBase,
progressCallback: ProgressCallback = () => {}): Promise<InstanceType<T>> {
await this.preload(url, element, progressCallback);
const gltf = await cache.get(url)!;
const clone = await gltf.clone() as InstanceType<T>;
this[$evictionPolicy].retain(url);
// Patch dispose so that we can properly account for instance use
// in the caching layer:
clone.dispose = (() => {
const originalDispose = clone.dispose;
let disposed = false;
return () => {
if (disposed) {
return;
}
disposed = true;
originalDispose.apply(clone);
this[$evictionPolicy].release(url);
};
})();
return clone;
}
}