UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

502 lines 21.8 kB
import { Matrix4, Vector3 } from 'three'; import { objectHasOwn } from 'ts-browser-helpers'; import { copyObject3DUserData } from '../../utils'; import { Box3B } from '../../three'; import { makeIObject3DUiConfig } from './IObjectUi'; import { iGeometryCommons } from '../geometry/iGeometryCommons'; import { iMaterialCommons } from '../material/iMaterialCommons'; export const iObjectCommons = { setDirty: function (options) { this.dispatchEvent({ bubbleToParent: true, ...options, type: 'objectUpdate', object: this }); // this sets sceneUpdate in root scene if (options?.refreshUi !== false && options?.last !== false) this.refreshUi?.(); // console.log('object update') }, upgradeObject3D: upgradeObject3D, makeUiConfig: makeIObject3DUiConfig, autoCenter: function (setDirty = true, undo = false) { if (undo) { if (!this.userData.autoCentered || !this.userData._lastCenter) return this; this.position.add(this.userData._lastCenter); delete this.userData.autoCentered; delete this.userData.isCentered; delete this.userData._lastCenter; } else { const bb = new Box3B().expandByObject(this, true, true); const center = bb.getCenter(new Vector3()); this.userData._lastCenter = center; /* .clone()*/ this.position.sub(center); this.userData.autoCentered = true; this.userData.isCentered = true; } this.updateMatrix(); if (setDirty) this.setDirty({ change: 'autoCenter', undo }); return this; }, autoScale: function (autoScaleRadius, isCentered, setDirty = true, undo = false) { let scale = 1; if (undo) { // Note - undo only works for quick undo, not for multiple times if (!this.userData.autoScaled || !this.userData._lastScaleRadius) return this; const rad = this.userData.autoScaleRadius || autoScaleRadius || 1; scale = this.userData._lastScaleRadius / rad; if (!isFinite(scale)) return this; // NaN when radius is 0 this.userData.autoScaled = true; this.userData.autoScaleRadius = autoScaleRadius; delete this.userData._lastScaleRadius; } else { const bbox = new Box3B().expandByObject(this, true, true); const radius = bbox.getSize(new Vector3()).length() * 0.5; if (autoScaleRadius === undefined) { autoScaleRadius = this.userData.autoScaleRadius || 1; } scale = autoScaleRadius / radius; if (!isFinite(scale)) return this; // NaN when radius is 0 this.userData.autoScaled = true; this.userData.autoScaleRadius = autoScaleRadius; this.userData._lastScaleRadius = radius; } if (this.userData.pseudoCentered) { this.children.forEach(child => { child.scale.multiplyScalar(scale); }); } else this.scale.multiplyScalar(scale); if (isCentered || this.userData.isCentered) this.position.multiplyScalar(scale); this.traverse((obj) => { const l = obj; if (l.isLight && l.shadow?.camera?.right) { l.shadow.camera.right *= scale; l.shadow.camera.left *= scale; l.shadow.camera.top *= scale; l.shadow.camera.bottom *= scale; obj.setDirty(); } if (l.isCamera && l.right) { l.right *= scale; l.left *= scale; l.top *= scale; l.bottom *= scale; obj.setDirty(); } }); if (setDirty) this.setDirty({ change: 'autoScale', undo }); return this; }, pivotToBoundsCenter: function (setDirty = true) { const bb = new Box3B().expandByObject(this, true, true); const center = bb.getCenter(new Vector3()); return iObjectCommons.pivotToPoint.call(this, center, setDirty); }, pivotToPoint: function (point, setDirty = true) { const worldCenter = new Vector3().copy(point); const localCenter = new Vector3().copy(worldCenter); const worldMatrixInv = new Matrix4().copy(this.matrixWorld).invert(); const m = this.parent?.matrixWorld; const parentWorldMatrixInv = new Matrix4(); if (m !== undefined) parentWorldMatrixInv.copy(m).invert(); // Get the center with respect to the parent worldCenter.applyMatrix4(parentWorldMatrixInv); const lastPosition = this.position.clone(); // Apply the new position this.position.copy(worldCenter); // local center localCenter.applyMatrix4(worldMatrixInv).negate(); // Shift the geometry if (this.geometry) { this.geometry.translate(localCenter.x, localCenter.y, localCenter.z); } // Add offsets this.children.forEach((object) => { object.position.add(localCenter); }); if (setDirty) this.setDirty({ change: 'pivotToPoint', undo: false }); return () => { // undo this.position.copy(lastPosition); if (this.geometry) { this.geometry.translate(-localCenter.x, -localCenter.y, -localCenter.z); } this.children.forEach((object) => { object.position.sub(localCenter); }); if (setDirty) this.setDirty({ change: 'pivotToPoint', undo: true }); }; }, eventCallbacks: { onAddedToParent: function (e) { // added to some parent const root = this.parent?.parentRoot ?? this.parent; if (!this.objectProcessor && root?.objectProcessor) { // this is added so that when an upgraded(not processed) object is added to the scene, it will be processed by the scene processor this.traverse(o => { o.objectProcessor = root.objectProcessor; o.objectProcessor?.processObject(o); }); } if (root !== this.parentRoot) { this.traverse(o => { o.parentRoot = root; }); } this.setDirty?.({ ...e, change: 'addedToParent' }); }, onRemovedFromParent: function (e) { // removed from some parent this.setDirty?.({ ...e, change: 'removedFromParent' }); if (this.parentRoot !== undefined) { this.traverse(o => { o.parentRoot = undefined; }); } }, onGeometryUpdate: function (e) { if (!e.bubbleToObject) return; this.dispatchEvent({ bubbleToParent: true, ...e, object: this, geometry: e.geometry }); }, }, initMaterial: function () { if (objectHasOwn(this, '_currentMaterial')) return; this._currentMaterial = null; const currentMaterial = this.material; delete this.material; Object.defineProperty(this, 'material', { get: iObjectCommons.getMaterial, set: iObjectCommons.setMaterial, }); Object.defineProperty(this, 'materials', { get: iObjectCommons.getMaterials, set: iObjectCommons.setMaterials, }); // this is called initially in Material manager from process model below, not required here... // todo: shouldnt be called from there. maybe check if material is upgraded before // if (currentMaterial && !Array.isArray(currentMaterial) && !currentMaterial.assetType) { // console.error('todo: initMaterial: material not upgraded') // } this.material = currentMaterial; // Legacy if (!this.setMaterial) { this.setMaterial = (m) => { const mats = this.material; console.error('setMaterial is deprecated, use material property directly'); this.material = m; return mats; }; } // Legacy if (this.userData.setMaterial) console.error('userData.setMaterial already defined'); this.userData.setMaterial = (m) => { console.error('userData.setMaterial is deprecated, use setMaterial directly'); this.material = m; }; }, getMaterial: function () { return this._currentMaterial || undefined; }, getMaterials: function () { return !this._currentMaterial ? [] : Array.isArray(this._currentMaterial) ? [...this._currentMaterial] : [this._currentMaterial]; }, setMaterial: function (material) { const imats = (Array.isArray(material) ? material : [material]).filter(v => v); if (this.material == imats || imats.length === 1 && this.material === imats[0]) return []; // todo: check by uuid? // Remove old material listeners const mats = Array.isArray(this.material) ? [...this.material] : [this.material]; for (const mat of mats) { if (!mat) continue; if (mat.appliedMeshes) { mat.appliedMeshes.delete(this); // if (mat.userData && mat.appliedMeshes?.size === 0 && mat.userData.disposeOnIdle !== false) mat.dispose(false); // this will dispose textures(if they are idle) if the material is registered in the material manager } } const materials = []; for (const mat of imats) { // const mat = material?.materialObject if (!mat) continue; if (!mat.assetType) { console.warn('Upgrading Material', mat); iMaterialCommons.upgradeMaterial.call(mat); } materials.push(mat); if (mat) { mat.appliedMeshes.add(this); } } this._currentMaterial = !materials.length ? null : materials.length !== 1 ? materials : materials[0] || null; this.dispatchEvent({ type: 'materialChanged', material, oldMaterial: mats, object: this, bubbleToParent: true }); this.refreshUi(); }, setMaterials: function (materials) { this.material = materials || undefined; }, initGeometry: function () { const currentGeometry = this.geometry; this._currentGeometry = null; delete this.geometry; Object.defineProperty(this, 'geometry', { get: iObjectCommons.getGeometry, set: iObjectCommons.setGeometry, }); this.geometry = currentGeometry; // Legacy if (!this.setGeometry) { this.setGeometry = (geometry) => { const geom = this.geometry; console.error('setGeometry is deprecated, use geometry property directly'); this.geometry = geometry; return geom; }; } // Legacy if (this.userData.setGeometry) console.error('userData.setGeometry already defined'); this.userData.setGeometry = (g) => { console.error('userData.setGeometry is deprecated, use setGeometry directly'); this.geometry = g; }; }, getGeometry: function () { return this._currentGeometry || undefined; }, setGeometry: function (geometry) { const geom = this.geometry || undefined; // todo: check by uuid? if (geom === geometry) return; if (geom) { this._onGeometryUpdate && geom.removeEventListener('geometryUpdate', this._onGeometryUpdate); if (geom.appliedMeshes) { geom.appliedMeshes.delete(this); geom.dispose(false); } } if (geometry) { if (!geometry.assetType) { // console.error('Geometry not upgraded') iGeometryCommons.upgradeGeometry.call(geometry); } } this._currentGeometry = geometry || null; if (geometry) { this.updateMorphTargets(); this._onGeometryUpdate && geometry.addEventListener('geometryUpdate', this._onGeometryUpdate); geometry.appliedMeshes.add(this); } this.dispatchEvent({ type: 'geometryChanged', geometry, oldGeometry: geom, bubbleToParent: true }); this.refreshUi(); }, refreshUi: function () { this.uiConfig?.uiRefresh?.(true, 'postFrame', 1); }, dispatchEvent: (superDispatch) => function (event) { if (event.bubbleToParent || this.userData?.__autoBubbleToParentEvents?.includes(event.type)) { // console.log('parent dispatch', e, this.parentRoot, this.parent) const pRoot = this.parentRoot || this.parent; if (this.parentRoot !== this) pRoot?.dispatchEvent(event); } superDispatch.call(this, event); }, clone: (superClone) => function (...args) { const userData = this.userData; this.userData = {}; const clone = superClone.call(this, ...args); this.userData = userData; copyObject3DUserData(clone.userData, userData); // todo: do same for this.toJSON() const objParent = this.parentRoot || undefined; if (objParent && objParent.assetType !== 'model') { console.warn('Cloning an IObject with a parent that is not an \'model\' is not supported'); } iObjectCommons.upgradeObject3D.call(clone, objParent, this.objectProcessor); clone.userData.cloneParent = this.uuid; return clone; }, copy: (superCopy) => function (source, ...args) { const lightTarget = this.isLight ? this.target : null; const userData = source.userData; source.userData = {}; const selfUserData = this.userData; superCopy.call(this, source, ...args); this.userData = selfUserData; source.userData = userData; copyObject3DUserData(this.userData, source.userData); // todo: do same for object.toJSON() if (lightTarget && this.target) { // For eg DirectionalLight2 lightTarget.position.copy(this.target.position); lightTarget.updateMatrixWorld(); this.target = lightTarget; // because t is a child and because of UI. } return this; }, add: (superAdd) => function (...args) { for (const a of args) iObjectCommons.upgradeObject3D.call(a, this.parentRoot || this, this.objectProcessor); return superAdd.call(this, ...args); }, dispose: (superDispose) => function (removeFromParent = true) { if (removeFromParent && this.parent) { this.removeFromParent(); delete this.parentRoot; } this.dispatchEvent({ type: 'dispose', bubbleToParent: false }); // if (this.__disposed) { // console.warn('Object already disposed', this) // return // } // this.__disposed = true for (const c of [...this.children]) c?.dispose && c.dispose(false); // not removing the children from parent to preserve hierarchy // this.children = [] // this.uiConfig?.dispose?.() // todo: make uiConfig.dispose superDispose?.call(this); }, }; /** * Converts three.js Object3D to IObject3D, setup object events, adds utility methods, and runs objectProcessor. * @param parent * @param objectProcessor */ function upgradeObject3D(parent, objectProcessor) { if (!this) return; // console.log('upgradeObject3D', this, parent, objectProcessor) // if (this.__disposed) { // console.warn('re-init/re-add disposed object, things might not work as intended', this) // delete this.__disposed // } if (!this.userData) this.userData = {}; this.userData.uuid = this.uuid; // not checking assetType but custom var __objectSetup because its required in types sometimes, check PerspectiveCamera2 // if (this.assetType) return if (this.userData.__objectSetup) return; this.userData.__objectSetup = true; if (!this.objectProcessor) this.objectProcessor = objectProcessor || this.parent?.objectProcessor || parent?.objectProcessor; if (!this.userData.__autoBubbleToParentEvents) this.userData.__autoBubbleToParentEvents = ['select']; // Event bubbling. todo: set bubbleToParent in these events when dispatched from child and remove from here? if (this.isLight) this.assetType = 'light'; else if (this.isCamera) this.assetType = 'camera'; else if (this.isWidget) this.assetType = 'widget'; else this.assetType = 'model'; if (parent) this.parentRoot = parent; // const oldFunctions = { // dispatchEvent: this.dispatchEvent, // clone: this.clone, // copy: this.copy, // add: this.add, // dispose: this.dispose, // } // this.addEventListener('dispose', () => Object.assign(this, oldFunctions)) // todo: is this required? // typed because of type-checking this.dispatchEvent = iObjectCommons.dispatchEvent(this.dispatchEvent); this.dispose = iObjectCommons.dispose(this.dispose); this.clone = iObjectCommons.clone(this.clone); this.copy = iObjectCommons.copy(this.copy); // todo: do same for object.toJSON() this.add = iObjectCommons.add(this.add); if (!this.setDirty) this.setDirty = iObjectCommons.setDirty; if (!this.refreshUi) this.refreshUi = iObjectCommons.refreshUi; if (!this.autoScale) this.autoScale = iObjectCommons.autoScale.bind(this); if (!this.autoCenter) this.autoCenter = iObjectCommons.autoCenter.bind(this); if (!this.pivotToBoundsCenter) this.pivotToBoundsCenter = iObjectCommons.pivotToBoundsCenter.bind(this); if (!this.pivotToPoint) this.pivotToPoint = iObjectCommons.pivotToPoint.bind(this); // fired from Object3D.js this.addEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent); this.addEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent); // this.addEventListener('dispose', ()=>{ // this.removeEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) // this.removeEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent) // }) if ((this.isMesh || this.isLine) && !this.userData.__meshSetup) { this.userData.__meshSetup = true; this._onGeometryUpdate = (e) => iObjectCommons.eventCallbacks.onGeometryUpdate.call(this, e); // Material, Geometry prop init iObjectCommons.initMaterial.call(this); iObjectCommons.initGeometry.call(this); // from GLTFObject3DExtrasExtension if (!this.userData.__keepShadowDef) { this.castShadow = true; this.receiveShadow = true; this.userData.__keepShadowDef = true; } this.addEventListener('dispose', () => { (this.materials || [this.material]).forEach(m => m?.dispose(false)); this.geometry?.dispose(false); // if (this.material) { // // const oldMats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!] // this.material = undefined // this will dispose material if not used by other meshes // // delete this.material // // for (const oldMat of oldMats) { // // if (oldMat && oldMat.userData && oldMat.appliedMeshes?.size === 0 && oldMat.userData.disposeOnIdle !== false) oldMat.dispose() // // } // } // if (this.geometry) { // // const oldGeom = this.geometry // this.geometry = undefined // this will dispose geometry if not used by other meshes // // delete this.geometry // // if (oldGeom && oldGeom.userData && oldGeom.appliedMeshes?.size === 0 && oldGeom.userData.disposeOnIdle !== false) oldGeom.dispose() // } // // delete this._onGeometryUpdate }); } if (!this.uiConfig && (this.assetType === 'model' || this.assetType === 'camera')) { // todo: lights/other types? iObjectCommons.makeUiConfig.call(this); } // todo: serialization? const children = [...this.children]; for (const c of children) upgradeObject3D.call(c, this); // region Legacy // eslint-disable-next-line deprecation/deprecation !this.userData.dispose && (this.userData.dispose = () => { console.warn('userData.dispose is deprecated, use dispose directly'); this.dispose && this.dispose(); }); // eslint-disable-next-line deprecation/deprecation !this.modelObject && Object.defineProperty(this, 'modelObject', { get: () => { console.error('modelObject is deprecated, use object directly'); return this; }, }); // eslint-disable-next-line deprecation/deprecation !this.userData.setDirty && (this.userData.setDirty = (e) => { console.error('object.userData.setDirty is deprecated, use object.setDirty directly'); this.setDirty?.(e); }); // endregion this.objectProcessor?.processObject(this); } //# sourceMappingURL=iObjectCommons.js.map