threepipe
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

HierarchyUiPlugin.ts 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import {createDiv, createStyles, css} from 'ts-browser-helpers'
  2. import Tree from 'treejs'
  3. import {
  4. AViewerPluginSync,
  5. IObject3D,
  6. ISceneEventMap,
  7. Object3D,
  8. PickingPlugin,
  9. ThreeViewer,
  10. type UndoManagerPlugin, Event2,
  11. } from 'threepipe'
  12. import {UiObjectConfig} from 'uiconfig.js'
  13. export class HierarchyUiPlugin extends AViewerPluginSync {
  14. enabled = true
  15. public static readonly PluginType = 'HierarchyUiPlugin'
  16. toJSON: any = undefined
  17. treeView?: Tree = undefined
  18. hierarchyDiv = createDiv({
  19. innerHTML: '',
  20. id: 'tpHierarchyContainer',
  21. addToBody: false,
  22. })
  23. constructor(enabled = true) {
  24. super()
  25. this.enabled = enabled
  26. this.reset = this.reset.bind(this)
  27. this._postFrame = this._postFrame.bind(this)
  28. this.uiConfig.domChildren = [this.hierarchyDiv]
  29. createStyles(css`
  30. #tpHierarchyContainer{
  31. width: 100%;
  32. height: auto;
  33. background-color: transparent;
  34. color: var(--tp-container-foreground-color, hsl(230, 7%, 75%));
  35. margin-top: 0;
  36. }
  37. .treejs .treejs-switcher:before {
  38. border-top: 6px solid var(--tp-container-foreground-color, hsl(230, 7%, 75%)) !important;
  39. }
  40. .treejs .treejs-switcher,.treejs-label,.treejs-checkbox {
  41. pointer-events: auto;
  42. }
  43. .treejs .treejs-node {
  44. pointer-events: none;
  45. position: relative;
  46. }
  47. .treejs .treejs-label {
  48. position: absolute;
  49. height: 16px;
  50. line-height: 16px;
  51. text-overflow: ellipsis;
  52. overflow: hidden;
  53. width: calc(100% - 50px);
  54. padding: 1px 4px;
  55. margin: 1px 0px;
  56. border-radius: 2px;
  57. box-sizing: content-box;
  58. }
  59. .treejs .treejs-node-selected .treejs-label {
  60. outline-offset: -1px;
  61. outline: solid 1px #1890ff;
  62. }
  63. .treejs .treejs-node-selected > .treejs-label {
  64. background-color: #067ce9;
  65. color: #eee;
  66. outline: none;
  67. }
  68. `)
  69. }
  70. reset(e?: Event2<'sceneUpdate', ISceneEventMap, IObject3D>) {
  71. if (e?.source === HierarchyUiPlugin.PluginType) return // for infinite loop
  72. // visible changed from outside
  73. if (e && e.object && e.change === 'visible' && this.treeView) {
  74. const nodeId = e.object.uuid
  75. // @ts-expect-error no type
  76. const node = this.treeView.nodesById[nodeId] as TNode
  77. if (node && !!node.status !== e.object.visible) {
  78. this.treeView.setValue(nodeId)
  79. this.treeView.updateLiElements()
  80. }
  81. // this.treeView.values = obj.children.reduce(this._findVisible, [])
  82. // console.log(this.treeView.values)
  83. }
  84. if (!e?.hierarchyChanged) return
  85. this._needsReset = true
  86. }
  87. protected async _reset() {
  88. this._needsReset = false
  89. while (this.hierarchyDiv.firstChild) this.hierarchyDiv.firstChild.remove()
  90. const obj = this._viewer?.scene.modelRoot
  91. if (!obj) return
  92. const data = obj.children.reduce(this._buildData, [])
  93. let firstChange = false
  94. return new Promise<void>((resolve, _reject) => {
  95. this.treeView = new Tree(this.hierarchyDiv, {
  96. closeDepth: 1,
  97. data,
  98. // values: visible, // uuids of visible nodes
  99. loaded: function() {
  100. this.values = []
  101. resolve()
  102. },
  103. onChange: () => {
  104. if (!firstChange) { // first time called when loaded
  105. firstChange = true
  106. return
  107. }
  108. // timeout(200).then(() => { // wait for the UI to update
  109. this._setVisible()
  110. // })
  111. },
  112. onItemLabelClick: (item: any) => {
  113. const obj1 = this._viewer?.scene.modelRoot.getObjectByProperty('uuid', item)
  114. if (!obj1 || !obj.visible) return
  115. obj1.dispatchEvent({type: 'select', value: obj1, object: obj1, ui: true})
  116. },
  117. })
  118. }).then(()=>{
  119. this._refreshVisible()
  120. this._selectedObjectChanged()
  121. })
  122. }
  123. private _refreshVisible() {
  124. const obj = this._viewer?.scene.modelRoot as Object3D
  125. if (!obj) return
  126. const visible = obj.children.reduce(this._findVisible, [])
  127. this.treeView?.emptyNodesCheckStatus()
  128. this.treeView?.treeNodes.map(n=>refreshVisible(n, visible, this.treeView!))
  129. this.treeView?.updateLiElements()
  130. }
  131. onAdded(viewer: ThreeViewer) {
  132. super.onAdded(viewer)
  133. viewer.scene.addEventListener('sceneUpdate', this.reset)
  134. viewer.addEventListener('postFrame', this._postFrame)
  135. viewer.forPlugin<PickingPlugin>('PickingPlugin', (pi)=>{
  136. pi.addEventListener('selectedObjectChanged', this._selectedObjectChanged)
  137. }, (pi)=>{
  138. pi.removeEventListener('selectedObjectChanged', this._selectedObjectChanged)
  139. })
  140. viewer.forPlugin<UndoManagerPlugin>('UndoManagerPlugin', (um)=>{
  141. this.undoManager = um.undoManager
  142. }, ()=>{
  143. this.undoManager = undefined
  144. })
  145. this.reset()
  146. }
  147. undoManager?: UndoManagerPlugin['undoManager'] = undefined
  148. private _selectedObjectChanged = ()=>{
  149. const picking = this._viewer?.getPlugin(PickingPlugin)
  150. if (!picking || !this.treeView) return
  151. const liElem = this.treeView.liElementsById as Record<string, HTMLLIElement>
  152. const elems = Object.values(liElem)
  153. for (const li of elems) {
  154. li.classList.remove('treejs-node-selected')
  155. }
  156. const selected = picking.getSelectedObject() as Object3D|undefined
  157. if (selected?.uuid) {
  158. const li = liElem[selected?.uuid]
  159. if (li) {
  160. li.classList.add('treejs-node-selected')
  161. li.scrollIntoView({block: 'nearest', inline: 'nearest'})
  162. }
  163. }
  164. }
  165. onRemove(viewer: ThreeViewer) {
  166. // todo: remove UI element.
  167. viewer.scene.removeEventListener('sceneUpdate', this.reset)
  168. viewer.removeEventListener('postFrame', this._postFrame)
  169. viewer.getPlugin(PickingPlugin)?.removeEventListener('selectedObjectChanged', this._selectedObjectChanged)
  170. return super.onRemove(viewer)
  171. }
  172. protected _needsReset = false
  173. protected _postFrame() {
  174. if (this._needsReset) this._reset()
  175. }
  176. dispose() {
  177. // todo destroy UI element.
  178. }
  179. uiConfig: UiObjectConfig = {
  180. type: 'folder',
  181. label: 'Hierarchy',
  182. children: [],
  183. }
  184. private _buildData = (data: any[], obj: IObject3D): any[] => {
  185. data.push({
  186. text: obj.name || 'unnamed',
  187. id: obj.uuid,
  188. children: obj.children.reduce(this._buildData, []),
  189. })
  190. return data
  191. }
  192. private _findVisible = (data: any[], obj: Object3D): any[] => { // only leaf.
  193. if (!obj.visible) return data
  194. data.push(obj.uuid)
  195. data.push(...obj.children.reduce(this._findVisible, []))
  196. return data
  197. }
  198. private _setVisible = (): void => {
  199. this._viewer?.doOnce('postFrame', () => {
  200. const obj = this._viewer?.scene.modelRoot
  201. if (!obj || !this.treeView) return
  202. const changeMap: Map<Object3D, [boolean, boolean]> = new Map()
  203. function cAdd(o: Object3D, v: boolean) {
  204. const c = changeMap.get(o)?.[0] ?? o.visible
  205. if (v !== c) changeMap.set(o, [v, o.visible])
  206. }
  207. obj.traverse((o)=>{
  208. if (o === obj) return
  209. // @ts-expect-error no type
  210. const node = this.treeView.nodesById[o.uuid] as TNode|undefined
  211. if (node) {
  212. cAdd(o, !!node.status)
  213. if (o.visible) o.traverseAncestors(p => cAdd(p, true))
  214. }
  215. })
  216. const cmd = {
  217. redo: (refresh = true)=>{
  218. let changed = false
  219. changeMap.entries().forEach(([o, v])=> {
  220. if (o.visible === v[0]) return
  221. o.visible = v[0]
  222. changed = true
  223. })
  224. if (!changed) return
  225. this._viewer?.scene?.setDirty({refreshScene: true, source: HierarchyUiPlugin.PluginType, updateGround: false})
  226. refresh && this._refreshVisible()
  227. },
  228. undo: ()=>{
  229. let changed = false
  230. changeMap.entries().forEach(([o, v])=> {
  231. if (o.visible === v[1]) return
  232. o.visible = v[1]
  233. changed = true
  234. })
  235. if (!changed) return
  236. this._viewer?.scene?.setDirty({refreshScene: true, source: HierarchyUiPlugin.PluginType, updateGround: false})
  237. this._refreshVisible()
  238. },
  239. }
  240. cmd.redo(false)
  241. this.undoManager?.record(cmd)
  242. })
  243. }
  244. }
  245. interface TNode {id: string, status: 0|1|2, text: string, children: TNode[]}
  246. // this is required because setValues in Treejs toggles it, not sets it
  247. function refreshVisible(node: TNode, visibles: string[], tree: Tree) {
  248. const v = visibles.includes(node.id)
  249. const last = node.status
  250. if (node.children.length) {
  251. let allVisible = true
  252. for (const child of node.children) {
  253. const res = refreshVisible(child, visibles, tree)
  254. if (!res) allVisible = false
  255. }
  256. node.status = v ? allVisible ? 2 : 1 : 0
  257. } else {
  258. node.status = v ? 2 : 0
  259. }
  260. if (last !== node.status) {
  261. // @ts-expect-error no type
  262. tree.willUpdateNodesById[node.id] = node
  263. }
  264. return node.status
  265. }