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.

PickingPlugin.ts 11KB

2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
2 lat temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import {Object3D} from 'three'
  2. import {Class, onChange, serialize} from 'ts-browser-helpers'
  3. import {AViewerPluginSync, ThreeViewer} from '../../viewer'
  4. import {BoxSelectionWidget, ObjectPicker, SelectionWidget} from '../../three'
  5. import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core'
  6. import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js'
  7. import {FrameFadePlugin} from '../pipeline/FrameFadePlugin'
  8. export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> {
  9. @serialize()
  10. @onChange(PickingPlugin.prototype.setDirty)
  11. enabled = true
  12. get picker(): ObjectPicker|undefined {
  13. return this._picker
  14. }
  15. static readonly PluginType = 'Picking'
  16. private _picker?: ObjectPicker
  17. private _widget?: SelectionWidget
  18. private _hoverWidget?: SelectionWidget
  19. private _pickUi: boolean
  20. get hoverEnabled() {
  21. return this._picker?.hoverEnabled ?? false
  22. }
  23. set hoverEnabled(v: boolean) {
  24. if (!this._picker) return
  25. this._picker.hoverEnabled = v
  26. this.uiConfig && this.uiConfig.uiRefresh?.()
  27. }
  28. @serialize()
  29. autoFocus
  30. // @serialize() // todo
  31. autoFocusHover = false
  32. /**
  33. * Note: this is for runtime use only, not serialized
  34. */
  35. @onChange(PickingPlugin.prototype._widgetEnabledChange)
  36. widgetEnabled = true
  37. protected _widgetEnabledChange() {
  38. if (this.widgetEnabled && this._picker?.selectedObject)
  39. this._widget?.attach(this._picker.selectedObject)
  40. else
  41. this._widget?.detach()
  42. this.uiConfig?.uiRefresh?.(true)
  43. }
  44. setDirty() {
  45. if (!this._viewer) return
  46. if (this.isDisabled()) this.setSelectedObject(undefined) // todo
  47. this._viewer.setDirty()
  48. }
  49. constructor(selection: Class<SelectionWidget>|undefined = BoxSelectionWidget, pickUi = true, autoFocus = false) {
  50. super()
  51. if (selection) {
  52. this._widget = new selection()
  53. this._hoverWidget = new selection()
  54. if (this._hoverWidget.lineMaterial) {
  55. this._hoverWidget.lineMaterial.linewidth! /= 2
  56. this._hoverWidget.lineMaterial.color!.set('#aa2222')
  57. }
  58. }
  59. this._pickUi = pickUi
  60. this.autoFocus = autoFocus
  61. this.dispatchEvent = this.dispatchEvent.bind(this)
  62. }
  63. getSelectedObject<T extends IObject3D = IObject3D>(): T|undefined {
  64. if (this.isDisabled()) return
  65. return this._picker?.selectedObject as T || undefined
  66. }
  67. setSelectedObject(object: IObject3D|undefined, focusCamera = false) { // todo: listen to object dispose
  68. if (this.isDisabled()) return
  69. if (!this._picker) return
  70. const t = this.autoFocus
  71. this.autoFocus = false
  72. this._picker.selectedObject = object || null
  73. this.autoFocus = t
  74. if (t || focusCamera) this.focusObject(object)
  75. }
  76. onAdded(viewer: ThreeViewer): void {
  77. super.onAdded(viewer)
  78. this.setDirty()
  79. this._picker = new ObjectPicker(viewer.scene.modelRoot, viewer.canvas, viewer.scene.mainCamera, (obj)=>{
  80. const hasMat = obj.material
  81. if (!hasMat) return false
  82. let o: IObject3D|null = obj
  83. let ret = false
  84. while (o) {
  85. if (!o.visible) return false
  86. if (o.assetType === 'model' || o.assetType === 'light') ret = true
  87. if (o.assetType === 'widget') return false
  88. if (o.userData.userSelectable === false) return false
  89. if (o.userData.bboxVisible === false) return false
  90. o = o.parent
  91. }
  92. return ret
  93. })
  94. if (this._widget) viewer.scene.addObject(this._widget, {addToRoot: true})
  95. if (this._hoverWidget) viewer.scene.addObject(this._hoverWidget, {addToRoot: true})
  96. this._picker.addEventListener('selectedObjectChanged', this._selectedObjectChanged)
  97. this._picker.addEventListener('hoverObjectChanged', this._hoverObjectChanged)
  98. this._picker.addEventListener('hitObject', this._onObjectHit)
  99. // on material drop on selected object
  100. // viewer.scene.addEventListener('addSceneObject', async(e) => {
  101. // const obj = e.object
  102. // const selected: IModel<Mesh> = this.getSelectedObject()! as any
  103. // if (selected
  104. // && obj?.assetType === 'material'
  105. // && typeof selected?.setMaterial === 'function'
  106. // && selected?.modelObject?.isMesh
  107. // && await viewer.confirm('Applying material: Apply material to the selected object?')
  108. // ) {
  109. // const oldMat = selected.material
  110. // if (Array.isArray(oldMat)) {
  111. // console.warn('Dropping on material array not yet fully supported.')
  112. // selected.setMaterial(obj)
  113. // } else {
  114. // let meshes: IModel<Mesh>[] = Array.from(oldMat?.userData.__appliedMeshes ?? [])
  115. // const c = meshes.length > 1 ? !await viewer.confirm('Applying material: Apply to all objects using this material?') : meshes.length < 1
  116. // if (c) meshes = [selected]
  117. // for (const mesh of meshes) {
  118. // if (mesh) mesh.setMaterial?.(obj)
  119. // }
  120. // }
  121. // }
  122. // })
  123. viewer.scene.addEventListener('select', this._onObjectSelectEvent)
  124. viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate)
  125. viewer.scene.addEventListener('mainCameraChange', this._mainCameraChange)
  126. }
  127. onRemove(viewer: ThreeViewer) {
  128. viewer.scene.removeEventListener('select', this._onObjectSelectEvent)
  129. viewer.scene.removeEventListener('sceneUpdate', this._onSceneUpdate)
  130. viewer.scene.removeEventListener('mainCameraChange', this._mainCameraChange)
  131. this._widget?.removeFromParent()
  132. this._hoverWidget?.removeFromParent()
  133. if (this._picker) {
  134. this._picker.removeEventListener('selectedObjectChanged', this._selectedObjectChanged)
  135. this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged)
  136. this._picker.removeEventListener('hitObject', this._onObjectHit)
  137. this._picker.dispose()
  138. this._picker = undefined
  139. }
  140. super.onRemove(viewer)
  141. }
  142. dispose() {
  143. super.dispose()
  144. this._widget?.dispose()
  145. this._hoverWidget?.dispose()
  146. }
  147. private _mainCameraChange = ()=>{
  148. if (!this._picker || !this._viewer) return
  149. this._picker.camera = this._viewer.scene.mainCamera
  150. }
  151. private _onSceneUpdate = (e: ISceneEvent)=>{
  152. if (!e.hierarchyChanged) return
  153. const s = this.getSelectedObject()
  154. let inScene = false
  155. s?.traverseAncestors((o)=>{
  156. if (o === this._viewer?.scene) inScene = true
  157. })
  158. if (!inScene) this.setSelectedObject(undefined)
  159. }
  160. private _onObjectSelectEvent = (e: IObject3DEvent)=>{
  161. if (e.source === PickingPlugin.PluginType) return
  162. if (e.object === undefined && e.value === undefined) console.error('e.object or e.value must be set for picking, can be null to unselect')
  163. else this.setSelectedObject(e.object || e.value, this.autoFocus || e.focusCamera)
  164. }
  165. private _selectedObjectChanged = (e: any) => {
  166. if (!this._viewer) return
  167. this.dispatchEvent(e)
  168. const selected = this._picker?.selectedObject || undefined // or use e.object. doing this so that listeners can change the selected object in dispatch above
  169. const frameFade = this._viewer.getPlugin(FrameFadePlugin)
  170. if (frameFade) {
  171. if (selected) frameFade.disable(this)
  172. else frameFade.enable(this)
  173. }
  174. this._viewer.scene.autoNearFarEnabled = !selected // for widgets etc, this can be removed when they are rendered in a separate pass
  175. if (this._pickUi) {
  176. const sUiConfig = (selected as IUiConfigContainer)?.uiConfig
  177. const ui = this.uiConfig
  178. ui.children = [...this._uiConfigChildren]
  179. if (sUiConfig) ui.children.push(sUiConfig)
  180. ui.uiRefresh?.()
  181. }
  182. const widget = this._widget
  183. if (widget && this.widgetEnabled) {
  184. if (selected) widget.attach(selected)
  185. else widget.detach()
  186. }
  187. // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected})
  188. this._viewer.setDirty()
  189. if (this.autoFocus) {
  190. // this._viewer.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)})
  191. this.focusObject(selected)
  192. }
  193. }
  194. private _hoverObjectChanged = (e: any) => {
  195. this.dispatchEvent(e)
  196. const selected = this._picker?.hoverObject || undefined
  197. const widget = this._hoverWidget
  198. if (widget && this.widgetEnabled) {
  199. if (selected) widget.attach(selected)
  200. else widget.detach()
  201. }
  202. // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected})
  203. this._viewer?.setDirty()
  204. if (this.autoFocusHover) {
  205. // this._viewer?.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)})
  206. this.focusObject(selected)
  207. }
  208. }
  209. private _onObjectHit = (e: any)=>{
  210. if (!this._viewer) return
  211. if (this.isDisabled()) {
  212. e.intersects.selectedObject = null
  213. return
  214. }
  215. this.dispatchEvent(e)
  216. }
  217. public async focusObject(selected?: Object3D) {
  218. this._viewer?.fitToView(selected, 1.25, 1000, 'easeOut')
  219. }
  220. private _uiConfigChildren: UiObjectConfig[] = [
  221. {
  222. label: 'Enabled',
  223. type: 'checkbox',
  224. property: [this, 'enabled'],
  225. },
  226. {
  227. label: 'Hover Enabled',
  228. type: 'checkbox',
  229. property: [this, 'hoverEnabled'],
  230. onChange: ()=>this.uiConfig.uiRefresh?.(true), // for autoFocusHover
  231. },
  232. {
  233. label: 'Auto Focus',
  234. type: 'checkbox',
  235. property: [this, 'autoFocus'],
  236. onChange: ()=>{
  237. const o = this.getSelectedObject()
  238. if (this.autoFocus && o) this.setSelectedObject(o, true)
  239. },
  240. },
  241. {
  242. label: 'Auto Focus on Hover',
  243. type: 'checkbox',
  244. hidden: ()=>!this.hoverEnabled,
  245. property: [this, 'autoFocusHover'],
  246. },
  247. {
  248. label: 'Widget Enabled',
  249. type: 'checkbox',
  250. property: [this, 'widgetEnabled'],
  251. },
  252. ]
  253. uiConfig: UiObjectConfig = {
  254. type: 'panel',
  255. label: 'Picker',
  256. expanded: true,
  257. children: [
  258. ...this._uiConfigChildren,
  259. ],
  260. }
  261. get widget(): SelectionWidget | undefined {
  262. return this._widget
  263. }
  264. }