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