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.

BlueprintJsUiPlugin.ts 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import {UiConfigRendererBlueprint} from 'uiconfig-blueprint'
  2. import {
  3. Class,
  4. Color,
  5. createDiv,
  6. CustomContextMenu,
  7. downloadBlob,
  8. getOrCall,
  9. IEvent,
  10. IViewerPlugin,
  11. IViewerPluginSync,
  12. Texture,
  13. ThreeViewer,
  14. uploadFile,
  15. Vector2,
  16. Vector3,
  17. Vector4,
  18. } from 'threepipe'
  19. import {UiObjectConfig} from 'uiconfig.js'
  20. export class BlueprintJsUiPlugin extends UiConfigRendererBlueprint implements IViewerPluginSync {
  21. declare ['constructor']: typeof BlueprintJsUiPlugin
  22. static readonly PluginType = 'BlueprintJsUi'
  23. enabled = true
  24. constructor(container: HTMLElement = document.body) {
  25. super(container, {
  26. autoPostFrame: false,
  27. })
  28. this.THREE = {Color, Vector4, Vector3, Vector2, Texture} as any
  29. }
  30. protected _viewer?: ThreeViewer
  31. onAdded(viewer: ThreeViewer): void {
  32. this._viewer = viewer
  33. viewer.addEventListener('preRender', this._preRender)
  34. viewer.addEventListener('postRender', this._postRender)
  35. viewer.addEventListener('preFrame', this._preFrame)
  36. viewer.addEventListener('postFrame', this._postFrame)
  37. }
  38. onRemove(viewer: ThreeViewer): void {
  39. this._viewer = undefined
  40. viewer.removeEventListener('preRender', this._preRender)
  41. viewer.removeEventListener('postRender', this._postRender)
  42. viewer.removeEventListener('preFrame', this._preFrame)
  43. viewer.removeEventListener('postFrame', this._postFrame)
  44. this.dispose()
  45. }
  46. private _plugins: IViewerPlugin[] = []
  47. setupPlugins(...plugins: Class<IViewerPlugin>[]): void {
  48. plugins.forEach(plugin => this.setupPluginUi(plugin))
  49. }
  50. setupPluginUi<T extends IViewerPlugin>(plugin: T|Class<T>): UiObjectConfig | undefined {
  51. const p = (<Class<IViewerPlugin>>plugin).prototype ? this._viewer?.getPlugin<T>(<Class<T>>plugin) : <T>plugin
  52. if (!p) {
  53. console.warn('plugin not found:', plugin)
  54. return undefined
  55. }
  56. this._plugins.push(p)
  57. if (p.uiConfig && p.uiConfig.hidden === undefined) p.uiConfig.hidden = false // todo; this is a hack for now
  58. const ui = p.uiConfig
  59. this.appendChild(ui)
  60. // this._setupPluginSerializationContext(ui, p)
  61. return ui
  62. }
  63. // @ts-expect-error todo: port to blueprint from tweakpane
  64. private _setupPluginSerializationContext(ui: any, p: IViewerPlugin) {
  65. // serialization
  66. if (!(ui?.uiRef && p.toJSON)) return;
  67. (p as any)._defaultState = typeof p.toJSON === 'function' ? p.toJSON() : null
  68. ;(p as any).resetDefaults = async() => {
  69. if (!(p as any)._defaultState) return
  70. await p.fromJSON?.((p as any)._defaultState)
  71. ui.uiRefresh?.(true, 'postFrame')
  72. }
  73. const topBtn = (ui.uiRef as any).controller_.view.element
  74. const opBtn = createDiv({
  75. innerHTML: '&#8942;',
  76. classList: ['pluginOptionsButton'],
  77. elementTag: 'button',
  78. })
  79. opBtn.onclick = (ev) => {
  80. const ops = {} as any
  81. if (typeof p.toJSON === 'function') {
  82. ops['download preset'] = async() => {
  83. if (!this._viewer) return
  84. const json = this._viewer.exportPluginConfig(p)
  85. await downloadBlob(new Blob([JSON.stringify(json, null, 2)], {type: 'application/json'}), 'preset.' + (p.constructor as any).PluginType + '.json')
  86. }
  87. }
  88. if (typeof p.fromJSON === 'function') {
  89. ops['upload preset'] = async() => {
  90. const files = await uploadFile(false, false)
  91. if (files.length === 0) return
  92. const file = files[0]
  93. const text = await file.text()
  94. const json = JSON.parse(text)
  95. await this._viewer?.importPluginConfig(json, p)
  96. ui.uiRefresh?.(true, 'postFrame')
  97. }
  98. if ((p as any)._defaultState) ops['reset defaults'] = () => (p as any).resetDefaults?.()
  99. }
  100. const menu = CustomContextMenu.Create(ops, topBtn.clientWidth - 120, 12)
  101. topBtn.append(menu)
  102. ev.preventDefault()
  103. }
  104. topBtn.appendChild(opBtn)
  105. }
  106. refreshPluginsEnabled() {
  107. this._plugins.forEach(p=>{
  108. const config = p.uiConfig
  109. if (config) {
  110. // const enabled = (p as any).enabled ?? true
  111. // safeSetProperty(config, 'hidden', !enabled, true)
  112. // if (config.expanded)
  113. // safeSetProperty(config, 'expanded', config.expanded && enabled, true)
  114. if (getOrCall(config.hidden) !== true)
  115. config.uiRefresh?.(true, 'postFrame')
  116. else if (config.uiRef) {
  117. config.uiRef.hidden = true
  118. }
  119. }
  120. })
  121. }
  122. /**
  123. * Required for loading files in BPFileComponent
  124. */
  125. get fileLoader() {
  126. return this._viewer
  127. }
  128. private _preRender = () => this.refreshQueue('preRender')
  129. private _postRender = () => this.refreshQueue('postRender')
  130. private _postFrame = (e: IEvent<'postFrame'>) => {
  131. this.dispatchEvent(e)
  132. this.refreshQueue('postFrame')
  133. }
  134. private _preFrame = () => this.refreshQueue('preFrame')
  135. // alert = async(message?: string): Promise<void> =>this._viewer ? this._viewer.dialog.alert(message) : window?.alert(message)
  136. // confirm = async(message?: string): Promise<boolean> =>this._viewer ? this._viewer.dialog.confirm(message) : window?.confirm(message)
  137. // prompt = async(message?: string, _default?: string, cancel = true): Promise<string | null> =>this._viewer ? this._viewer.dialog.prompt(message, _default, cancel) : window?.prompt(message, _default)
  138. }