threepipe
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

iObjectCommons.ts 19KB

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