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 16KB

2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
2 yıl önce
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import {EventListener2, Object3D} from 'three'
  2. import {Class, onChange, serialize} from 'ts-browser-helpers'
  3. import {AViewerPluginEventMap, AViewerPluginSync, ThreeViewer} from '../../viewer'
  4. import {bindToValue, BoxSelectionWidget, ObjectPicker, SelectionWidget} from '../../three'
  5. import {IMaterial, IObject3D, IScene, ISceneEventMap} from '../../core'
  6. import {UiObjectConfig} from 'uiconfig.js'
  7. import {FrameFadePlugin} from '../pipeline/FrameFadePlugin'
  8. import {type UndoManagerPlugin} from './UndoManagerPlugin'
  9. import {ObjectPickerEventMap} from '../../three/utils/ObjectPicker'
  10. import {CameraViewPlugin} from '../animation/CameraViewPlugin'
  11. export interface PickingPluginEventMap extends AViewerPluginEventMap, ObjectPickerEventMap{
  12. }
  13. export class PickingPlugin extends AViewerPluginSync<PickingPluginEventMap> {
  14. @serialize()
  15. @onChange(PickingPlugin.prototype.setDirty)
  16. enabled = true
  17. get picker(): ObjectPicker|undefined {
  18. return this._picker
  19. }
  20. static readonly PluginType = 'Picking'
  21. static readonly OldPluginType = 'PickingPlugin' // todo: swap
  22. private _picker?: ObjectPicker
  23. private _widget?: SelectionWidget
  24. private _hoverWidget?: SelectionWidget
  25. private _pickUi: boolean
  26. dependencies = [CameraViewPlugin]
  27. get hoverEnabled() {
  28. return this._picker?.hoverEnabled ?? false
  29. }
  30. set hoverEnabled(v: boolean) {
  31. if (!this._picker) return
  32. this._picker.hoverEnabled = v
  33. this.uiConfig && this.uiConfig.uiRefresh?.()
  34. }
  35. @bindToValue({obj: '_picker', key: 'selectionMode'})
  36. selectionMode: 'object' | 'material' = 'object'
  37. @serialize()
  38. autoFocus
  39. // @serialize() // todo
  40. autoFocusHover = false
  41. /**
  42. * Note: this is for runtime use only, not serialized
  43. */
  44. @onChange(PickingPlugin.prototype._widgetEnabledChange)
  45. widgetEnabled = true
  46. protected _widgetEnabledChange() {
  47. if (!this._widget) return
  48. if (this.widgetEnabled && (this._picker?.selectedObject as IObject3D)?.isObject3D)
  49. this._widget.attach(this._picker!.selectedObject as IObject3D)
  50. else
  51. this._widget.detach()
  52. this.uiConfig?.uiRefresh?.(true)
  53. }
  54. setDirty() {
  55. if (!this._viewer) return
  56. if (this.isDisabled()) this.setSelectedObject(undefined)
  57. this._viewer.setDirty()
  58. }
  59. constructor(selection: Class<SelectionWidget>|undefined = BoxSelectionWidget, pickUi = true, autoFocus = false) {
  60. super()
  61. if (selection) {
  62. this._widget = new selection()
  63. this._hoverWidget = new selection()
  64. if (this._hoverWidget.lineMaterial) {
  65. this._hoverWidget.lineMaterial.linewidth! /= 2
  66. this._hoverWidget.lineMaterial.color!.set('#aa2222')
  67. }
  68. }
  69. this._pickUi = pickUi
  70. this.autoFocus = autoFocus
  71. this.dispatchEvent = this.dispatchEvent.bind(this)
  72. }
  73. getSelectedObject<T extends IObject3D|IMaterial = IObject3D|IMaterial>(): T|undefined {
  74. if (this.isDisabled()) return
  75. return this._picker?.selectedObject as T || undefined
  76. }
  77. setSelectedObject(object: IObject3D|IMaterial|undefined, focusCamera = false, trackUndo = true) { // todo: listen to object disposed
  78. const disabled = this.isDisabled()
  79. if (disabled && !object) return
  80. if (!this._picker) return
  81. const t = this.autoFocus
  82. this.autoFocus = false
  83. this._picker.setSelected(object || null, trackUndo)
  84. this.autoFocus = t
  85. if (!disabled && object && this.selectionMode === 'object' && (t || focusCamera)) this.focusObject(object as IObject3D)
  86. }
  87. onAdded(viewer: ThreeViewer): void {
  88. super.onAdded(viewer)
  89. this.setDirty()
  90. this._picker = new ObjectPicker(viewer.scene.modelRoot, viewer.canvas, viewer.scene.mainCamera, (obj)=>{
  91. const hasMat = obj.material
  92. if (!hasMat) return false
  93. let o: IObject3D|null = obj
  94. let ret = false
  95. while (o) {
  96. if (!o.visible) return false
  97. if (o.assetType === 'model' || o.assetType === 'light') ret = true
  98. if (o.assetType === 'widget') return false
  99. if (o.userData.userSelectable === false) return false
  100. if (o.userData.bboxVisible === false) return false
  101. o = o.parent
  102. }
  103. return ret
  104. })
  105. if (this._widget) viewer.scene.addObject(this._widget, {addToRoot: true})
  106. if (this._hoverWidget) viewer.scene.addObject(this._hoverWidget, {addToRoot: true})
  107. this._picker.addEventListener('selectedObjectChanged', this._selectedObjectChanged)
  108. this._picker.addEventListener('hoverObjectChanged', this._hoverObjectChanged)
  109. this._picker.addEventListener('hitObject', this._onObjectHit)
  110. this._picker.addEventListener('selectionModeChanged', this._selectionModeChanged)
  111. // on material drop on selected object
  112. // viewer.scene.addEventListener('addSceneObject', async(e) => {
  113. // const obj = e.object
  114. // const selected: IModel<Mesh> = this.getSelectedObject()! as any
  115. // if (selected
  116. // && obj?.assetType === 'material'
  117. // && typeof selected?.setMaterial === 'function'
  118. // && selected?.modelObject?.isMesh
  119. // && await viewer.confirm('Applying material: Apply material to the selected object?')
  120. // ) {
  121. // const oldMat = selected.material
  122. // if (Array.isArray(oldMat)) {
  123. // console.warn('Dropping on material array not yet fully supported.')
  124. // selected.setMaterial(obj)
  125. // } else {
  126. // let meshes: IModel<Mesh>[] = Array.from(oldMat?.userData.__appliedMeshes ?? [])
  127. // const c = meshes.length > 1 ? !await viewer.confirm('Applying material: Apply to all objects using this material?') : meshes.length < 1
  128. // if (c) meshes = [selected]
  129. // for (const mesh of meshes) {
  130. // if (mesh) mesh.setMaterial?.(obj)
  131. // }
  132. // }
  133. // }
  134. // })
  135. viewer.scene.addEventListener('select', this._onObjectSelectEvent)
  136. viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate)
  137. viewer.scene.addEventListener('mainCameraChange', this._mainCameraChange)
  138. viewer.forPlugin<UndoManagerPlugin>('UndoManagerPlugin', (um)=>{
  139. if (!this._picker) return
  140. this._picker.undoManager = um.undoManager
  141. }, ()=>{
  142. if (!this._picker) return
  143. this._picker.undoManager = undefined
  144. })
  145. }
  146. onRemove(viewer: ThreeViewer) {
  147. viewer.scene.removeEventListener('select', this._onObjectSelectEvent)
  148. viewer.scene.removeEventListener('sceneUpdate', this._onSceneUpdate)
  149. viewer.scene.removeEventListener('mainCameraChange', this._mainCameraChange)
  150. this._widget?.removeFromParent()
  151. this._hoverWidget?.removeFromParent()
  152. if (this._picker) {
  153. this._picker.removeEventListener('selectedObjectChanged', this._selectedObjectChanged)
  154. this._picker.removeEventListener('hoverObjectChanged', this._hoverObjectChanged)
  155. this._picker.removeEventListener('hitObject', this._onObjectHit)
  156. this._picker.removeEventListener('selectionModeChanged', this._selectionModeChanged)
  157. this._picker.dispose()
  158. this._picker.undoManager = undefined // because setting above
  159. this._picker = undefined
  160. }
  161. super.onRemove(viewer)
  162. }
  163. dispose() {
  164. super.dispose()
  165. this._widget?.dispose()
  166. this._hoverWidget?.dispose()
  167. }
  168. private _mainCameraChange = ()=>{
  169. if (!this._picker || !this._viewer) return
  170. this._picker.camera = this._viewer.scene.mainCamera
  171. }
  172. private _sceneUpdated = false
  173. private _onSceneUpdate: EventListener2<'sceneUpdate', ISceneEventMap, IScene> = (e)=>{
  174. if (!e.hierarchyChanged) return
  175. this._sceneUpdated = true
  176. }
  177. private _checkSelectedInScene() {
  178. if (this.isDisabled() || !this._viewer) return
  179. const s = this.getSelectedObject()
  180. if (!s || !(s as IObject3D).isObject3D) return // ignoring checking for materials in scene
  181. let inScene = false
  182. ;(s as IObject3D).traverseAncestors((o) => {
  183. if (inScene || o !== this._viewer!.scene) return
  184. inScene = true
  185. })
  186. if (!inScene) this.setSelectedObject(undefined, false, false)
  187. }
  188. protected _viewerListeners = {
  189. preFrame: ()=>{
  190. if (!this._viewer || !this._picker) return
  191. if (this._sceneUpdated) {
  192. this._checkSelectedInScene()
  193. this._sceneUpdated = false
  194. }
  195. },
  196. }
  197. private _onObjectSelectEvent: EventListener2<'select', ISceneEventMap, IScene> = (e)=>{
  198. if (e.source === PickingPlugin.PluginType) return
  199. if (e.object === undefined && e.value === undefined) console.error('PickingPlugin - Error handling object/material `select` event `e.object` or `e.value` must be set for picking, `value` can be null to unselect')
  200. else this.setSelectedObject(e.object || e.value, this.autoFocus || e.focusCamera, true)
  201. }
  202. private _selectedObjectChanged: EventListener2<'selectedObjectChanged', ObjectPickerEventMap, ObjectPicker> = (e: any) => {
  203. if (!this._viewer) return
  204. this.dispatchEvent(e)
  205. const selected = this._picker?.selectedObject || undefined // or use e.object. doing this so that listeners can change the selected object in dispatch above
  206. const frameFade = this._viewer.getPlugin(FrameFadePlugin)
  207. if (frameFade) {
  208. if (selected) frameFade.disable(this)
  209. else frameFade.enable(this)
  210. }
  211. this._viewer.scene.autoNearFarEnabled = !selected // for widgets etc, this can be removed when they are rendered in a separate pass
  212. if (this._pickUi) {
  213. const ui = this.uiConfig
  214. ui.children = [...this._uiConfigChildren]
  215. if (selected) {
  216. if ((selected as IObject3D).isObject3D) {
  217. const obj = (selected as IObject3D)
  218. ui.children.push(
  219. {
  220. type: 'button',
  221. label: 'Focus',
  222. value: () => {
  223. if (!obj.isObject3D) return
  224. // const selected = this.getSelectedObject()
  225. if (selected.assetType && obj.parentRoot) // todo also check if acceptChildEvents is set on some parent?
  226. obj.dispatchEvent({
  227. type: 'select',
  228. ui: true,
  229. object: obj,
  230. bubbleToParent: true,
  231. focusCamera: true,
  232. })
  233. else
  234. this.setSelectedObject(obj, true)
  235. },
  236. },
  237. {
  238. type: 'button',
  239. label: 'Select Parent',
  240. hidden: () => !obj.parent,
  241. value: () => {
  242. if (!obj.isObject3D) return
  243. const parent = obj.parent
  244. if (parent) {
  245. if (parent.assetType && parent.parentRoot) // todo also check if acceptChildEvents is set on some parent?
  246. parent.dispatchEvent({
  247. type: 'select',
  248. ui: true,
  249. bubbleToParent: true,
  250. object: parent,
  251. })
  252. else
  253. this.setSelectedObject(parent, false)
  254. }
  255. },
  256. },
  257. )
  258. }
  259. let c = selected.uiConfig
  260. if (c) ui.children.push(c)
  261. else {
  262. // check materials
  263. const mats = (selected as IObject3D).materials ?? [(selected as IObject3D).material as IMaterial]
  264. for (const m of mats) {
  265. c = m?.uiConfig
  266. if (c) ui.children.push(c)
  267. }
  268. }
  269. }
  270. ui.uiRefresh?.()
  271. }
  272. const widget = this._widget
  273. if (widget && this.widgetEnabled) {
  274. if ((selected as IObject3D)?.isObject3D) widget.attach((selected as IObject3D))
  275. else widget.detach()
  276. }
  277. // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected})
  278. this._viewer.setDirty()
  279. if (this.autoFocus && this.selectionMode === 'object') {
  280. // this._viewer.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)})
  281. this.focusObject(selected as IObject3D | undefined)
  282. }
  283. }
  284. private _hoverObjectChanged = (e: any) => {
  285. if (!this._viewer) return
  286. this.dispatchEvent(e)
  287. const selected = this._picker?.hoverObject || undefined
  288. const widget = this._hoverWidget
  289. if (widget && this.widgetEnabled) {
  290. if ((selected as IObject3D)?.isObject3D) widget.attach((selected as IObject3D))
  291. else widget.detach()
  292. }
  293. // if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected})
  294. this._viewer?.setDirty()
  295. if (this.autoFocusHover && this.selectionMode === 'object') {
  296. // this._viewer?.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)})
  297. this.focusObject(selected as IObject3D | undefined)
  298. }
  299. }
  300. private _onObjectHit = (e: any)=>{
  301. if (!this._viewer) return
  302. if (this.isDisabled()) {
  303. e.intersects.selectedObject = null
  304. return
  305. }
  306. this.dispatchEvent(e)
  307. }
  308. private _selectionModeChanged = (e: any)=>{
  309. if (!this._viewer) return
  310. this.dispatchEvent(e)
  311. if (this.isDisabled()) return
  312. this.uiConfig?.uiRefresh?.(true, 'postFrame', 1)
  313. }
  314. public async focusObject(selected?: Object3D|null) {
  315. this._viewer?.fitToView(selected ?? undefined, 1.25, 1000, 'easeOut')
  316. }
  317. private _uiConfigChildren: UiObjectConfig[] = [
  318. {
  319. label: 'Enabled',
  320. type: 'checkbox',
  321. property: [this, 'enabled'],
  322. },
  323. {
  324. label: 'Hover Enabled',
  325. type: 'checkbox',
  326. property: [this, 'hoverEnabled'],
  327. onChange: ()=>this.uiConfig.uiRefresh?.(true), // for autoFocusHover
  328. },
  329. // {
  330. // label: 'Selection Mode',
  331. // type: 'dropdown',
  332. // children: ['object', 'material'].map(v=>({label: v, value: v})),
  333. // onChange: ()=>this.uiConfig.uiRefresh?.(true),
  334. // },
  335. {
  336. label: 'Auto Focus',
  337. type: 'checkbox',
  338. property: [this, 'autoFocus'],
  339. onChange: ()=>{
  340. const o = this.getSelectedObject()
  341. if (this.autoFocus && o) this.setSelectedObject(o, true)
  342. },
  343. },
  344. {
  345. label: 'Auto Focus on Hover',
  346. type: 'checkbox',
  347. hidden: ()=>!this.hoverEnabled,
  348. property: [this, 'autoFocusHover'],
  349. },
  350. {
  351. label: 'Widget Enabled',
  352. type: 'checkbox',
  353. property: [this, 'widgetEnabled'],
  354. },
  355. ]
  356. uiConfig: UiObjectConfig = {
  357. type: 'panel',
  358. label: 'Picker',
  359. expanded: true,
  360. children: [
  361. ...this._uiConfigChildren,
  362. ],
  363. }
  364. get widget(): SelectionWidget | undefined {
  365. return this._widget
  366. }
  367. }