| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- import {createDiv, createStyles, css} from 'ts-browser-helpers'
- import Tree from 'treejs'
- import {
- AViewerPluginSync,
- IObject3D,
- ISceneEventMap,
- Object3D,
- PickingPlugin,
- ThreeViewer,
- type UndoManagerPlugin, Event2,
- } from 'threepipe'
- import {UiObjectConfig} from 'uiconfig.js'
-
- export class HierarchyUiPlugin extends AViewerPluginSync {
- enabled = true
- public static readonly PluginType = 'HierarchyUiPlugin'
-
- toJSON: any = undefined
-
- treeView?: Tree = undefined
-
- hierarchyDiv = createDiv({
- innerHTML: '',
- id: 'tpHierarchyContainer',
- addToBody: false,
- })
-
- constructor(enabled = true) {
- super()
- this.enabled = enabled
- this.reset = this.reset.bind(this)
- this._postFrame = this._postFrame.bind(this)
- this.uiConfig.domChildren = [this.hierarchyDiv]
- createStyles(css`
- #tpHierarchyContainer{
- width: 100%;
- height: auto;
- background-color: transparent;
- color: var(--tp-container-foreground-color, hsl(230, 7%, 75%));
- margin-top: 0;
- }
- .treejs .treejs-switcher:before {
- border-top: 6px solid var(--tp-container-foreground-color, hsl(230, 7%, 75%)) !important;
- }
- .treejs .treejs-switcher,.treejs-label,.treejs-checkbox {
- pointer-events: auto;
- }
-
- .treejs .treejs-node {
- pointer-events: none;
- position: relative;
- }
-
- .treejs .treejs-label {
- position: absolute;
- height: 16px;
- line-height: 16px;
- text-overflow: ellipsis;
- overflow: hidden;
- width: calc(100% - 50px);
- padding: 1px 4px;
- margin: 1px 0px;
- border-radius: 2px;
- box-sizing: content-box;
- }
-
- .treejs .treejs-node-selected .treejs-label {
- outline-offset: -1px;
- outline: solid 1px #1890ff;
- }
-
- .treejs .treejs-node-selected > .treejs-label {
- background-color: #067ce9;
- color: #eee;
- outline: none;
- }
- `)
-
- }
-
- reset(e?: Event2<'sceneUpdate', ISceneEventMap, IObject3D>) {
- if (e?.source === HierarchyUiPlugin.PluginType) return // for infinite loop
-
- // visible changed from outside
- if (e && e.object && e.change === 'visible' && this.treeView) {
- const nodeId = e.object.uuid
- // @ts-expect-error no type
- const node = this.treeView.nodesById[nodeId] as TNode
- if (node && !!node.status !== e.object.visible) {
- this.treeView.setValue(nodeId)
- this.treeView.updateLiElements()
- }
- // this.treeView.values = obj.children.reduce(this._findVisible, [])
- // console.log(this.treeView.values)
- }
-
- if (!e?.hierarchyChanged) return
- this._needsReset = true
- }
-
- protected async _reset() {
- this._needsReset = false
-
- while (this.hierarchyDiv.firstChild) this.hierarchyDiv.firstChild.remove()
-
- const obj = this._viewer?.scene.modelRoot
- if (!obj) return
-
- const data = obj.children.reduce(this._buildData, [])
- let firstChange = false
-
- return new Promise<void>((resolve, _reject) => {
- this.treeView = new Tree(this.hierarchyDiv, {
- closeDepth: 1,
- data,
- // values: visible, // uuids of visible nodes
- loaded: function() {
- this.values = []
- resolve()
- },
- onChange: () => {
- if (!firstChange) { // first time called when loaded
- firstChange = true
- return
- }
- // timeout(200).then(() => { // wait for the UI to update
- this._setVisible()
- // })
- },
- onItemLabelClick: (item: any) => {
- const obj1 = this._viewer?.scene.modelRoot.getObjectByProperty('uuid', item)
- if (!obj1 || !obj.visible) return
- obj1.dispatchEvent({type: 'select', value: obj1, object: obj1, ui: true})
- },
- })
- }).then(()=>{
- this._refreshVisible()
- this._selectedObjectChanged()
- })
- }
-
- private _refreshVisible() {
- const obj = this._viewer?.scene.modelRoot as Object3D
- if (!obj) return
- const visible = obj.children.reduce(this._findVisible, [])
- this.treeView?.emptyNodesCheckStatus()
- this.treeView?.treeNodes.map(n=>refreshVisible(n, visible, this.treeView!))
- this.treeView?.updateLiElements()
- }
-
- onAdded(viewer: ThreeViewer) {
- super.onAdded(viewer)
- viewer.scene.addEventListener('sceneUpdate', this.reset)
- viewer.addEventListener('postFrame', this._postFrame)
-
- viewer.forPlugin<PickingPlugin>('PickingPlugin', (pi)=>{
- pi.addEventListener('selectedObjectChanged', this._selectedObjectChanged)
- }, (pi)=>{
- pi.removeEventListener('selectedObjectChanged', this._selectedObjectChanged)
- })
- viewer.forPlugin<UndoManagerPlugin>('UndoManagerPlugin', (um)=>{
- this.undoManager = um.undoManager
- }, ()=>{
- this.undoManager = undefined
- })
-
- this.reset()
- }
-
- undoManager?: UndoManagerPlugin['undoManager'] = undefined
-
- private _selectedObjectChanged = ()=>{
- const picking = this._viewer?.getPlugin(PickingPlugin)
- if (!picking || !this.treeView) return
- const liElem = this.treeView.liElementsById as Record<string, HTMLLIElement>
- const elems = Object.values(liElem)
- for (const li of elems) {
- li.classList.remove('treejs-node-selected')
- }
- const selected = picking.getSelectedObject() as Object3D|undefined
- if (selected?.uuid) {
- const li = liElem[selected?.uuid]
- if (li) {
- li.classList.add('treejs-node-selected')
- li.scrollIntoView({block: 'nearest', inline: 'nearest'})
- }
- }
- }
-
- onRemove(viewer: ThreeViewer) {
- // todo: remove UI element.
- viewer.scene.removeEventListener('sceneUpdate', this.reset)
- viewer.removeEventListener('postFrame', this._postFrame)
- viewer.getPlugin(PickingPlugin)?.removeEventListener('selectedObjectChanged', this._selectedObjectChanged)
- return super.onRemove(viewer)
- }
-
- protected _needsReset = false
- protected _postFrame() {
- if (this._needsReset) this._reset()
- }
-
- dispose() {
- // todo destroy UI element.
- }
-
- uiConfig: UiObjectConfig = {
- type: 'folder',
- label: 'Hierarchy',
- children: [],
- }
-
- private _buildData = (data: any[], obj: IObject3D): any[] => {
- data.push({
- text: obj.name || 'unnamed',
- id: obj.uuid,
- children: obj.children.reduce(this._buildData, []),
- })
- return data
- }
- private _findVisible = (data: any[], obj: Object3D): any[] => { // only leaf.
- if (!obj.visible) return data
- data.push(obj.uuid)
- data.push(...obj.children.reduce(this._findVisible, []))
- return data
- }
- private _setVisible = (): void => {
- this._viewer?.doOnce('postFrame', () => {
- const obj = this._viewer?.scene.modelRoot
- if (!obj || !this.treeView) return
- const changeMap: Map<Object3D, [boolean, boolean]> = new Map()
- function cAdd(o: Object3D, v: boolean) {
- const c = changeMap.get(o)?.[0] ?? o.visible
- if (v !== c) changeMap.set(o, [v, o.visible])
- }
- obj.traverse((o)=>{
- if (o === obj) return
- // @ts-expect-error no type
- const node = this.treeView.nodesById[o.uuid] as TNode|undefined
- if (node) {
- cAdd(o, !!node.status)
- if (o.visible) o.traverseAncestors(p => cAdd(p, true))
- }
- })
- const cmd = {
- redo: (refresh = true)=>{
- let changed = false
- changeMap.entries().forEach(([o, v])=> {
- if (o.visible === v[0]) return
- o.visible = v[0]
- changed = true
- })
- if (!changed) return
- this._viewer?.scene?.setDirty({refreshScene: true, source: HierarchyUiPlugin.PluginType, updateGround: false})
- refresh && this._refreshVisible()
- },
- undo: ()=>{
- let changed = false
- changeMap.entries().forEach(([o, v])=> {
- if (o.visible === v[1]) return
- o.visible = v[1]
- changed = true
- })
- if (!changed) return
- this._viewer?.scene?.setDirty({refreshScene: true, source: HierarchyUiPlugin.PluginType, updateGround: false})
- this._refreshVisible()
- },
- }
- cmd.redo(false)
- this.undoManager?.record(cmd)
- })
- }
- }
-
- interface TNode {id: string, status: 0|1|2, text: string, children: TNode[]}
- // this is required because setValues in Treejs toggles it, not sets it
- function refreshVisible(node: TNode, visibles: string[], tree: Tree) {
- const v = visibles.includes(node.id)
- const last = node.status
- if (node.children.length) {
- let allVisible = true
- for (const child of node.children) {
- const res = refreshVisible(child, visibles, tree)
- if (!res) allVisible = false
- }
- node.status = v ? allVisible ? 2 : 1 : 0
- } else {
- node.status = v ? 2 : 0
- }
- if (last !== node.status) {
- // @ts-expect-error no type
- tree.willUpdateNodesById[node.id] = node
- }
- return node.status
- }
|