threepipe
Version:
A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.
538 lines • 23.6 kB
JavaScript
import { Cache as threeCache, EventDispatcher, LinearFilter, LinearMipmapLinearFilter, TextureLoader, } from 'three';
import { AssetImporter } from './AssetImporter';
import { generateUUID, getTextureDataType, overrideThreeCache } from '../three';
import { AmbientLight2, DirectionalLight2, HemisphereLight2, iCameraCommons, iLightCommons, iMaterialCommons, iObjectCommons, PerspectiveCamera2, PointLight2, RectAreaLight2, SpotLight2, upgradeTexture, } from '../core';
import { Importer } from './Importer';
import { MaterialManager } from './MaterialManager';
import { DRACOLoader2, GLTFLoader2, JSONMaterialLoader, MTLLoader2, OBJLoader2, ZipLoader } from './import';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
import { AssetExporter } from './AssetExporter';
import { GLTFExporter2 } from './export';
import { legacySeparateMapSamplerUVFix } from '../utils/legacy';
/**
* Asset Manager
*
* Utility class to manage import, export, and material management.
* @category Asset Manager
*/
export class AssetManager extends EventDispatcher {
get storage() {
return this._storage;
}
constructor(viewer, { simpleCache = false, storage } = {}) {
super();
this._gltfExporter = {
ext: ['gltf', 'glb'],
extensions: [],
ctor: (_, exporter) => {
const ex = new GLTFExporter2();
// This should be added at the end.
ex.setup(this.viewer, exporter.extensions);
return ex;
},
};
/**
* State of download/upload/process/other processes in the viewer.
* Subscribes to importer and exporter by default, more can be added by plugins like {@link FileTransferPlugin}
*/
this.processState = new Map();
// region glTF extensions registration helpers
this.gltfExtensions = [];
this._sceneUpdated = this._sceneUpdated.bind(this);
this.addAsset = this.addAsset.bind(this);
this.addRaw = this.addRaw.bind(this);
this._loaderCreate = this._loaderCreate.bind(this);
this.addImported = this.addImported.bind(this);
this.importer = new AssetImporter(!!viewer.getPlugin('debug'));
this.exporter = new AssetExporter();
this.materials = new MaterialManager();
this.viewer = viewer;
this.viewer.scene.addEventListener('addSceneObject', this._sceneUpdated);
this.viewer.scene.addEventListener('materialChanged', this._sceneUpdated);
this.viewer.scene.addEventListener('beforeDeserialize', this._sceneUpdated);
this._initCacheStorage(simpleCache, storage ?? true);
this._setupGltfExtensions();
this._setupObjectProcess();
this._setupProcessState();
this._addImporters();
this._addExporters();
}
async addAsset(assetOrPath, options) {
if (!this.importer || !this.viewer)
return [];
const imported = await this.importer.import(assetOrPath, options);
if (!imported) {
const path = typeof assetOrPath === 'string' ? assetOrPath : assetOrPath?.path;
if (path && !path.split('?')[0].endsWith('.vjson'))
console.warn('Threepipe AssetManager - Unable to import', assetOrPath, imported);
return [];
}
return this.loadImported(imported, options);
}
// materials: IMaterial[] = []
// textures: ITexture[] = []
// todo move this function to viewer
async loadImported(imported, { autoSetEnvironment = true, autoSetBackground = false, ...options } = {}) {
const arr = Array.isArray(imported) ? imported : [imported];
let ret = Array.isArray(imported) ? [] : undefined;
if (options?.importConfig !== false) {
const config = arr.find(v => v?.assetType === 'config') || arr.find(v => v && !!v.importedViewerConfig)?.importedViewerConfig;
if (config)
legacySeparateMapSamplerUVFix(config, arr.filter(a => a?.isObject3D));
}
for (const obj of arr) {
if (!obj) {
if (Array.isArray(ret))
ret.push(undefined);
continue;
}
let r = obj;
switch (obj.assetType) {
case 'material':
this.materials.registerMaterial(obj);
break;
case 'texture':
if (autoSetEnvironment && (obj.__rootPath?.endsWith('.hdr') || obj.__rootPath?.endsWith('.exr')))
this.viewer.scene.environment = obj;
if (autoSetBackground)
this.viewer.scene.background = obj;
break;
case 'model':
case 'light':
case 'camera':
r = await this.viewer.addSceneObject(obj, options); // todo update references in scene update event
break;
case 'config':
if (options?.importConfig !== false)
await this.viewer.importConfig(obj);
break;
default:
// legacy
if (obj.type && typeof obj.type === 'string' && (Array.isArray(obj.plugins) ||
obj.type === 'ThreeViewer' || this.viewer.getPlugin(obj.type))) {
await this.viewer.importConfig(obj);
}
break;
}
this.dispatchEvent({ type: 'loadAsset', data: obj });
if (Array.isArray(ret))
ret.push(r);
else
ret = r;
}
return ret || [];
}
/**
* same as {@link loadImported}
* @param imported
* @param options
*/
async addProcessedAssets(imported, options) {
return this.loadImported(imported, options);
}
async addAssetSingle(asset, options) {
return !asset ? undefined : (await this.addAsset(asset, options))?.[0];
}
// processAndAddObjects
async addRaw(res, options = {}) {
const r = await this.importer.processRaw(res, options);
return this.loadImported(r, options);
}
async addRawSingle(res, options = {}) {
return (await this.addRaw(res, options))?.[0];
}
_sceneUpdated(event) {
if (event.type === 'addSceneObject') {
const target = event.object;
switch (target.assetType) {
case 'material':
this.materials.registerMaterial(target);
break;
case 'texture':
break;
case 'model':
case 'light':
case 'camera':
break;
default:
break;
}
}
else if (event.type === 'materialChanged') {
const target = event.material;
const targets = Array.isArray(target) ? target : target ? [target] : [];
for (const t of targets) {
this.materials.registerMaterial(t);
}
}
else if (event.type === 'beforeDeserialize') {
// object/material/texture to be deserialized
const data = event.data;
const meta = event.meta;
if (!data.metadata) {
console.warn('Invalid data(no metadata)', data);
}
if (event.material) {
if (data.metadata?.type !== 'Material') {
console.warn('Invalid material data', data);
}
JSONMaterialLoader.DeserializeMaterialJSON(data, this.viewer, meta, event.material).then(() => {
//
});
}
}
else {
console.error('Unexpected');
}
}
dispose() {
this.importer.dispose();
this.materials.dispose();
this.processState.clear();
this.viewer.scene.removeEventListener('addSceneObject', this._sceneUpdated);
this.viewer.scene.removeEventListener('materialChanged', this._sceneUpdated);
this.exporter.dispose();
}
_addImporters() {
const viewer = this.viewer;
if (!viewer)
return;
const importers = [
new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'svg', 'ico', 'data:image', 'avif'], [
'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif',
], false), // todo: use ImageBitmapLoader if supported (better performance)
new Importer(JSONMaterialLoader, ['mat', ...this.materials.templates.map(t => t.typeSlug).filter(v => v)], // todo add others
[], false, (loader) => {
if (loader)
loader.viewer = this.viewer;
return loader;
}),
new Importer(class extends RGBELoader {
constructor(manager) {
super(manager);
this.setDataType(getTextureDataType(viewer.renderManager.renderer));
}
}, ['hdr'], ['image/vnd.radiance'], false),
new Importer(class extends EXRLoader {
constructor(manager) {
super(manager);
this.setDataType(getTextureDataType(viewer.renderManager.renderer));
}
}, ['exr'], ['image/x-exr'], false),
new Importer(FBXLoader, ['fbx'], ['model/fbx'], true),
new Importer(ZipLoader, ['zip', 'glbz', 'gltfz'], ['application/zip', 'model/gltf+zip', 'model/zip'], true), // gltfz and glbz are invented zip files with gltf/glb inside along with resources
new Importer(OBJLoader2, ['obj'], ['model/obj'], true),
new Importer(MTLLoader2, ['mtl'], ['model/mtl'], false),
new Importer(GLTFLoader2, ['gltf', 'glb', 'data:model/gltf', 'data:model/glb'], ['model/gltf', 'model/gltf+json', 'model/gltf-binary', 'model/glb'], true, (l, _, i) => l?.setup(this.viewer, i.extensions)),
new Importer(DRACOLoader2, ['drc'], ['model/mesh+draco', 'model/drc'], true),
];
this.importer.addImporter(...importers);
}
_addExporters() {
const exporters = [this._gltfExporter];
this.exporter.addExporter(...exporters);
}
_initCacheStorage(simpleCache, storage) {
if (storage === true && window?.caches) {
window.caches.open?.('threepipe-assetmanager').then(c => {
this._initCacheStorage(simpleCache, c);
this._storage = c;
});
return;
}
if (simpleCache || storage) {
// three.js built-in simple memory cache. used in FileLoader.js todo: use local storage somehow
if (simpleCache)
threeCache.enabled = true;
if (storage && window.Cache && typeof window.Cache === 'function' && storage instanceof window.Cache) {
overrideThreeCache(storage);
// todo: clear cache
}
}
this._storage = typeof storage === 'boolean' ? undefined : storage;
}
_setupObjectProcess() {
this.importer.addEventListener('processRaw', (event) => {
// console.log('preprocess mat', mat)
const mat = event.data;
if (!mat || !mat.isMaterial || !mat.uuid)
return;
if (this.materials?.findMaterial(mat.uuid)) {
console.warn('imported material uuid already exists, creating new uuid');
mat.uuid = generateUUID();
if (mat.userData.uuid)
mat.userData.uuid = mat.uuid;
}
// todo: check for name exists also
this.materials.registerMaterial(mat);
});
this.importer.addEventListener('processRawStart', (event) => {
// console.log('preprocess mat', mat)
const res = event.data;
const options = event.options;
// if (!res.assetType) {
// if (res.isBufferGeometry) { // for eg stl todo
// res = new Mesh(res, new MeshStandardMaterial())
// }
// if (res.isObject3D) {
// }
// }
if (res.isObject3D) {
const cameras = [];
const lights = [];
res.traverse((obj) => {
if (obj.material) {
const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
const newMaterials = [];
for (const material of materials) {
const mat = this.materials.convertToIMaterial(material, { createFromTemplate: options.replaceMaterials !== false }) || material;
mat.uuid = material.uuid;
mat.userData.uuid = material.uuid;
newMaterials.push(mat);
}
if (Array.isArray(obj.material))
obj.material = newMaterials;
else
obj.material = newMaterials[0];
}
if (obj.isCamera)
cameras.push(obj);
if (obj.isLight)
lights.push(obj);
});
for (const camera of cameras) {
if (camera.assetType === 'camera')
continue;
// todo: OrthographicCamera
if (!camera.isPerspectiveCamera || !camera.parent || options.replaceCameras === false) {
iCameraCommons.upgradeCamera.call(camera);
}
else {
const newCamera = camera.iCamera ??
new PerspectiveCamera2('', this.viewer.canvas);
if (camera === newCamera)
continue;
camera.parent.children.splice(camera.parent.children.indexOf(camera), 1, newCamera);
newCamera.parent = camera.parent;
newCamera.copy(camera);
camera.parent = null;
newCamera.uuid = camera.uuid;
newCamera.userData.uuid = camera.uuid;
camera.iCamera = newCamera;
// console.log('replacing camera', camera, newCamera)
}
}
for (const light of lights) {
if (light.assetType === 'light')
continue;
if (!light.parent || options.replaceLights === false) {
iLightCommons.upgradeLight.call(light);
}
else {
const newLight = (light.iLight ??
light.isDirectionalLight) ? new DirectionalLight2() :
light.isPointLight ? new PointLight2() :
light.isSpotLight ? new SpotLight2() :
light.isAmbientLight ? new AmbientLight2() :
light.isHemisphereLight ? new HemisphereLight2() :
light.isRectAreaLight ? new RectAreaLight2() :
undefined;
if (light === newLight || !newLight)
continue;
light.parent.children.splice(light.parent.children.indexOf(light), 1, newLight);
newLight.parent = light.parent;
newLight.copy(light);
light.parent = null;
newLight.uuid = light.uuid;
newLight.userData.uuid = light.uuid;
light.iLight = newLight;
}
}
iObjectCommons.upgradeObject3D.call(res);
}
else if (res.isMaterial) {
iMaterialCommons.upgradeMaterial.call(res);
// todo update res by generating new material?
}
else if (res.isTexture) {
upgradeTexture.call(res);
if (event?.options?.generateMipmaps !== undefined)
res.generateMipmaps = event?.options.generateMipmaps;
if (!res.generateMipmaps && !res.isRenderTargetTexture) { // todo: do we need to check more?
res.minFilter = res.minFilter === LinearMipmapLinearFilter ? LinearFilter : res.minFilter;
res.magFilter = res.magFilter === LinearMipmapLinearFilter ? LinearFilter : res.magFilter;
}
}
// todo other asset/object types?
});
}
/**
* Set process state for a path
* Progress should be a number between 0 and 100
* Pass undefined in value to remove the state
* @param path
* @param value
*/
setProcessState(path, value) {
if (value === undefined)
this.processState.delete(path);
else
this.processState.set(path, value);
this.dispatchEvent({ type: 'processStateUpdate' });
}
_setupProcessState() {
this.importer.addEventListener('importFile', (data) => {
this.setProcessState(data.path, data.state !== 'done' ? {
state: data.state,
progress: data.progress ? data.progress * 100 : undefined,
} : undefined);
});
this.importer.addEventListener('processRawStart', (data) => {
this.setProcessState(data.path, {
state: 'processing',
progress: undefined,
});
});
this.importer.addEventListener('processRaw', (data) => {
this.setProcessState(data.path, undefined);
});
this.exporter.addEventListener('exportFile', (data) => {
this.setProcessState(data.obj.name, data.state !== 'done' ? {
state: data.state,
progress: data.progress ? data.progress * 100 : undefined,
} : undefined);
});
}
_setupGltfExtensions() {
this.importer.addEventListener('loaderCreate', this._loaderCreate);
this.viewer.forPlugin('GLTFDracoExportPlugin', (p) => {
if (!p.addExtension)
return;
for (const gltfExtension of this.gltfExtensions) {
p.addExtension(gltfExtension.name, gltfExtension.textures);
}
});
}
_loaderCreate({ loader }) {
if (!loader.isGLTFLoader2)
return;
for (const gltfExtension of this.gltfExtensions) {
loader.register(gltfExtension.import);
}
}
registerGltfExtension(ext) {
const ext1 = this.gltfExtensions.findIndex(e => e.name === ext.name);
if (ext1 >= 0)
this.gltfExtensions.splice(ext1, 1);
this.gltfExtensions.push(ext);
this._gltfExporter.extensions.push(ext.export);
const exporter2 = this.exporter.getExporter('gltf', 'glb');
if (exporter2 && exporter2 !== this._gltfExporter)
exporter2.extensions?.push(ext.export);
}
unregisterGltfExtension(name) {
const ind = this.gltfExtensions.findIndex(e => e.name === name);
if (ind < 0)
return;
this.gltfExtensions.splice(ind, 1);
const ind1 = this._gltfExporter.extensions.findIndex(e => e.name === name);
if (ind1 >= 0)
this._gltfExporter.extensions.splice(ind1, 1);
const exporter2 = this.exporter.getExporter('gltf', 'glb');
if (exporter2?.extensions && exporter2 !== this._gltfExporter) {
const ind2 = exporter2.extensions.findIndex(e => e.name === name);
if (ind2 >= 0)
exporter2.extensions?.splice(ind2, 1);
}
}
// endregion
// region deprecated
/**
* @deprecated use addRaw instead
* @param res
* @param options
*/
async addImported(res, options = {}) {
console.error('addImported is deprecated, use addRaw instead');
return this.addRaw(res, options);
}
/**
* @deprecated use addAsset instead
* @param path
* @param options
*/
async addFromPath(path, options = {}) {
console.error('addFromPath is deprecated, use addAsset instead');
return this.addAsset(path, options);
}
/**
* @deprecated use {@link ThreeViewer.exportConfig} instead
* @param binary - if set to false, encodes all the array buffers to base64
*/
exportViewerConfig(binary = true) {
if (!this.viewer)
return {};
console.error('exportViewerConfig is deprecated, use viewer.toJSON instead');
return this.viewer.toJSON(binary, undefined);
}
/**
* @deprecated use {@link ThreeViewer.exportPluginsConfig} instead
* @param filter
*/
exportPluginPresets(filter) {
console.error('exportPluginPresets is deprecated, use viewer.exportPluginsConfig instead');
return this.viewer?.exportPluginsConfig(filter);
}
/**
* @deprecated use {@link ThreeViewer.exportPluginConfig} instead
* @param plugin
*/
exportPluginPreset(plugin) {
console.error('exportPluginPreset is deprecated, use viewer.exportPluginConfig instead');
return this.viewer?.exportPluginConfig(plugin);
}
/**
* @deprecated use {@link ThreeViewer.importPluginConfig} instead
* @param json
* @param plugin
*/
async importPluginPreset(json, plugin) {
console.error('importPluginPreset is deprecated, use viewer.importPluginConfig instead');
return this.viewer?.importPluginConfig(json, plugin);
}
// todo continue from here by moving functions to the viewer.
/**
* @deprecated use {@link ThreeViewer.importConfig} instead
* @param viewerConfig
*/
async importViewerConfig(viewerConfig) {
return this.viewer?.importConfig(viewerConfig);
}
/**
* @deprecated use {@link ThreeViewer.fromJSON} instead
* @param viewerConfig
*/
applyViewerConfig(viewerConfig, resources) {
console.error('applyViewerConfig is deprecated, use viewer.fromJSON instead');
return this.viewer?.fromJSON(viewerConfig, resources);
}
/**
* @deprecated moved to {@link ThreeViewer.loadConfigResources}
* @param json
* @param extraResources - preloaded resources in the format of viewer config resources.
*/
async importConfigResources(json, extraResources) {
if (!this.importer)
throw 'Importer not initialized yet.';
if (json.__isLoadedResources)
return json;
return this.viewer?.loadConfigResources(json, extraResources);
}
}
/**
* @deprecated not a plugin anymore
*/
AssetManager.PluginType = 'AssetManager';
//# sourceMappingURL=AssetManager.js.map