threepipe
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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