UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

456 lines 16.7 kB
import { iGeometryCommons, iMaterialCommons, iObjectCommons, LegacyPhongMaterial, PhysicalMaterial, UnlitLineMaterial, UnlitMaterial, upgradeTexture, } from '../core'; import { EventDispatcher } from 'three'; import { generateUUID } from '../three'; import { deepAccessObject } from 'ts-browser-helpers'; export class Object3DManager extends EventDispatcher { getVideos() { return [...this._videos]; } constructor() { super(); this._objects = new Set(); this._objectExtensions = []; this._materials = new Set(); this._geometries = new Set(); this._textures = new Set(); this._videos = new Set(); this.autoDisposeTextures = true; this.autoDisposeMaterials = true; this.autoDisposeGeometries = true; this.autoDisposeObjects = false; this._rootChanged = (ev) => { if (!ev.target || !this._root) return; const parent = ev.target.parentRoot; let inRoot = false; if (parent === this._root) inRoot = true; else { ev.target.traverseAncestors(a => { if (a === this._root) inRoot = true; }); } if (inRoot) { this.registerObject(ev.target); } else { this.unregisterObject(ev.target); } }; this._rootChanged = this._rootChanged.bind(this); // this._objAdded = this._objAdded.bind(this) } onPostFrame(time) { // const delta = time.delta for (const video of this._videos) { const data = video.userData.timeline; if (data) { if (!data.enabled) continue; } const elem = video.image; const delay = data?.delay || 0; const scale = data?.scale || 1; const start = data?.start || 0; const duration = elem.duration || 1; const end = duration - (data?.end || 0); // elem.pause() let t = time.time; t -= delay; t *= scale; if (t < start) t = start; if (t > end) t = end; if (t < 0) t = 0; if (t > duration) t = duration; const d1 = Math.abs(t - elem.currentTime); if ( /* d1 > delta && */d1 > 1 / 60) { // todo determine fps? // console.log(t) elem.currentTime = t; if (elem.paused) { const i1 = (video._playid || 0) + 1; // increment play id to avoid playing the video multiple times video._playid = i1; elem.play().then(() => { if (video._playid !== i1) return; // if play id changed, do not play the video if (!elem.paused) { elem.pause(); } delete video._playid; }); } } if (!time.running) { // if the timeline is not running, pause the video if (!elem.paused && !video._playid) { elem.pause(); } } } } setRoot(root) { this._root = root; } registerObject(obj) { if (!obj || !obj.uuid || !obj.isObject3D) return; if (this._objects.has(obj)) return; const existing = [...this._objects].find(o => o.uuid === obj.uuid); if (existing) { if (existing && obj !== existing) { console.error('AssetManager - Object with the same uuid already registered', obj, existing); } return; } if (!obj.assetType) { iObjectCommons.upgradeObject3D.call(obj); } this._objects.add(obj); obj.addEventListener('parentRootChanged', this._rootChanged); this._registerMaterials(obj.materials, obj); this._registerGeometry(obj.geometry, obj); const maps = Object3DManager.GetMapsForObject3D(obj); if (maps) for (const tex of maps) { this._registerTexture(tex, obj); } if (!obj.objectExtensions) obj.objectExtensions = []; const exts = obj.objectExtensions; for (const ext of this._objectExtensions) { if (exts.includes(ext)) continue; const compatible = ext.isCompatible ? ext.isCompatible(obj) : true; if (compatible) { exts.push(ext); ext.onRegister && ext.onRegister(obj); } } this.dispatchEvent({ type: 'objectAdd', object: obj }); } unregisterObject(obj) { if (!obj || !obj.uuid || !this._objects.has(obj)) return false; this._objects.delete(obj); // obj.removeEventListener('added', this._objAdded) this._unregisterMaterials(obj.materials, obj); this._unregisterGeometry(obj.geometry, obj); const maps = Object3DManager.GetMapsForObject3D(obj); if (maps) for (const tex of maps) { this._unregisterTexture(tex, obj); } if (this.autoDisposeObjects && obj.userData?.disposeOnIdle !== false) { // todo add disposeOnIdle to types and docs obj.dispose(false); } this.dispatchEvent({ type: 'objectRemove', object: obj }); return true; // todo - extensions are not removed from the object, so they can be reused later // if (obj.objectExtensions) { // for (const ext of this._objectExtensions) { // const ind1 = obj.objectExtensions.indexOf(ext) // if (ind1 >= 0) obj.objectExtensions.splice(ind1, 1) // } // } // listener is not removed, it will be used to know when its added back to root. todo - because of this reference to the manager is kept even after dispose, if the object is removed from the scene before dispose. but it would be empty. // obj.removeEventListener('parentRootChanged', this._rootChanged) } // private _objAdded = (ev: Event<'added', IObject3D>) => { // if (!ev.target) return // let inRoot = false // ev.target.traverseAncestors(a => { // if (a === this._root) inRoot = true // }) // if (!inRoot) return // this.registerObject(ev.target) // } registerObjectExtension(ext) { if (!ext) return; if (!ext.uuid) ext.uuid = generateUUID(); const ind = this._objectExtensions.includes(ext); if (ind) return; this._objectExtensions.push(ext); for (const obj of this._objects) { if (obj.objectExtensions && !obj.objectExtensions.includes(ext)) { const compatible = ext.isCompatible ? ext.isCompatible(obj) : true; if (compatible) { obj.objectExtensions.push(ext); } } } } unregisterObjectExtension(ext) { if (!ext) return; const ind = this._objectExtensions.indexOf(ext); if (ind < 0) return; this._objectExtensions.splice(ind, 1); // todo - extensions are not removed from objects at the moment, so they can be reused later // for (const obj of this._objects) { // if (obj.objectExtensions && obj.objectExtensions.includes(ext)) { // const ind1 = obj.objectExtensions.indexOf(ext) // if (ind1 >= 0) obj.objectExtensions.splice(ind1, 1) // } // } } // region materials _registerMaterials(mat, mesh) { return mat && mat.forEach(m => this._registerMaterial(m, mesh)); } _unregisterMaterials(mat, mesh) { return mat && mat.forEach(m => this._unregisterMaterial(m, mesh)); } _registerMaterial(mat, mesh) { if (!mat || !mat.isMaterial || !mesh || !mesh.uuid) return; if (!mat.assetType) { iMaterialCommons.upgradeMaterial.call(mat); } let meshes = mat.appliedMeshes; if (!meshes) { meshes = new Set(); mat.appliedMeshes = meshes; } const isNewMaterial = !this._materials.has(mat); meshes.add(mesh); this._materials.add(mat); const maps = Object3DManager.GetMapsForMaterial(mat); if (maps) for (const tex of maps) { this._registerTexture(tex, mat); } if (isNewMaterial) { this.dispatchEvent({ type: 'materialAdd', material: mat }); } } _unregisterMaterial(mat, mesh) { if (!mat || !mesh || !mesh.uuid) return; const meshes = mat.appliedMeshes; if (!meshes) return; meshes.delete(mesh); if (meshes.size === 0 && this._materials.has(mat)) { this._materials.delete(mat); const maps = Object3DManager.GetMapsForMaterial(mat); if (maps) for (const tex of maps) { this._unregisterTexture(tex, mat); } this.dispatchEvent({ type: 'materialRemove', material: mat }); if (this.autoDisposeMaterials) mat.dispose(false); } } // endregion // region geometry _registerGeometry(geom, mesh) { if (!geom || !geom.isBufferGeometry || !mesh || !mesh.uuid) return; if (!geom.assetType) { iGeometryCommons.upgradeGeometry.call(geom); } let meshes = geom.appliedMeshes; if (!meshes) { meshes = new Set(); geom.appliedMeshes = meshes; } const isNewGeometry = !this._geometries.has(geom); meshes.add(mesh); this._geometries.add(geom); if (isNewGeometry) { this.dispatchEvent({ type: 'geometryAdd', geometry: geom }); } } _unregisterGeometry(geom, mesh) { if (!geom || !mesh || !mesh.uuid) return; const meshes = geom.appliedMeshes; if (!meshes) return; meshes.delete(mesh); if (meshes.size === 0 && this._geometries.has(geom)) { this._geometries.delete(geom); this.dispatchEvent({ type: 'geometryRemove', geometry: geom }); if (this.autoDisposeGeometries) geom.dispose(false); } } // endregion // region textures _registerTexture(tex, obj) { if (!tex || !tex.isTexture || !obj || !obj.uuid) return; if (!tex.assetType) { upgradeTexture.call(tex); } let objects = tex.appliedObjects; if (!objects) { objects = new Set(); tex.appliedObjects = objects; } const isNewTexture = !this._textures.has(tex); objects.add(obj); this._textures.add(tex); if (tex.isVideoTexture) this._registerVideo(tex); if (isNewTexture) { this.dispatchEvent({ type: 'textureAdd', texture: tex }); } } _unregisterTexture(tex, obj) { if (!tex || !obj || !obj.uuid) return; const objects = tex.appliedObjects; if (!objects) return; objects.delete(obj); if (objects.size === 0 && this._textures.has(tex)) { this._textures.delete(tex); if (tex.isVideoTexture) this._videos.delete(tex); this.dispatchEvent({ type: 'textureRemove', texture: tex }); if (tex.userData?.disposeOnIdle !== false && this.autoDisposeTextures && !tex.isRenderTargetTexture && tex.dispose) tex.dispose(); if (tex.isVideoTexture) { const elem = tex.image; if (elem) { // elem.pause() // stop the video, todo required? } this.dispatchEvent({ type: 'videoRemove', video: tex }); } } } _registerVideo(tex) { this._videos.add(tex); const elem = tex.image; elem.preload = 'auto'; elem.autoplay = true; // elem.play().then(() => { // console.log('video started playing', elem) // elem.pause() // }) elem.loop = true; elem.muted = true; // to avoid autoplay issues in browsers this.dispatchEvent({ type: 'videoAdd', video: tex }); } // endregion textures // region utils findObject(nameOrUuid) { if (!nameOrUuid) return undefined; const obj = this._objects.values().find(o => o.uuid === nameOrUuid); if (obj) return obj; const obj1 = this.findObjectsByName(nameOrUuid); if (obj1.length > 1) { console.warn('Multiple objects found with name:', nameOrUuid, obj1); return undefined; } return obj1[0]; } findObjectsByName(name) { const objs = []; this._objects.forEach(o => { if (o.name === name) { objs.push(o); } }); return objs; } findMaterial(nameOrUuid) { if (!nameOrUuid) return undefined; const mat = this._materials.values().find(m => m.uuid === nameOrUuid); if (mat) return mat; const mats = this.findMaterialsByName(nameOrUuid); if (mats.length > 1) { console.warn('Multiple materials found with name:', nameOrUuid, mats); return undefined; } return mats[0]; } findMaterialsByName(name) { const mats = []; this._materials.forEach(m => { if (m.name === name) { mats.push(m); } }); return mats; } // endregion utils dispose() { const objects = [...this._objects]; for (const o of objects) { this.unregisterObject(o); o.removeEventListener('parentRootChanged', this._rootChanged); // o.removeEventListener('added', this._objAdded) } this._objectExtensions = []; this._objects.clear(); // todo should this dispatch objectRemove events? this._materials.clear(); // todo should this dispatch materialRemove events? this._geometries.clear(); // todo should this dispatch geometryRemove events? // this._root = undefined this.dispatchEvent({ type: 'dispose' }); } static GetMapsForMaterial(material) { const maps = new Set(); for (const prop of material.constructor?.MapProperties || Object3DManager.MaterialTextureProperties) { const val = prop in material ? material[prop] : undefined; if (val && val.isTexture) { maps.add(val); } Object3DManager._addMap(prop, material, maps); } if (material.userData) for (const prop of Object3DManager.MaterialTexturePropertiesUserData) { Object3DManager._addMap(prop, material.userData, maps, true); } return maps; } static GetMapsForObject3D(object) { const maps = new Set(); for (const prop of Object3DManager.Object3DTextureProperties) { Object3DManager._addMap(prop, object, maps); } if (object.isScene) { for (const prop of Object3DManager.SceneTextureProperties) { Object3DManager._addMap(prop, object, maps); } } return maps; } static _addMap(prop, object, maps, deep = false) { const val = deep ? deepAccessObject(prop, object, false) : prop in object ? object[prop] : undefined; if (val && val.isTexture) { maps.add(val); } } } Object3DManager.MaterialTextureProperties = new Set([ ...UnlitMaterial.MapProperties, ...UnlitLineMaterial.MapProperties, ...PhysicalMaterial.MapProperties, ...LegacyPhongMaterial.MapProperties, ]); // todo add from plugins like custom bump map etc. Object3DManager.MaterialTexturePropertiesUserData = new Set([]); Object3DManager.SceneTextureProperties = new Set([ 'environmentMap', 'background', ]); Object3DManager.Object3DTextureProperties = new Set([]); //# sourceMappingURL=Object3DManager.js.map