Browse Source

added docking-viewer example

- expects a pdbqt and a mol2 file in the url get params
Alexander Rose 4 years ago
parent
commit
b5252516e3

+ 50 - 0
src/examples/docking-viewer/index.html

@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+        <title>Mol* Docking Viewer</title>
+        <style>
+            #app {
+                position: absolute;
+                left: 100px;
+                top: 100px;
+                width: 800px;
+                height: 600px;
+            }
+        </style>
+        <link rel="stylesheet" type="text/css" href="molstar.css" />
+    </head>
+    <body>
+        <div id="app"></div>
+        <script type="text/javascript" src="./index.js"></script>
+        <script type="text/javascript">
+            var viewer = new DockingViewer('app', {
+                layoutIsExpanded: false,
+                layoutShowControls: false,
+                layoutShowRemoteState: false,
+                layoutShowSequence: true,
+                layoutShowLog: false,
+                layoutShowLeftPanel: true,
+
+                viewportShowExpand: true,
+                viewportShowControls: false,
+                viewportShowSettings: false,
+                viewportShowSelectionMode: false,
+                viewportShowAnimation: false,
+            });
+
+            function getParam(name, regex) {
+                var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
+                return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
+            }
+            var pdbqt = getParam('pdbqt', '[^&]+').trim();
+            var mol2 = getParam('mol2', '[^&]+').trim();
+
+            viewer.loadStructuresFromUrlsAndMerge([
+                { url: pdbqt, format: 'pdbqt' },
+                { url: mol2, format: 'mol2' }
+            ]);
+        </script>
+    </body>
+</html>

+ 263 - 0
src/examples/docking-viewer/index.ts

