threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

iObjectCommons.ts 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. import {Event, Matrix4, Mesh, Vector3} from 'three'
  2. import {IMaterial} from '../IMaterial'
  3. import {objectHasOwn} from 'ts-browser-helpers'
  4. import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject'
  5. import {copyObject3DUserData} from '../../utils'
  6. import {IGeometry, IGeometryEvent} from '../IGeometry'
  7. import {Box3B} from '../../three'
  8. import {makeIObject3DUiConfig} from './IObjectUi'
  9. import {iGeometryCommons} from '../geometry/iGeometryCommons'
  10. import {iMaterialCommons} from '../material/iMaterialCommons'
  11. import {ILight} from '../light/ILight'
  12. export const iObjectCommons = {
  13. setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions, ...args: any[]): void {
  14. if (typeof options === 'string') { // just incase called by decorators
  15. options = {change: options}
  16. }
  17. this.dispatchEvent({bubbleToParent: true, ...options, type: 'objectUpdate', object: this, args}) // this sets sceneUpdate in root scene
  18. if (options?.refreshUi !== false && options?.last !== false) this.refreshUi?.()
  19. // console.log('object update')
  20. },
  21. upgradeObject3D: upgradeObject3D,
  22. makeUiConfig: makeIObject3DUiConfig,
  23. autoCenter: function<T extends IObject3D>(this: T, setDirty = true, undo = false): T {
  24. if (undo) {
  25. if (!this.userData.autoCentered || !this.userData._lastCenter) return this
  26. this.position.add(this.userData._lastCenter)
  27. delete this.userData.autoCentered
  28. delete this.userData.isCentered
  29. delete this.userData._lastCenter
  30. } else {
  31. const bb = new Box3B().expandByObject(this, true, true)
  32. const center = bb.getCenter(new Vector3())
  33. this.userData._lastCenter = center/* .clone()*/
  34. this.position.sub(center)
  35. this.userData.autoCentered = true
  36. this.userData.isCentered = true
  37. }
  38. this.updateMatrix()
  39. if (setDirty) this.setDirty({change: 'autoCenter', undo})
  40. return this
  41. },
  42. autoScale: function<T extends IObject3D>(this: T, autoScaleRadius?: number, isCentered?: boolean, setDirty = true, undo = false): T {
  43. let scale = 1
  44. if (undo) { // Note - undo only works for quick undo, not for multiple times
  45. if (!this.userData.autoScaled || !this.userData._lastScaleRadius) return this
  46. const rad = this.userData.autoScaleRadius || autoScaleRadius || 1
  47. scale = this.userData._lastScaleRadius / rad
  48. if (!isFinite(scale)) return this // NaN when radius is 0
  49. this.userData.autoScaled = true
  50. this.userData.autoScaleRadius = autoScaleRadius
  51. delete this.userData._lastScaleRadius
  52. } else {
  53. const bbox = new Box3B().expandByObject(this, true, true)
  54. const radius = bbox.getSize(new Vector3()).length() * 0.5
  55. if (autoScaleRadius === undefined) {
  56. autoScaleRadius = this.userData.autoScaleRadius || 1
  57. }
  58. scale = autoScaleRadius / radius
  59. if (!isFinite(scale)) return this // NaN when radius is 0
  60. this.userData.autoScaled = true
  61. this.userData.autoScaleRadius = autoScaleRadius
  62. this.userData._lastScaleRadius = radius
  63. }
  64. if (this.userData.pseudoCentered) {
  65. this.children.forEach(child => {
  66. child.scale.multiplyScalar(scale)
  67. })
  68. } else
  69. this.scale.multiplyScalar(scale)
  70. if (isCentered || this.userData.isCentered) this.position.multiplyScalar(scale)
  71. this.traverse((obj) => {
  72. const l = obj as any
  73. if (l.isLight && l.shadow?.camera?.right) {
  74. l.shadow.camera.right *= scale
  75. l.shadow.camera.left *= scale
  76. l.shadow.camera.top *= scale
  77. l.shadow.camera.bottom *= scale
  78. obj.setDirty()
  79. }
  80. if (l.isCamera && l.right) {
  81. l.right *= scale
  82. l.left *= scale
  83. l.top *= scale
  84. l.bottom *= scale
  85. obj.setDirty()
  86. }
  87. })
  88. if (setDirty) this.setDirty({change: 'autoScale', undo})
  89. return this
  90. },
  91. pivotToBoundsCenter: function<T extends IObject3D>(this: T, setDirty = true): ()=>void {
  92. const bb = new Box3B().expandByObject(this, true, true)
  93. const center = bb.getCenter(new Vector3())
  94. return iObjectCommons.pivotToPoint.call(this, center, setDirty)
  95. },
  96. pivotToPoint: function<T extends IObject3D>(this: T, point: Vector3, setDirty = true): ()=>void {
  97. const worldCenter = new Vector3().copy(point)
  98. const localCenter = new Vector3().copy(worldCenter)
  99. const worldMatrixInv = new Matrix4().copy(this.matrixWorld).invert()
  100. const m = this.parent?.matrixWorld
  101. const parentWorldMatrixInv = new Matrix4()
  102. if (m !== undefined)
  103. parentWorldMatrixInv.copy(m).invert()
  104. // Get the center with respect to the parent
  105. worldCenter.applyMatrix4(parentWorldMatrixInv)
  106. const lastPosition = this.position.clone()
  107. // Apply the new position
  108. this.position.copy(worldCenter)
  109. // local center
  110. localCenter.applyMatrix4(worldMatrixInv).negate()
  111. // Shift the geometry
  112. if (this.geometry) {
  113. this.geometry.translate(localCenter.x, localCenter.y, localCenter.z)
  114. }
  115. // Add offsets
  116. this.children.forEach((object)=> {
  117. object.position.add(localCenter)
  118. })
  119. if (setDirty) this.setDirty({change: 'pivotToPoint', undo: false})
  120. return ()=>{
  121. // undo
  122. this.position.copy(lastPosition)
  123. if (this.geometry) {
  124. this.geometry.translate(-localCenter.x, -localCenter.y, -localCenter.z)
  125. }
  126. this.children.forEach((object)=> {
  127. object.position.sub(localCenter)
  128. })
  129. if (setDirty) this.setDirty({change: 'pivotToPoint', undo: true})
  130. }
  131. },
  132. eventCallbacks: {
  133. onAddedToParent: function(this: IObject3D, e: Event): void {
  134. // added to some parent
  135. const root = this.parent?.parentRoot ?? this.parent
  136. 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
  137. this.traverse(o=>{
  138. o.objectProcessor = root.objectProcessor
  139. o.objectProcessor?.processObject(o)
  140. })
  141. }
  142. if (root !== this.parentRoot) {
  143. this.traverse(o=>{
  144. o.parentRoot = root
  145. })
  146. }
  147. this.setDirty?.({...e, change: 'addedToParent'})
  148. },
  149. onRemovedFromParent: function(this: IObject3D, e: Event): void {
  150. // removed from some parent
  151. this.setDirty?.({...e, change: 'removedFromParent'})
  152. if (this.parentRoot !== undefined) {
  153. this.traverse(o=>{
  154. o.parentRoot = undefined
  155. })
  156. }
  157. },
  158. onGeometryUpdate: function(this: IObject3D, e: IGeometryEvent<'geometryUpdate'>): void {
  159. if (!e.bubbleToObject) return
  160. this.dispatchEvent({bubbleToParent: true, ...e, object: this, geometry: e.geometry})
  161. },
  162. },
  163. initMaterial: function(this: IObject3D): void {
  164. if (objectHasOwn(this, '_currentMaterial')) return
  165. this._currentMaterial = null
  166. const currentMaterial = this.material
  167. delete this.material
  168. Object.defineProperty(this, 'material', {
  169. get: iObjectCommons.getMaterial,
  170. set: iObjectCommons.setMaterial,
  171. })
  172. Object.defineProperty(this, 'materials', {
  173. get: iObjectCommons.getMaterials,
  174. set: iObjectCommons.setMaterials,
  175. })
  176. // this is called initially in Material manager from process model below, not required here...
  177. // todo: shouldnt be called from there. maybe check if material is upgraded before
  178. // if (currentMaterial && !Array.isArray(currentMaterial) && !currentMaterial.assetType) {
  179. // console.error('todo: initMaterial: material not upgraded')
  180. // }
  181. this.material = currentMaterial
  182. // Legacy
  183. if (!(this as any).setMaterial) {
  184. (this as any).setMaterial = (m: IMaterial | IMaterial[]| undefined)=>{
  185. const mats = this.material
  186. console.error('setMaterial is deprecated, use material property directly')
  187. this.material = m
  188. return mats
  189. }
  190. }
  191. // Legacy
  192. if (this.userData.setMaterial) console.error('userData.setMaterial already defined')
  193. this.userData.setMaterial = (m: any)=>{
  194. console.error('userData.setMaterial is deprecated, use setMaterial directly')
  195. this.material = m
  196. }
  197. },
  198. getMaterial: function(this: IObject3D): IMaterial | IMaterial[] | undefined {
  199. return this._currentMaterial || undefined
  200. },
  201. getMaterials: function(this: IObject3D): IMaterial[] {
  202. return !this._currentMaterial ? [] : Array.isArray(this._currentMaterial) ? [...this._currentMaterial] : [this._currentMaterial]
  203. },
  204. setMaterial: function(this: IObject3D, material: IMaterial | IMaterial[] | undefined) {
  205. const imats = (Array.isArray(material) ? material : [material]).filter(v=>v)
  206. if (this.material == imats || imats.length === 1 && this.material === imats[0]) return []
  207. // todo: check by uuid?
  208. // Remove old material listeners
  209. const mats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!]
  210. for (const mat of mats) {
  211. if (!mat) continue
  212. if (mat.appliedMeshes) {
  213. mat.appliedMeshes.delete(this)
  214. // if (mat.userData && mat.appliedMeshes?.size === 0 && mat.userData.disposeOnIdle !== false)
  215. mat.dispose(false) // this will dispose textures(if they are idle) if the material is registered in the material manager
  216. }
  217. }
  218. const materials = []
  219. for (const mat of imats) {
  220. // const mat = material?.materialObject
  221. if (!mat) continue
  222. if (!mat.assetType) {
  223. console.warn('Upgrading Material', mat)
  224. iMaterialCommons.upgradeMaterial.call(mat)
  225. }
  226. materials.push(mat)
  227. if (mat) {
  228. mat.appliedMeshes.add(this)
  229. }
  230. }
  231. this._currentMaterial = !materials.length ? null : materials.length !== 1 ? materials : materials[0] || null
  232. this.dispatchEvent({type: 'materialChanged', material, oldMaterial: mats, object: this, bubbleToParent: true})
  233. this.refreshUi()
  234. },
  235. setMaterials: function(this: IObject3D, materials: IMaterial[]) {
  236. this.material = materials || undefined
  237. },
  238. initGeometry: function(this: IObject3D): void {
  239. const currentGeometry = this.geometry
  240. this._currentGeometry = null
  241. delete this.geometry
  242. Object.defineProperty(this, 'geometry', {
  243. get: iObjectCommons.getGeometry,
  244. set: iObjectCommons.setGeometry,
  245. })
  246. this.geometry = currentGeometry
  247. // Legacy
  248. if (!(this as any).setGeometry) {
  249. (this as any).setGeometry = (geometry: IGeometry) =>{
  250. const geom = this.geometry
  251. console.error('setGeometry is deprecated, use geometry property directly')
  252. this.geometry = geometry
  253. return geom
  254. }
  255. }
  256. // Legacy
  257. if (this.userData.setGeometry) console.error('userData.setGeometry already defined')
  258. this.userData.setGeometry = (g: any)=>{
  259. console.error('userData.setGeometry is deprecated, use setGeometry directly')
  260. this.geometry = g
  261. }
  262. },
  263. getGeometry: function(this: IObject3D&Mesh): IGeometry | undefined {
  264. return this._currentGeometry || undefined
  265. },
  266. setGeometry: function(this: IObject3D&Mesh, geometry: IGeometry | undefined): void {
  267. const geom = this.geometry || undefined
  268. // todo: check by uuid?
  269. if (geom === geometry) return
  270. if (geom) {
  271. this._onGeometryUpdate && geom.removeEventListener('geometryUpdate', this._onGeometryUpdate)
  272. if (geom.appliedMeshes) {
  273. geom.appliedMeshes.delete(this)
  274. geom.dispose(false)
  275. }
  276. }
  277. if (geometry) {
  278. if (!geometry.assetType) {
  279. // console.error('Geometry not upgraded')
  280. iGeometryCommons.upgradeGeometry.call(geometry)
  281. }
  282. }
  283. this._currentGeometry = geometry || null
  284. if (geometry) {
  285. this.updateMorphTargets()
  286. this._onGeometryUpdate && geometry.addEventListener('geometryUpdate', this._onGeometryUpdate)
  287. geometry.appliedMeshes.add(this)
  288. }
  289. this.dispatchEvent({type: 'geometryChanged', geometry, oldGeometry: geom, bubbleToParent: true})
  290. this.refreshUi()
  291. },
  292. refreshUi: function(this: IObject3D): void {
  293. this.uiConfig?.uiRefresh?.(true, 'postFrame', 1)
  294. },
  295. dispatchEvent: (superDispatch: IObject3D['dispatchEvent']) =>
  296. function(this: IObject3D, event: IObject3DEvent): void {
  297. if (event.bubbleToParent || this.userData?.__autoBubbleToParentEvents?.includes(event.type)) {
  298. // console.log('parent dispatch', e, this.parentRoot, this.parent)
  299. const pRoot = this.parentRoot || this.parent
  300. if (this.parentRoot !== this) pRoot?.dispatchEvent(event)
  301. }
  302. superDispatch.call(this, event)
  303. },
  304. clone: (superClone: IObject3D['clone']): IObject3D['clone'] =>
  305. function(this: IObject3D, ...args): IObject3D {
  306. const userData = this.userData
  307. this.userData = {}
  308. const clone: any = superClone.call(this, ...args)
  309. this.userData = userData
  310. copyObject3DUserData(clone.userData, userData) // todo: do same for this.toJSON()
  311. const objParent = this.parentRoot || undefined
  312. if (objParent && objParent.assetType !== 'model') {
  313. console.warn('Cloning an IObject with a parent that is not an \'model\' is not supported')
  314. }
  315. iObjectCommons.upgradeObject3D.call(clone, objParent, this.objectProcessor)
  316. clone.userData.cloneParent = this.uuid
  317. return clone
  318. },
  319. copy: (superCopy: IObject3D['copy']): IObject3D['copy'] =>
  320. function(this: IObject3D|ILight, source: IObject3D, ...args): IObject3D {
  321. const lightTarget = this.isLight ? (this as ILight).target : null
  322. const userData = source.userData
  323. source.userData = {}
  324. const selfUserData = this.userData
  325. superCopy.call(this, source, ...args)
  326. this.userData = selfUserData
  327. source.userData = userData
  328. copyObject3DUserData(this.userData, source.userData) // todo: do same for object.toJSON()
  329. if (lightTarget && (this as ILight).target) { // For eg DirectionalLight2
  330. lightTarget.position.copy((this as ILight).target!.position)
  331. lightTarget.updateMatrixWorld()
  332. ;(this as ILight).target = lightTarget // because t is a child and because of UI.
  333. }
  334. return this
  335. },
  336. add: (superAdd: IObject3D['add']): IObject3D['add'] =>
  337. function(this: IObject3D, ...args): IObject3D {
  338. for (const a of args) iObjectCommons.upgradeObject3D.call(a, this.parentRoot || this, this.objectProcessor)
  339. return superAdd.call(this, ...args)
  340. },
  341. dispose: (superDispose?: IObject3D['dispose']) =>
  342. function(this: IObject3D, removeFromParent = true): void {
  343. if (removeFromParent && this.parent) {
  344. this.removeFromParent()
  345. delete this.parentRoot
  346. }
  347. this.dispatchEvent({type: 'dispose', bubbleToParent: false})
  348. // if (this.__disposed) {
  349. // console.warn('Object already disposed', this)
  350. // return
  351. // }
  352. // this.__disposed = true
  353. for (const c of [...this.children]) c?.dispose && c.dispose(false) // not removing the children from parent to preserve hierarchy
  354. // this.children = []
  355. // this.uiConfig?.dispose?.() // todo: make uiConfig.dispose
  356. superDispose?.call(this)
  357. },
  358. }
  359. /**
  360. * Converts three.js Object3D to IObject3D, setup object events, adds utility methods, and runs objectProcessor.
  361. * @param parent
  362. * @param objectProcessor
  363. */
  364. function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectProcessor?: IObjectProcessor): void {
  365. if (!this) return
  366. // console.log('upgradeObject3D', this, parent, objectProcessor)
  367. // if (this.__disposed) {
  368. // console.warn('re-init/re-add disposed object, things might not work as intended', this)
  369. // delete this.__disposed
  370. // }
  371. if (!this.userData) this.userData = {}
  372. this.userData.uuid = this.uuid
  373. // not checking assetType but custom var __objectSetup because its required in types sometimes, check PerspectiveCamera2
  374. // if (this.assetType) return
  375. if (this.userData.__objectSetup) return
  376. this.userData.__objectSetup = true
  377. if (!this.objectProcessor) this.objectProcessor = objectProcessor || this.parent?.objectProcessor || parent?.objectProcessor
  378. if (!this.userData.__autoBubbleToParentEvents) this.userData.__autoBubbleToParentEvents = ['select']
  379. // Event bubbling. todo: set bubbleToParent in these events when dispatched from child and remove from here?
  380. if (this.isLight) this.assetType = 'light'
  381. else if (this.isCamera) this.assetType = 'camera'
  382. else if (this.isWidget) this.assetType = 'widget'
  383. else this.assetType = 'model'
  384. if (parent) this.parentRoot = parent
  385. // const oldFunctions = {
  386. // dispatchEvent: this.dispatchEvent,
  387. // clone: this.clone,
  388. // copy: this.copy,
  389. // add: this.add,
  390. // dispose: this.dispose,
  391. // }
  392. // this.addEventListener('dispose', () => Object.assign(this, oldFunctions)) // todo: is this required?
  393. // typed because of type-checking
  394. this.dispatchEvent = iObjectCommons.dispatchEvent(this.dispatchEvent)
  395. this.dispose = iObjectCommons.dispose(this.dispose)
  396. this.clone = iObjectCommons.clone(this.clone)
  397. this.copy = iObjectCommons.copy(this.copy) // todo: do same for object.toJSON()
  398. this.add = iObjectCommons.add(this.add)
  399. if (!this.setDirty) this.setDirty = iObjectCommons.setDirty
  400. if (!this.refreshUi) this.refreshUi = iObjectCommons.refreshUi
  401. if (!this.autoScale) this.autoScale = iObjectCommons.autoScale.bind(this)
  402. if (!this.autoCenter) this.autoCenter = iObjectCommons.autoCenter.bind(this)
  403. if (!this.pivotToBoundsCenter) this.pivotToBoundsCenter = iObjectCommons.pivotToBoundsCenter.bind(this)
  404. if (!this.pivotToPoint) this.pivotToPoint = iObjectCommons.pivotToPoint.bind(this)
  405. // fired from Object3D.js
  406. this.addEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent)
  407. this.addEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent)
  408. // this.addEventListener('dispose', ()=>{
  409. // this.removeEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent)
  410. // this.removeEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent)
  411. // })
  412. if ((this.isMesh || this.isLine) && !this.userData.__meshSetup) {
  413. this.userData.__meshSetup = true
  414. this._onGeometryUpdate = (e: IGeometryEvent) => iObjectCommons.eventCallbacks.onGeometryUpdate.call(this, e)
  415. // Material, Geometry prop init
  416. iObjectCommons.initMaterial.call(this)
  417. iObjectCommons.initGeometry.call(this)
  418. // from GLTFObject3DExtrasExtension
  419. if (!this.userData.__keepShadowDef) {
  420. this.castShadow = true
  421. this.receiveShadow = true
  422. this.userData.__keepShadowDef = true
  423. }
  424. this.addEventListener('dispose', ()=>{
  425. (this.materials || [<IMaterial> this.material]).forEach(m => m?.dispose(false))
  426. this.geometry?.dispose(false)
  427. // if (this.material) {
  428. // // const oldMats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!]
  429. // this.material = undefined // this will dispose material if not used by other meshes
  430. // // delete this.material
  431. // // for (const oldMat of oldMats) {
  432. // // if (oldMat && oldMat.userData && oldMat.appliedMeshes?.size === 0 && oldMat.userData.disposeOnIdle !== false) oldMat.dispose()
  433. // // }
  434. // }
  435. // if (this.geometry) {
  436. // // const oldGeom = this.geometry
  437. // this.geometry = undefined // this will dispose geometry if not used by other meshes
  438. // // delete this.geometry
  439. // // if (oldGeom && oldGeom.userData && oldGeom.appliedMeshes?.size === 0 && oldGeom.userData.disposeOnIdle !== false) oldGeom.dispose()
  440. // }
  441. //
  442. // delete this._onGeometryUpdate
  443. })
  444. }
  445. if (!this.uiConfig && (this.assetType === 'model' || this.assetType === 'camera')) {
  446. // todo: lights/other types?
  447. iObjectCommons.makeUiConfig.call(this)
  448. }
  449. // todo: serialization?
  450. const children = [...this.children]
  451. for (const c of children) upgradeObject3D.call(c, this)
  452. // region Legacy
  453. // eslint-disable-next-line deprecation/deprecation
  454. !this.userData.dispose && (this.userData.dispose = () => {
  455. console.warn('userData.dispose is deprecated, use dispose directly')
  456. this.dispose && this.dispose()
  457. })
  458. // eslint-disable-next-line deprecation/deprecation
  459. !this.modelObject && Object.defineProperty(this, 'modelObject', {
  460. get: ()=>{
  461. console.error('modelObject is deprecated, use object directly')
  462. return this
  463. },
  464. })
  465. // eslint-disable-next-line deprecation/deprecation
  466. !this.userData.setDirty && (this.userData.setDirty = (e: any)=>{
  467. console.error('object.userData.setDirty is deprecated, use object.setDirty directly')
  468. this.setDirty?.(e)
  469. })
  470. // endregion
  471. this.objectProcessor?.processObject(this)
  472. }