UNPKG

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
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