@@ -0,0 +1,263 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import '../../mol-util/polyfill';
+import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
+import './index.html';
+import { PluginContext } from '../../mol-plugin/context';
+import { PluginCommands } from '../../mol-plugin/commands';
+import { PluginSpec } from '../../mol-plugin/spec';
+import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
+import { PluginConfig } from '../../mol-plugin/config';
+import { Asset } from '../../mol-util/assets';
+import { ObjectKeys } from '../../mol-util/type-helpers';
+import { PluginState } from '../../mol-plugin/state';
+import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
+import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
+import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
+import { Structure } from '../../mol-model/structure';
+import { PluginStateTransform, PluginStateObject as PSO } from '../../mol-plugin-state/objects';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Task } from '../../mol-task';
+import { StateObject } from '../../mol-state';
+import { ViewportComponent, StructurePreset } from './viewport';
+import { PluginBehaviors } from '../../mol-plugin/behavior';
+import { ColorNames } from '../../mol-util/color/names';
+
+require('mol-plugin-ui/skin/light.scss');
+
+export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
+export { setProductionMode, setDebugMode } from '../../mol-util/debug';
+
+const DefaultViewerOptions = {
+    extensions: ObjectKeys({}),
+    layoutIsExpanded: true,
+    layoutShowControls: true,
+    layoutShowRemoteState: true,
+    layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
+    layoutShowSequence: true,
+    layoutShowLog: true,
+    layoutShowLeftPanel: true,
+
+    viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
+    viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
+    viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
+    viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
+    viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
+    pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
+    volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
+    pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
+    emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
+};
+type ViewerOptions = typeof DefaultViewerOptions;
+
+class Viewer {
+    plugin: PluginContext
+
+    constructor(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
+        const o = { ...DefaultViewerOptions, ...options };
+
+        const spec: PluginSpec = {
+            actions: [...DefaultPluginSpec.actions],
+            behaviors: [
+                PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci, { mark: false }),
+                PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
+                PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
+
+                PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo),
+                PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions),
+                PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
+            ],
+            animations: [...DefaultPluginSpec.animations || []],
+            customParamEditors: DefaultPluginSpec.customParamEditors,
+            layout: {
+                initial: {
+                    isExpanded: o.layoutIsExpanded,
+                    showControls: o.layoutShowControls,
+                    controlsDisplay: o.layoutControlsDisplay,
+                },
+                controls: {
+                    ...DefaultPluginSpec.layout && DefaultPluginSpec.layout.controls,
+                    top: o.layoutShowSequence ? undefined : 'none',
+                    bottom: o.layoutShowLog ? undefined : 'none',
+                    left: o.layoutShowLeftPanel ? undefined : 'none',
+                }
+            },
+            components: {
+                ...DefaultPluginSpec.components,
+                remoteState: o.layoutShowRemoteState ? 'default' : 'none',
+                viewport: {
+                    view: ViewportComponent
+                }
+            },
+            config: [
+                [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
+                [PluginConfig.Viewport.ShowControls, o.viewportShowControls],
+                [PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
+                [PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
+                [PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
+                [PluginConfig.State.DefaultServer, o.pluginStateServer],
+                [PluginConfig.State.CurrentServer, o.pluginStateServer],
+                [PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
+                [PluginConfig.Download.DefaultPdbProvider, o.pdbProvider],
+                [PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider]
+            ]
+        };
+
+        const element = typeof elementOrId === 'string'
+            ? document.getElementById(elementOrId)
+            : elementOrId;
+        if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
+        this.plugin = createPlugin(element, spec);
+
+        PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: {
+            renderer: {
+                ...this.plugin.canvas3d!.props.renderer,
+                backgroundColor: ColorNames.white,
+            },
+            camera: {
+                ...this.plugin.canvas3d!.props.camera,
+                helper: { axes: { name: 'off', params: {} } }
+            }
+        } });
+    }
+
+    setRemoteSnapshot(id: string) {
+        const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
+        return PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
+    }
+
+    loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {
+        return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
+    }
+
+    loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false) {
+        const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
+            source: {
+                name: 'url',
+                params: {
+                    url: Asset.Url(url),
+                    format: format as any,
+                    isBinary,
+                    options: params.source.params.options,
+                }
+            }
+        }));
+    }
+
+    async loadStructuresFromUrlsAndMerge(sources: { url: string, format: BuiltInTrajectoryFormat, isBinary?: boolean }[]) {
+        const structures: { ref: string }[] = [];
+        for (const { url, format, isBinary } of sources) {
+            const data = await this.plugin.builders.data.download({ url, isBinary });
+            const trajectory = await this.plugin.builders.structure.parseTrajectory(data, format);
+            const model = await this.plugin.builders.structure.createModel(trajectory);
+            const modelProperties = await this.plugin.builders.structure.insertModelProperties(model);
+            const structure = await this.plugin.builders.structure.createStructure(modelProperties || model);
+            const structureProperties = await this.plugin.builders.structure.insertStructureProperties(structure);
+
+            structures.push({ ref: structureProperties?.ref || structure.ref });
+        }
+        const dependsOn = structures.map(({ ref }) => ref);
+        const data = this.plugin.state.data.build().toRoot().apply(MergeStructures, { structures }, { dependsOn });
+        const structure = await data.commit();
+        const structureProperties = await this.plugin.builders.structure.insertStructureProperties(structure);
+        await this.plugin.builders.structure.representation.applyPreset(structureProperties || structure, StructurePreset);
+    }
+
+    async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, options?: { dataLabel?: string }) {
+        const _data = await this.plugin.builders.data.rawData({ data, label: options?.dataLabel });
+        const trajectory = await this.plugin.builders.structure.parseTrajectory(_data, format);
+        await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
+    }
+
+    loadPdb(pdb: string) {
+        const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
+        const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!;
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
+            source: {
+                name: 'pdb' as const,
+                params: {
+                    provider: {
+                        id: pdb,
+                        server: {
+                            name: provider,
+                            params: PdbDownloadProvider[provider].defaultValue as any
+                        }
+                    },
+                    options: params.source.params.options,
+                }
+            }
+        }));
+    }
+
+    loadPdbDev(pdbDev: string) {
+        const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
+            source: {
+                name: 'pdb-dev' as const,
+                params: {
+                    provider: {
+                        id: pdbDev,
+                        encoding: 'bcif',
+                    },
+                    options: params.source.params.options,
+                }
+            }
+        }));
+    }
+
+    loadEmdb(emdb: string) {
+        const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!;
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
+            source: {
+                name: 'pdb-emd-ds' as const,
+                params: {
+                    provider: {
+                        id: emdb,
+                        server: provider,
+                    },
+                    detail: 3,
+                }
+            }
+        }));
+    }
+}
+
+type MergeStructures = typeof MergeStructures
+const MergeStructures = PluginStateTransform.BuiltIn({
+    name: 'merge-structures',
+    display: { name: 'Merge Structures', description: 'Merge Structure' },
+    from: PSO.Root,
+    to: PSO.Molecule.Structure,
+    params: {
+        structures: PD.ObjectList({
+            ref: PD.Text('')
+        }, ({ ref }) => ref, { isHidden: true })
+    }
+})({
+    apply({ params, dependencies }) {
+        return Task.create('Merge Structures', async ctx => {
+            if (params.structures.length === 0) return StateObject.Null;
+
+            const first = dependencies![params.structures[0].ref].data as Structure;
+            const builder = Structure.Builder({ masterModel: first.models[0] });
+            for (const { ref } of params.structures) {
+                const s = dependencies![ref].data as Structure;
+                for (const unit of s.units) {
+                    // TODO invariantId
+                    builder.addUnit(unit.kind, unit.model, unit.conformation.operator, unit.elements, unit.traits);
+                }
+            }
+
+            const structure = builder.getStructure();
+            return new PSO.Molecule.Structure(structure, { label: 'Merged Structure' });
+        });
+    }
+});
+
+(window as any).DockingViewer = Viewer;

