|
|
|
|
|
|
|
|
import {createDiv, createStyles, css, timeout} from 'ts-browser-helpers' |
|
|
|
|
|
|
|
|
import {createDiv, createStyles, css} from 'ts-browser-helpers' |
|
|
import Tree from 'treejs' |
|
|
import Tree from 'treejs' |
|
|
import {AViewerPluginSync, IObject3D, Object3D, ThreeViewer} from 'threepipe' |
|
|
|
|
|
|
|
|
import { |
|
|
|
|
|
AViewerPluginSync, |
|
|
|
|
|
IObject3D, |
|
|
|
|
|
ISceneEventMap, |
|
|
|
|
|
Object3D, |
|
|
|
|
|
PickingPlugin, |
|
|
|
|
|
ThreeViewer, |
|
|
|
|
|
type UndoManagerPlugin, Event2, |
|
|
|
|
|
} from 'threepipe' |
|
|
import {UiObjectConfig} from 'uiconfig.js' |
|
|
import {UiObjectConfig} from 'uiconfig.js' |
|
|
|
|
|
|
|
|
export class HierarchyUiPlugin extends AViewerPluginSync { |
|
|
export class HierarchyUiPlugin extends AViewerPluginSync { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toJSON: any = undefined |
|
|
toJSON: any = undefined |
|
|
|
|
|
|
|
|
treeView: any = undefined |
|
|
|
|
|
|
|
|
treeView?: Tree = undefined |
|
|
|
|
|
|
|
|
hierarchyDiv = createDiv({ |
|
|
hierarchyDiv = createDiv({ |
|
|
innerHTML: '', |
|
|
innerHTML: '', |
|
|
|
|
|
|
|
|
.treejs .treejs-switcher:before { |
|
|
.treejs .treejs-switcher:before { |
|
|
border-top: 6px solid var(--tp-container-foreground-color, hsl(230, 7%, 75%)) !important; |
|
|
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?: any) { |
|
|
|
|
|
|
|
|
reset(e?: Event2<'sceneUpdate', ISceneEventMap, IObject3D>) { |
|
|
if (e?.source === HierarchyUiPlugin.PluginType) return // for infinite loop |
|
|
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 |
|
|
if (!e?.hierarchyChanged) return |
|
|
this._needsReset = true |
|
|
this._needsReset = true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
protected async _reset() { |
|
|
protected async _reset() { |
|
|
this._needsReset = false |
|
|
this._needsReset = false |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!obj) return |
|
|
if (!obj) return |
|
|
|
|
|
|
|
|
const data = obj.children.reduce(this._buildData, []) |
|
|
const data = obj.children.reduce(this._buildData, []) |
|
|
const visible = obj.children.reduce(this._findVisible, []) |
|
|
|
|
|
let firstChange = false |
|
|
let firstChange = false |
|
|
return new Promise<void>((resolve) => { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return new Promise<void>((resolve, _reject) => { |
|
|
this.treeView = new Tree(this.hierarchyDiv, { |
|
|
this.treeView = new Tree(this.hierarchyDiv, { |
|
|
closeDepth: 1, |
|
|
closeDepth: 1, |
|
|
data, |
|
|
data, |
|
|
// values: visible, // uuids of visible nodes |
|
|
// values: visible, // uuids of visible nodes |
|
|
loaded: function() { |
|
|
loaded: function() { |
|
|
this.values = visible |
|
|
|
|
|
|
|
|
this.values = [] |
|
|
resolve() |
|
|
resolve() |
|
|
}, |
|
|
}, |
|
|
onChange: () => { |
|
|
onChange: () => { |
|
|
|
|
|
|
|
|
firstChange = true |
|
|
firstChange = true |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
timeout(200).then(() => { // wait for the UI to update |
|
|
|
|
|
if (this.treeView) |
|
|
|
|
|
this._setVisible(this.treeView.values) |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
// timeout(200).then(() => { // wait for the UI to update |
|
|
|
|
|
this._setVisible() |
|
|
|
|
|
// }) |
|
|
}, |
|
|
}, |
|
|
onItemLabelClick: (item: any) => { |
|
|
onItemLabelClick: (item: any) => { |
|
|
const obj1 = this._viewer?.scene.modelRoot.getObjectByProperty('uuid', item) |
|
|
const obj1 = this._viewer?.scene.modelRoot.getObjectByProperty('uuid', item) |
|
|
|
|
|
|
|
|
obj1.dispatchEvent({type: 'select', value: obj1, object: obj1, ui: true}) |
|
|
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) { |
|
|
onAdded(viewer: ThreeViewer) { |
|
|
super.onAdded(viewer) |
|
|
super.onAdded(viewer) |
|
|
this.reset() |
|
|
|
|
|
viewer.scene.addEventListener('sceneUpdate', this.reset) |
|
|
viewer.scene.addEventListener('sceneUpdate', this.reset) |
|
|
viewer.addEventListener('postFrame', this._postFrame) |
|
|
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) { |
|
|
onRemove(viewer: ThreeViewer) { |
|
|
// todo: remove UI element. |
|
|
// todo: remove UI element. |
|
|
viewer.scene.removeEventListener('sceneUpdate', this.reset) |
|
|
viewer.scene.removeEventListener('sceneUpdate', this.reset) |
|
|
viewer.removeEventListener('postFrame', this._postFrame) |
|
|
viewer.removeEventListener('postFrame', this._postFrame) |
|
|
|
|
|
viewer.getPlugin(PickingPlugin)?.removeEventListener('selectedObjectChanged', this._selectedObjectChanged) |
|
|
return super.onRemove(viewer) |
|
|
return super.onRemove(viewer) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
private _findVisible = (data: any[], obj: Object3D): any[] => { // only leaf. |
|
|
private _findVisible = (data: any[], obj: Object3D): any[] => { // only leaf. |
|
|
if (!obj.visible) return data |
|
|
if (!obj.visible) return data |
|
|
if (obj.children.length < 1) data.push(obj.uuid) |
|
|
|
|
|
else data.push(...obj.children.reduce(this._findVisible, [])) |
|
|
|
|
|
|
|
|
data.push(obj.uuid) |
|
|
|
|
|
data.push(...obj.children.reduce(this._findVisible, [])) |
|
|
return data |
|
|
return data |
|
|
} |
|
|
} |
|
|
private _setVisible = (values: string[]): void => { |
|
|
|
|
|
|
|
|
private _setVisible = (): void => { |
|
|
this._viewer?.doOnce('postFrame', () => { |
|
|
this._viewer?.doOnce('postFrame', () => { |
|
|
const obj = this._viewer?.scene.modelRoot |
|
|
const obj = this._viewer?.scene.modelRoot |
|
|
if (!obj || values === undefined || values === null) return |
|
|
|
|
|
const ancestorSet: Set<Object3D> = new Set() |
|
|
|
|
|
|
|
|
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)=>{ |
|
|
obj.traverse((o)=>{ |
|
|
if (o === obj) return |
|
|
if (o === obj) return |
|
|
o.visible = values.includes(o.uuid) |
|
|
|
|
|
if (o.visible) o.traverseAncestors(p=>ancestorSet.add(p)) |
|
|
|
|
|
|
|
|
// @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)) |
|
|
|
|
|
} |
|
|
}) |
|
|
}) |
|
|
ancestorSet.forEach(o=> o.visible = true) |
|
|
|
|
|
this._viewer?.scene?.setDirty({refreshScene: true, source: HierarchyUiPlugin.PluginType, updateGround: false}) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
} |