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