| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- import {Event, Matrix4, Mesh, Vector3} from 'three'
- import {IMaterial} from '../IMaterial'
- import {objectHasOwn} from 'ts-browser-helpers'
- import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject'
- import {copyObject3DUserData} from '../../utils'
- import {IGeometry, IGeometryEvent} from '../IGeometry'
- import {Box3B} from '../../three'
- import {makeIObject3DUiConfig} from './IObjectUi'
- import {iGeometryCommons} from '../geometry/iGeometryCommons'
- import {iMaterialCommons} from '../material/iMaterialCommons'
- import {ILight} from '../light/ILight'
-
- export const iObjectCommons = {
- setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions, ...args: any[]): void {
- if (typeof options === 'string') { // just incase called by decorators
- options = {change: options}
- }
- this.dispatchEvent({bubbleToParent: true, ...options, type: 'objectUpdate', object: this, args}) // 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<T extends IObject3D>(this: T, setDirty = true, undo = false): T {
- 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<T extends IObject3D>(this: T, autoScaleRadius?: number, isCentered?: boolean, setDirty = true, undo = false): T {
- 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 as any
- 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<T extends IObject3D>(this: T, setDirty = true): ()=>void {
- const bb = new Box3B().expandByObject(this, true, true)
- const center = bb.getCenter(new Vector3())
- return iObjectCommons.pivotToPoint.call(this, center, setDirty)
- },
-
- pivotToPoint: function<T extends IObject3D>(this: T, point: Vector3, setDirty = true): ()=>void {
- 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(this: IObject3D, e: Event): void {
- // 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(this: IObject3D, e: Event): void {
- // removed from some parent
- this.setDirty?.({...e, change: 'removedFromParent'})
- if (this.parentRoot !== undefined) {
- this.traverse(o=>{
- o.parentRoot = undefined
- })
- }
- },
- onGeometryUpdate: function(this: IObject3D, e: IGeometryEvent<'geometryUpdate'>): void {
- if (!e.bubbleToObject) return
- this.dispatchEvent({bubbleToParent: true, ...e, object: this, geometry: e.geometry})
- },
- },
-
- initMaterial: function(this: IObject3D): void {
- 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 as any).setMaterial) {
- (this as any).setMaterial = (m: IMaterial | IMaterial[]| undefined)=>{
- 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: any)=>{
- console.error('userData.setMaterial is deprecated, use setMaterial directly')
- this.material = m
- }
-
- },
-
- getMaterial: function(this: IObject3D): IMaterial | IMaterial[] | undefined {
- return this._currentMaterial || undefined
- },
- getMaterials: function(this: IObject3D): IMaterial[] {
- return !this._currentMaterial ? [] : Array.isArray(this._currentMaterial) ? [...this._currentMaterial] : [this._currentMaterial]
- },
-
- setMaterial: function(this: IObject3D, material: IMaterial | IMaterial[] | undefined) {
- 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 as IMaterial[])] : [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(this: IObject3D, materials: IMaterial[]) {
- this.material = materials || undefined
- },
-
- initGeometry: function(this: IObject3D): void {
- 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 as any).setGeometry) {
- (this as any).setGeometry = (geometry: IGeometry) =>{
- 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: any)=>{
- console.error('userData.setGeometry is deprecated, use setGeometry directly')
- this.geometry = g
- }
-
- },
- getGeometry: function(this: IObject3D&Mesh): IGeometry | undefined {
- return this._currentGeometry || undefined
- },
- setGeometry: function(this: IObject3D&Mesh, geometry: IGeometry | undefined): void {
- 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: IObject3D): void {
- this.uiConfig?.uiRefresh?.(true, 'postFrame', 1)
- },
-
- dispatchEvent: (superDispatch: IObject3D['dispatchEvent']) =>
- function(this: IObject3D, event: IObject3DEvent): void {
- 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: IObject3D['clone']): IObject3D['clone'] =>
- function(this: IObject3D, ...args): IObject3D {
- const userData = this.userData
- this.userData = {}
- const clone: any = 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: IObject3D['copy']): IObject3D['copy'] =>
- function(this: IObject3D|ILight, source: IObject3D, ...args): IObject3D {
- const lightTarget = this.isLight ? (this as ILight).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 as ILight).target) { // For eg DirectionalLight2
- lightTarget.position.copy((this as ILight).target!.position)
- lightTarget.updateMatrixWorld()
- ;(this as ILight).target = lightTarget // because t is a child and because of UI.
- }
-
- return this
- },
- add: (superAdd: IObject3D['add']): IObject3D['add'] =>
- function(this: IObject3D, ...args): IObject3D {
- for (const a of args) iObjectCommons.upgradeObject3D.call(a, this.parentRoot || this, this.objectProcessor)
- return superAdd.call(this, ...args)
- },
- dispose: (superDispose?: IObject3D['dispose']) =>
- function(this: IObject3D, removeFromParent = true): void {
- 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(this: IObject3D, parent?: IObject3D|undefined, objectProcessor?: IObjectProcessor): void {
- 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: IGeometryEvent) => 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 || [<IMaterial> 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: any)=>{
- console.error('object.userData.setDirty is deprecated, use object.setDirty directly')
- this.setDirty?.(e)
- })
-
- // endregion
-
- this.objectProcessor?.processObject(this)
-
- }
|