diff --git a/CHANGELOG.md b/CHANGELOG.md index d7211759e33229d017b9ab1850d51dc66a08ec7a..d6b7c270d150181d4a18f093f711aa707f1ab37a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] -- Add `PluginContext.mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI +- Add `PluginContext.initContainer/mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI +- Add `PluginContext.canvas3dInitialized` +- `createPluginUI` now resolves after the 3d canvas has been initialized. ## [v3.22.0] - 2022-10-17 diff --git a/src/apps/docking-viewer/index.ts b/src/apps/docking-viewer/index.ts index a1fd579695a748004fc9e7559f294581fee2d2aa..2d7f5d71c97b7ed6a7aa473727b1143ef74935e5 100644 --- a/src/apps/docking-viewer/index.ts +++ b/src/apps/docking-viewer/index.ts @@ -58,20 +58,22 @@ class Viewer { } static async create(elementOrId: string | HTMLElement, colors = [Color(0x992211), Color(0xDDDDDD)], showButtons = true) { - const o = { ...DefaultViewerOptions, ...{ - layoutIsExpanded: false, - layoutShowControls: false, - layoutShowRemoteState: false, - layoutShowSequence: true, - layoutShowLog: false, - layoutShowLeftPanel: true, - - viewportShowExpand: true, - viewportShowControls: false, - viewportShowSettings: false, - viewportShowSelectionMode: false, - viewportShowAnimation: false, - } }; + const o = { + ...DefaultViewerOptions, ...{ + layoutIsExpanded: false, + layoutShowControls: false, + layoutShowRemoteState: false, + layoutShowSequence: true, + layoutShowLog: false, + layoutShowLeftPanel: true, + + viewportShowExpand: true, + viewportShowControls: false, + viewportShowSettings: false, + viewportShowSelectionMode: false, + viewportShowAnimation: false, + } + }; const defaultSpec = DefaultPluginUISpec(); const spec: PluginUISpec = { @@ -135,18 +137,16 @@ class Viewer { } }; - plugin.behaviors.canvas3d.initialized.subscribe(v => { - if (v) { - PluginCommands.Canvas3D.SetSettings(plugin, { settings: { - renderer: { - ...plugin.canvas3d!.props.renderer, - backgroundColor: ColorNames.white, - }, - camera: { - ...plugin.canvas3d!.props.camera, - helper: { axes: { name: 'off', params: {} } } - } - } }); + PluginCommands.Canvas3D.SetSettings(plugin, { + settings: { + renderer: { + ...plugin.canvas3d!.props.renderer, + backgroundColor: ColorNames.white, + }, + camera: { + ...plugin.canvas3d!.props.camera, + helper: { axes: { name: 'off', params: {} } } + } } }); diff --git a/src/examples/alpha-orbitals/controls.tsx b/src/examples/alpha-orbitals/controls.tsx index bbde98561a7f76137e9b7456338871f3a1efe9dc..11c8147d9453dcd1107a33a760c3c7ed600bc5c7 100644 --- a/src/examples/alpha-orbitals/controls.tsx +++ b/src/examples/alpha-orbitals/controls.tsx @@ -4,16 +4,16 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import * as ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { AlphaOrbitalsExample } from '.'; import { ParameterControls } from '../../mol-plugin-ui/controls/parameters'; import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior'; import { PluginContextContainer } from '../../mol-plugin-ui/plugin'; export function mountControls(orbitals: AlphaOrbitalsExample, parent: Element) { - ReactDOM.render(<PluginContextContainer plugin={orbitals.plugin}> + createRoot(parent).render(<PluginContextContainer plugin={orbitals.plugin}> <Controls orbitals={orbitals} /> - </PluginContextContainer>, parent); + </PluginContextContainer>); } function Controls({ orbitals }: { orbitals: AlphaOrbitalsExample }) { diff --git a/src/examples/alpha-orbitals/index.ts b/src/examples/alpha-orbitals/index.ts index 7239bcc511664789ab4878dab94cf4b8dc8b2564..cf463bfeefd8d92aa74137c562be5d416d286983 100644 --- a/src/examples/alpha-orbitals/index.ts +++ b/src/examples/alpha-orbitals/index.ts @@ -82,24 +82,20 @@ export class AlphaOrbitalsExample { this.plugin.managers.interactivity.setProps({ granularity: 'element' }); - this.plugin.behaviors.canvas3d.initialized.subscribe(init => { - if (!init) return; - - if (!canComputeGrid3dOnGPU(this.plugin.canvas3d?.webgl)) { - PluginCommands.Toast.Show(this.plugin, { - title: 'Error', - message: `Browser/device does not support required WebGL extension (OES_texture_float).` - }); - return; - } - - this.load({ - moleculeSdf: DemoMoleculeSDF, - ...DemoOrbitals + if (!canComputeGrid3dOnGPU(this.plugin.canvas3d?.webgl)) { + PluginCommands.Toast.Show(this.plugin, { + title: 'Error', + message: `Browser/device does not support required WebGL extension (OES_texture_float).` }); + return; + } - mountControls(this, document.getElementById('controls')!); + this.load({ + moleculeSdf: DemoMoleculeSDF, + ...DemoOrbitals }); + + mountControls(this, document.getElementById('controls')!); } readonly params = new BehaviorSubject<ParamDefinition.For<Params>>({} as any); diff --git a/src/mol-plugin-ui/index.ts b/src/mol-plugin-ui/index.ts index 4b32f50fa698c9a20535fc663adb60d194cd0f8e..01c4f85c9f6c56766a511703e39a23de94f09fd3 100644 --- a/src/mol-plugin-ui/index.ts +++ b/src/mol-plugin-ui/index.ts @@ -18,5 +18,10 @@ export async function createPluginUI(target: HTMLElement, spec?: PluginUISpec, o await options.onBeforeUIRender(ctx); } ReactDOM.render(React.createElement(Plugin, { plugin: ctx }), target); + try { + await ctx.canvas3dInitialized; + } catch { + // Error reported in UI/console elsewhere. + } return ctx; } \ No newline at end of file diff --git a/src/mol-plugin-ui/react18.ts b/src/mol-plugin-ui/react18.ts index 6309eebc1a7f0bd49aed99ac13662ac0a8a02bb4..fd9113ba83a1257c533bbbf7e3fb5d8f52aae94f 100644 --- a/src/mol-plugin-ui/react18.ts +++ b/src/mol-plugin-ui/react18.ts @@ -18,5 +18,10 @@ export async function createPluginUI(target: HTMLElement, spec?: PluginUISpec, o await options.onBeforeUIRender(ctx); } createRoot(target).render(createElement(Plugin, { plugin: ctx })); + try { + await ctx.canvas3dInitialized; + } catch { + // Error reported in UI/console elsewhere. + } return ctx; } \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 16cb5b519d18bc8b69926f686b433b8304cef16e..37b7b598df8a8f4ead895e590921c18ec5a2f148 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -35,7 +35,7 @@ import { Representation } from '../mol-repr/representation'; import { StructureRepresentationRegistry } from '../mol-repr/structure/registry'; import { VolumeRepresentationRegistry } from '../mol-repr/volume/registry'; import { StateTransform } from '../mol-state'; -import { RuntimeContext, Task } from '../mol-task'; +import { RuntimeContext, Scheduler, Task } from '../mol-task'; import { ColorTheme } from '../mol-theme/color'; import { SizeTheme } from '../mol-theme/size'; import { ThemeRegistryContext } from '../mol-theme/theme'; @@ -71,6 +71,7 @@ export class PluginContext { }; protected subs: Subscription[] = []; + private initCanvas3dPromiseCallbacks: [res: () => void, rej: (err: any) => void] = [() => {}, () => {}]; private disposed = false; private canvasContainer: HTMLDivElement | undefined = void 0; @@ -103,10 +104,15 @@ export class PluginContext { leftPanelTabName: this.ev.behavior<LeftPanelTabName>('root') }, canvas3d: { + // TODO: remove in 4.0? initialized: this.canvas3dInit.pipe(filter(v => !!v), take(1)) } } as const; + readonly canvas3dInitialized = new Promise<void>((res, rej) => { + this.initCanvas3dPromiseCallbacks = [res, rej]; + }); + readonly canvas3dContext: Canvas3DContext | undefined; readonly canvas3d: Canvas3D | undefined; readonly layout = new PluginLayout(this); @@ -187,44 +193,49 @@ export class PluginContext { */ readonly customState: unknown = Object.create(null); - mount(target: HTMLElement, canvas3dContext?: Canvas3DContext) { - if (this.disposed) throw new Error('Cannot mount a disposed context'); - - if (!this.canvasContainer) { - const container = document.createElement('div'); - Object.assign(container.style, { - position: 'absolute', - left: 0, - top: 0, - right: 0, - bottom: 0, - '-webkit-user-select': 'none', - 'user-select': 'none', - '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', - '-webkit-touch-callout': 'none', - 'touch-action': 'manipulation', + initContainer(canvas3dContext?: Canvas3DContext) { + if (this.canvasContainer) return true; + + const container = document.createElement('div'); + Object.assign(container.style, { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + '-webkit-user-select': 'none', + 'user-select': 'none', + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', + '-webkit-touch-callout': 'none', + 'touch-action': 'manipulation', + }); + let canvas = canvas3dContext?.canvas; + if (!canvas) { + canvas = document.createElement('canvas'); + Object.assign(canvas.style, { + 'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)', + 'background-size': '60px 60px', + 'background-position': '0 0, 30px 30px' }); - let canvas = canvas3dContext?.canvas; - if (!canvas) { - canvas = document.createElement('canvas'); - Object.assign(canvas.style, { - 'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)', - 'background-size': '60px 60px', - 'background-position': '0 0, 30px 30px' - }); - container.appendChild(canvas); - } - if (!this.initViewer(canvas, container, canvas3dContext)) { - return false; - } - this.canvasContainer = container; + container.appendChild(canvas); + } + if (!this.initViewer(canvas, container, canvas3dContext)) { + return false; } + this.canvasContainer = container; + return true; + } + + mount(target: HTMLElement) { + if (this.disposed) throw new Error('Cannot mount a disposed context'); + + if (!this.initContainer()) return false; - if (this.canvasContainer.parentElement !== target) { - this.canvasContainer.parentElement?.removeChild(this.canvasContainer); + if (this.canvasContainer!.parentElement !== target) { + this.canvasContainer!.parentElement?.removeChild(this.canvasContainer!); } - target.appendChild(this.canvasContainer); + target.appendChild(this.canvasContainer!); this.handleResize(); return true; } @@ -279,10 +290,12 @@ export class PluginContext { this.handleResize(); + Scheduler.setImmediate(() => this.initCanvas3dPromiseCallbacks[0]()); return true; } catch (e) { this.log.error('' + e); console.error(e); + Scheduler.setImmediate(() => this.initCanvas3dPromiseCallbacks[1](e)); return false; } }