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

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