+ 254 - 0
src/examples/docking-viewer/viewport.tsx

@@ -0,0 +1,254 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react';
+import { PluginUIComponent } from '../../mol-plugin-ui/base';
+import { Viewport, ViewportControls } from '../../mol-plugin-ui/viewport';
+import { BackgroundTaskProgress } from '../../mol-plugin-ui/task';
+import { LociLabels } from '../../mol-plugin-ui/controls';
+import { Toasts } from '../../mol-plugin-ui/toast';
+import { Button } from '../../mol-plugin-ui/controls/common';
+import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../mol-plugin-state/builder/structure/representation-preset';
+import { StateObjectRef } from '../../mol-state';
+import { StructureSelectionQueries, StructureSelectionQuery } from '../../mol-plugin-state/helpers/structure-selection-query';
+import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
+import { InteractionsRepresentationProvider } from '../../mol-model-props/computed/representations/interactions';
+import { InteractionTypeColorThemeProvider } from '../../mol-model-props/computed/themes/interaction-type';
+import { compile } from '../../mol-script/runtime/query/compiler';
+import { StructureSelection, QueryContext, Structure } from '../../mol-model/structure';
+import { PluginCommands } from '../../mol-plugin/commands';
+import { PluginContext } from '../../mol-plugin/context';
+
+function shinyStyle(plugin: PluginContext) {
+    return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
+        renderer: {
+            ...plugin.canvas3d!.props.renderer,
+            style: { name: 'plastic', params: {} },
+        },
+        postprocessing: {
+            ...plugin.canvas3d!.props.postprocessing,
+            occlusion: { name: 'off', params: {} },
+            outline: { name: 'off', params: {} }
+        }
+    } });
+}
+
+function occlusionStyle(plugin: PluginContext) {
+    return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
+        renderer: {
+            ...plugin.canvas3d!.props.renderer,
+            style: { name: 'flat', params: {} }
+        },
+        postprocessing: {
+            ...plugin.canvas3d!.props.postprocessing,
+            occlusion: { name: 'on', params: {
+                kernelSize: 8,
+                bias: 0.8,
+                radius: 64
+            } },
+            outline: { name: 'on', params: {
+                scale: 1.0,
+                threshold: 0.8
+            } }
+        }
+    } });
+}
+
+const ligandPlusSurroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Ligand plus Ligand itself', MS.struct.modifier.union([
+    MS.struct.modifier.includeSurroundings({
+        0: StructureSelectionQueries.ligand.expression,
+        radius: 5,
+        'as-whole-residues': true
+    })
+]));
+
+const ligandSurroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Ligand', MS.struct.modifier.union([
+    MS.struct.modifier.exceptBy({
+        0: ligandPlusSurroundings.expression,
+        by: StructureSelectionQueries.ligand.expression
+    })
+]));
+
+const PresetParams = {
+    ...StructureRepresentationPresetProvider.CommonParams,
+};
+
+export const StructurePreset = StructureRepresentationPresetProvider({
+    id: 'preset-structure',
+    display: { name: 'Structure' },
+    params: () => PresetParams,
+    async apply(ref, params, plugin) {
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        if (!structureCell) return {};
+
+        const components = {
+            ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
+            polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
+        };
+
+        const { update, builder, typeParams, color } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
+        const representations = {
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color }, { tag: 'ligand' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams }, color }, { tag: 'polymer' }),
+        };
+
+        await update.commit({ revertOnError: true });
+        await shinyStyle(plugin);
+        plugin.managers.interactivity.setProps({ granularity: 'residue' });
+
+        return { components, representations };
+    }
+});
+
+export const IllustrativePreset = StructureRepresentationPresetProvider({
+    id: 'preset-illustrative',
+    display: { name: 'Illustrative' },
+    params: () => PresetParams,
+    async apply(ref, params, plugin) {
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        if (!structureCell) return {};
+
+        const components = {
+            all: await presetStaticComponent(plugin, structureCell, 'all')
+        };
+
+        const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
+        const representations = {
+            all: builder.buildRepresentation(update, components.all, { type: 'spacefill', typeParams: { ...typeParams }, color: 'illustrative' }, { tag: 'all' }),
+        };
+
+        await update.commit({ revertOnError: true });
+        await occlusionStyle(plugin);
+        plugin.managers.interactivity.setProps({ granularity: 'residue' });
+
+        return { components, representations };
+    }
+});
+
+const PocketPreset = StructureRepresentationPresetProvider({
+    id: 'preset-pocket',
+    display: { name: 'Pocket' },
+    params: () => PresetParams,
+    async apply(ref, params, plugin) {
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        const structure = structureCell?.obj?.data;
+        if (!structureCell || !structure) return {};
+
+        const components = {
+            ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
+            surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `selection`),
+        };
+
+        const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
+        const representations = {
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'partial-charge' }, { tag: 'ligand' }),
+            surroundings: builder.buildRepresentation(update, components.surroundings, { type: 'molecular-surface', typeParams: { ...typeParams, includeParent: true, quality: 'custom', resolution: 0.2, doubleSided: true }, color: 'partial-charge' }, { tag: 'surroundings' }),
+        };
+
+        await update.commit({ revertOnError: true });
+        await shinyStyle(plugin);
+        plugin.managers.interactivity.setProps({ granularity: 'element' });
+
+        const compiled = compile<StructureSelection>(StructureSelectionQueries.ligand.expression);
+        const result = compiled(new QueryContext(structure));
+        const selection = StructureSelection.unionStructure(result);
+        plugin.managers.camera.focusLoci(Structure.toStructureElementLoci(selection));
+
+        return { components, representations };
+    }
+});
+
+const InteractionsPreset = StructureRepresentationPresetProvider({
+    id: 'preset-interactions',
+    display: { name: 'Interactions' },
+    params: () => PresetParams,
+    async apply(ref, params, plugin) {
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        const structure = structureCell?.obj?.data;
+        if (!structureCell || !structure) return {};
+
+        const components = {
+            ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
+            selection: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `selection`)
+        };
+
+        const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
+        const representations = {
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'partial-charge' }, { tag: 'ligand' }),
+            ballAndStick: builder.buildRepresentation(update, components.selection, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'partial-charge' }, { tag: 'ball-and-stick' }),
+            interactions: builder.buildRepresentation(update, components.selection, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
+        };
+
+        await update.commit({ revertOnError: true });
+        await shinyStyle(plugin);
+        plugin.managers.interactivity.setProps({ granularity: 'element' });
+
+        const compiled = compile<StructureSelection>(StructureSelectionQueries.ligand.expression);
+        const result = compiled(new QueryContext(structure));
+        const selection = StructureSelection.unionStructure(result);
+        plugin.managers.camera.focusLoci(Structure.toStructureElementLoci(selection));
+
+        return { components, representations };
+    }
+});
+
+export class ViewportComponent extends PluginUIComponent {
+    structurePreset = () => {
+        this.plugin.managers.structure.component.applyPreset(
+            this.plugin.managers.structure.hierarchy.selection.structures,
+            StructurePreset
+        );
+    }
+
+    illustrativePreset = () => {
+        this.plugin.managers.structure.component.applyPreset(
+            this.plugin.managers.structure.hierarchy.selection.structures,
+            IllustrativePreset
+        );
+    }
+
+    pocketPreset = () => {
+        this.plugin.managers.structure.component.applyPreset(
+            this.plugin.managers.structure.hierarchy.selection.structures,
+            PocketPreset
+        );
+    }
+
+    interactionsPreset = () => {
+        this.plugin.managers.structure.component.applyPreset(
+            this.plugin.managers.structure.hierarchy.selection.structures,
+            InteractionsPreset
+        );
+    }
+
+    render() {
+        const VPControls = this.plugin.spec.components?.viewport?.controls || ViewportControls;
+
+        return <>
+            <Viewport />
+            <div className='msp-viewport-top-left-controls'>
+                <div style={{ marginBottom: '4px' }}>
+                    <Button onClick={this.structurePreset} >Structure</Button>
+                </div>
+                <div style={{ marginBottom: '4px' }}>
+                    <Button onClick={this.illustrativePreset}>Illustrative</Button>
+                </div>
+                <div style={{ marginBottom: '4px' }}>
+                    <Button onClick={this.pocketPreset}>Pocket</Button>
+                </div>
+                <div style={{ marginBottom: '4px' }}>
+                    <Button onClick={this.interactionsPreset}>Interactions</Button>
+                </div>
+            </div>
+            <VPControls />
+            <BackgroundTaskProgress />
+            <div className='msp-highlight-toast-wrapper'>
+                <LociLabels />
+                <Toasts />
+            </div>
+        </>;
+    }
+}

+ 1 - 1
webpack.config.js

@@ -1,6 +1,6 @@
 const { createApp, createExample, createBrowserTest } = require('./webpack.config.common.js');
 
-const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting'];
+const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting', 'docking-viewer'];
 const tests = [
     'font-atlas',
     'marching-cubes',