Browse Source

Issue #2: separated tm-viewer application and reverted viewer source

cycle20 2 years ago
parent
commit
d26cb97600

+ 4 - 0
package.json

@@ -16,20 +16,24 @@
     "test": "npm run lint && jest",
     "jest": "jest",
     "build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
+    "build-tm": "npm run build-tsc && npm run build-webpack-tm",
     "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
     "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
     "build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/",
     "build-webpack": "webpack --mode production --config ./webpack.config.production.js",
     "build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js",
+    "build-webpack-tm": "webpack --mode production --config ./webpack.config.tm-viewer.js",
     "watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"",
     "watch-viewer": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer\"",
     "watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"",
+    "watch-tm": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-webpack-tm\"",
     "watch-tsc": "tsc --watch --incremental",
     "watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
     "watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch",
     "watch-webpack": "webpack -w --mode development --stats minimal",
     "watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
     "watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
+    "watch-webpack-tm": "webpack -w --mode development --stats minimal --config ./webpack.config.tm-viewer.js",
     "serve": "http-server -p 1338 -g",
     "model-server": "node lib/commonjs/servers/model/server.js",
     "model-server-watch": "nodemon --watch lib lib/commonjs/servers/model/server.js",

+ 44 - 0
src/apps/tm-viewer/embedded.html

@@ -0,0 +1,44 @@
+<!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">
+        <link rel="icon" href="./favicon.ico" type="image/x-icon">
+        <title>Embedded Mol* 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="./molstar.js"></script>
+        <script type="text/javascript">
+            var viewer = new molstar.Viewer('app', {
+                layoutIsExpanded: true,
+                layoutShowControls: false,
+                layoutShowRemoteState: false,
+                layoutShowSequence: true,
+                layoutShowLog: false,
+                layoutShowLeftPanel: true,
+
+                viewportShowExpand: true,
+                viewportShowSelectionMode: false,
+                viewportShowAnimation: false,
+
+                pdbProvider: 'rcsb',
+                emdbProvider: 'rcsb',
+            });
+            viewer.loadPdb('7bv2');
+            viewer.loadEmdb('EMD-30210', { detail: 6 });
+
+            // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
+        </script>
+    </body>
+</html>

BIN
src/apps/tm-viewer/favicon.ico


+ 71 - 0
src/apps/tm-viewer/index.html

@@ -0,0 +1,71 @@
+<!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">
+        <link rel="icon" href="./favicon.ico" type="image/x-icon">
+        <title>Mol* Viewer</title>
+        <style>
+            * {
+                margin: 0;
+                padding: 0;
+                box-sizing: border-box;
+            }
+            html, body {
+                width: 100%;
+                height: 100%;
+                overflow: hidden;
+            }
+            hr {
+                margin: 10px;
+            }
+            h1, h2, h3, h4, h5 {
+                margin-top: 5px;
+                margin-bottom: 3px;
+            }
+            button {
+                padding: 2px;
+            }
+            #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="./molstar.js"></script>
+        <script type="text/javascript">
+            function getParam(name, regex) {
+                var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
+                return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
+            }
+
+            var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
+            if (debugMode) molstar.setDebugMode(debugMode, debugMode);
+
+            var viewer = new molstar.Viewer('app', {
+                layoutShowControls: true,
+                viewportShowExpand: true,
+                collapseLeftPanel: false
+            });
+
+            // Set PDB Id here
+            var regionDescriptors = {"pdb-id":"1afo", "creation-date":"2021-09-03","is-transmembrane":1,"membrane-normal":{"x":0,"y":0,"z":17.75},"chains":[{"chain_id":"A","type":"alpha","seq":"VQLAHHFSEPEITLIIFGVMAGVIGTILLISYGIRRLIKK","regions":{"1":{"auth_ids":[62,63,64,65,66,67,68,69,70,71,72,73],"color":[255,0,0]},"H":{"auth_ids":[74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95],"color":[255,255,0]},"2":{"auth_ids":[96,97,98,99,100,101],"color":[0,0,255]}}},{"chain_id":"B","type":"alpha","seq":"VQLAHHFSEPEITLIIFGVMAGVIGTILLISYGIRRLIKK","regions":{"1":{"auth_ids":[62,63,64,65,66,67,68,69,70,71,72,73],"color":[255,0,0]},"H":{"auth_ids":[74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99],"color":[255,255,0]},"2":{"auth_ids":[100,101],"color":[0,0,255]}}}]};
+            var pdbtmId = regionDescriptors["pdb-id"];
+            regionDescriptors.format = 'mmcif'
+            viewer.loadWithUNITMPMembraneRepresentation(
+                // NOTE: Prepare CORS settings appropriately on backend
+                //       or made this index.html accessible from the same
+                //       origin (DOMAIN:PORT values).
+                //`https://DOMAIN[:PORT]/api/pdbtm/${pdbtmId}/trpdb`,
+                `https://cs.litemol.org/${pdbtmId}/full`,
+                regionDescriptors
+            );
+        </script>
+    </body>
+</html>

+ 267 - 0
src/apps/tm-viewer/index.ts

@@ -0,0 +1,267 @@
+/**
+ * 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 { TMDETMembraneOrientation } from '../../extensions/tmdet/behavior';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { createPlugin } from '../../mol-plugin-ui';
+import { PluginUIContext } from '../../mol-plugin-ui/context';
+import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
+import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
+import { PluginConfig } from '../../mol-plugin/config';
+import { PluginSpec } from '../../mol-plugin/spec';
+import { StateBuilder, StateObjectRef } from '../../mol-state';
+import { Color } from '../../mol-util/color';
+import '../../mol-util/polyfill';
+import { ObjectKeys } from '../../mol-util/type-helpers';
+import './embedded.html';
+import './favicon.ico';
+import './index.html';
+import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
+import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
+import { Expression } from '../../mol-script/language/expression';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { MembraneOrientation } from '../../extensions/tmdet/prop';
+import { MEMBRANE_STORAGE_KEY } from '../../extensions/tmdet/algorithm';
+import { Quat, Vec3 } from '../../mol-math/linear-algebra';
+import { PluginCommands } from '../../mol-plugin/commands';
+
+require('mol-plugin-ui/skin/light.scss');
+
+export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
+export { setDebugMode, setProductionMode } from '../../mol-util/debug';
+
+const Extensions = {
+    'tmdet-membrane-orientation': PluginSpec.Behavior(TMDETMembraneOrientation)
+};
+
+const DefaultViewerOptions = {
+    extensions: ObjectKeys(Extensions),
+    layoutIsExpanded: true,
+    layoutShowControls: true,
+    layoutShowRemoteState: true,
+    layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
+    layoutShowSequence: true,
+    layoutShowLog: true,
+    layoutShowLeftPanel: true,
+    collapseLeftPanel: false,
+    disableAntialiasing: false,
+    pixelScale: 1,
+    enableWboit: 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,
+    volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
+    pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
+    emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
+};
+type ViewerOptions = typeof DefaultViewerOptions;
+
+
+export var membrane: MembraneOrientation;
+
+export class Viewer {
+    plugin: PluginUIContext
+
+
+
+
+
+    ////////////////////////////// UNITMP VIEWER PROTOTYPING SECTION
+
+    async loadWithUNITMPMembraneRepresentation(url: string, regionDescriptors: any) {
+        const membraneNormal: Vec3 = Vec3.fromObj(
+            regionDescriptors['membrane-normal']
+        );
+        const membrane: MembraneOrientation = {
+            planePoint1: Vec3.fromArray(Vec3.zero(), membraneNormal, 0),
+            planePoint2: Vec3.fromArray(Vec3.zero(), membraneNormal, 0),
+            // NOTE: centroid is not 0,0,0. It is x,y,0. Right?
+            centroid: Vec3.fromArray(
+                Vec3.zero(), [ membraneNormal[0], membraneNormal[1], 0 ], 0
+            ),
+            normalVector: membraneNormal,
+
+            // TODO: radius is still just a dummy value now.
+            //       Can we send a precalculated value by our backend?
+            //
+            // (NOTE: the TMDET extension calculates and sets it during applying preset)
+            radius: regionDescriptors[ 'radius' ]
+        };
+        membrane.planePoint2[2] *= -1;
+
+        localStorage.setItem(MEMBRANE_STORAGE_KEY, JSON.stringify(membrane));
+
+        const isBinary = false;
+
+        const data = await this.plugin.builders.data.download({
+            url, label: regionDescriptors['pdb-id'], isBinary
+        }); //, { state: { isGhost: true } });
+        const trajectory = await this.plugin.builders.structure.parseTrajectory(data, regionDescriptors.format);
+        // create membrane representation
+        await this.plugin.builders.structure.hierarchy.applyPreset(
+            trajectory, 'default', { representationPreset: 'preset-membrane-orientation' as any });
+
+        const structure: StateObjectRef<PluginStateObject.Molecule.Structure> =
+            this.plugin.managers.structure.hierarchy.current.models[0].structures[0].cell;
+        const components = {
+            polymer: await this.plugin.builders.structure.tryCreateComponentStatic(structure, 'polymer'),
+            ligand: await this.plugin.builders.structure.tryCreateComponentStatic(structure, 'ligand'),
+            water: await this.plugin.builders.structure.tryCreateComponentStatic(structure, 'water'),
+        };
+
+        const builder = this.plugin.builders.structure.representation;
+        const update = this.plugin.build();
+        if (components.polymer) builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { alpha: 0.5 } }, { tag: 'polymer' });
+        if (components.ligand) builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick' }, { tag: 'ligand' });
+        if (components.water) builder.buildRepresentation(update, components.water, { type: 'ball-and-stick', typeParams: { alpha: 0.6 } }, { tag: 'water' });
+        await update.commit();
+
+        regionDescriptors.chains.forEach((chain: any) => {
+
+            for(let regionKey in chain.regions) {
+              const update = this.plugin.build();
+              const region = chain.regions[regionKey];
+              this.createRegionRepresentation(chain.chain_id, region, update.to(structure));
+              update.commit();
+            }
+
+        });
+
+        //
+        // reset the camera because the membranes render 1st and the structure might not be fully visible
+        //
+        this.rotateCamera();
+
+        //requestAnimationFrame(() => this.plugin.canvas3d?.requestCameraReset());
+    }
+
+    private createRegionRepresentation(chain: string, region: any, update: StateBuilder.To<any, any>) {
+        const regionLabel: string = `${chain} | ${region.name}`;
+        const color: Color = Color.fromArray(region.color, 0);
+        const query: Expression = this.getQuery(chain, region.auth_ids as number[]);
+
+        // based on https://github.com/molstar/molstar/issues/209
+        update
+            .apply(StateTransforms.Model.StructureSelectionFromExpression, { label: regionLabel, expression: query })
+            .apply(StateTransforms.Representation.StructureRepresentation3D, createStructureRepresentationParams(this.plugin, update.selector.data, {
+                color: 'uniform',
+                colorParams: { value: color }
+            }));
+    }
+
+    private getQuery(chainId: string, auth_array: number[]): Expression {
+        const query: Expression =
+            MS.struct.generator.atomGroups({
+                'residue-test': MS.core.set.has([MS.set( ...auth_array ), MS.ammp('auth_seq_id')]),
+                'chain-test': MS.core.rel.eq([chainId, MS.ammp('label_asym_id')])
+            });
+        return query;
+    }
+
+    private rotateCamera(): void {
+        function rot90q(v: Vec3, axis: Vec3 = Vec3.create(1, 0, 0)): Vec3 {
+            const q = Quat.setAxisAngle(Quat(), axis, -Math.PI/2);
+            return Vec3.transformQuat(Vec3(), v, q);
+        }
+        function sub(v: Vec3, u: Vec3): Vec3 {
+            return Vec3.sub(Vec3(), v, u);
+        }
+        function add(v: Vec3, u: Vec3): Vec3 {
+            return Vec3.add(Vec3(), v, u);
+        }
+
+
+        const cam = this.plugin.canvas3d!.camera;
+        const snapshot = cam.getSnapshot();
+        const newSnapshot = {
+            ...snapshot,
+            // target + rotateBy90(postition - target)
+            position: add(snapshot.target, rot90q(sub(snapshot.position, snapshot.target))),
+            target: snapshot.target,
+            up: Vec3.negUnitZ
+        };
+
+        PluginCommands.Camera.Reset(this.plugin, { snapshot: newSnapshot, durationMs: 1000 });
+        //requestAnimationFrame(() => this.plugin.canvas3d?.requestCameraReset({ snapshot: newSnapshot }));
+    }
+
+    ////////////////////////////// END OF PROTOTYPING SECTION
+
+
+
+
+
+
+    constructor(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
+        const o = { ...DefaultViewerOptions, ...options };
+        const defaultSpec = DefaultPluginUISpec();
+
+        const spec: PluginUISpec = {
+            actions: defaultSpec.actions,
+            behaviors: [
+                ...defaultSpec.behaviors,
+                ...o.extensions.map(e => Extensions[e]),
+            ],
+            animations: [...defaultSpec.animations || []],
+            customParamEditors: defaultSpec.customParamEditors,
+            layout: {
+                initial: {
+                    isExpanded: o.layoutIsExpanded,
+                    showControls: o.layoutShowControls,
+                    controlsDisplay: o.layoutControlsDisplay,
+                    regionState: {
+                        bottom: 'full',
+                        left: o.collapseLeftPanel ? 'collapsed' : 'full',
+                        right: 'full',
+                        top: 'full',
+                    }
+                },
+            },
+            components: {
+                ...defaultSpec.components,
+                controls: {
+                    ...defaultSpec.components?.controls,
+                    top: o.layoutShowSequence ? undefined : 'none',
+                    bottom: o.layoutShowLog ? undefined : 'none',
+                    left: o.layoutShowLeftPanel ? undefined : 'none',
+                },
+                remoteState: o.layoutShowRemoteState ? 'default' : 'none',
+            },
+            config: [
+                [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
+                [PluginConfig.General.PixelScale, o.pixelScale],
+                [PluginConfig.General.EnableWboit, o.enableWboit],
+                [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.VolumeStreaming.Enabled, !o.volumeStreamingDisabled],
+                [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);
+    }
+
+    handleResize() {
+        this.plugin.layout.events.updated.next();
+    }
+}

+ 31 - 13
src/apps/viewer/index.html

@@ -36,7 +36,7 @@
         </style>
         <link rel="stylesheet" type="text/css" href="molstar.css" />
     </head>
-    <body onload="loadPdb()">
+    <body>
         <div id="app"></div>
         <script type="text/javascript" src="./molstar.js"></script>
         <script type="text/javascript">
@@ -48,20 +48,38 @@
             var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
             if (debugMode) molstar.setDebugMode(debugMode, debugMode);
 
+            var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
+            var collapseLeftPanel = getParam('collapse-left-panel', '[^&]+').trim() === '1';
+            var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase();
+            var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase();
             var viewer = new molstar.Viewer('app', {
-                layoutShowControls: false,
-                viewportShowExpand: true,
-                collapseLeftPanel: true
+                layoutShowControls: !hideControls,
+                viewportShowExpand: false,
+                collapseLeftPanel: collapseLeftPanel,
+                pdbProvider: pdbProvider || 'pdbe',
+                emdbProvider: emdbProvider || 'pdbe',
             });
 
-            // Set PDB Id here
-            var pdbId = '2atk';
-            function loadPdb() {
-                molstar.loadWithUNITMPMembraneRepresentation(viewer.plugin, {
-                    structureUrl: `https://cs.litemol.org/${pdbId}/full`,
-                    regionDescriptorUrl: `http://localhost:8000/build/data/${pdbId}.json`,
-                });
-            }
+            var snapshotId = getParam('snapshot-id', '[^&]+').trim();
+            if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
+
+            var snapshotUrl = getParam('snapshot-url', '[^&]+').trim();
+            var snapshotUrlType = getParam('snapshot-url-type', '[^&]+').toLowerCase().trim() || 'molj';
+            if (snapshotUrl && snapshotUrlType) viewer.loadSnapshotFromUrl(snapshotUrl, snapshotUrlType);
+
+            var structureUrl = getParam('structure-url', '[^&]+').trim();
+            var structureUrlFormat = getParam('structure-url-format', '[a-z]+').toLowerCase().trim();
+            var structureUrlIsBinary = getParam('structure-url-is-binary', '[^&]+').trim() === '1';
+            if (structureUrl) viewer.loadStructureFromUrl(structureUrl, structureUrlFormat, structureUrlIsBinary);
+
+            var pdb = getParam('pdb', '[^&]+').trim();
+            if (pdb) viewer.loadPdb(pdb);
+
+            var pdbDev = getParam('pdb-dev', '[^&]+').trim();
+            if (pdbDev) viewer.loadPdbDev(pdbDev);
+
+            var emdb = getParam('emdb', '[^&]+').trim();
+            if (emdb) viewer.loadEmdb(emdb);
         </script>
     </body>
-</html>
+</html>

+ 342 - 0
src/apps/viewer/index.js

@@ -0,0 +1,342 @@
+/**
+ * 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 { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
+import { CellPack } from '../../extensions/cellpack';
+import { DnatcoConfalPyramids } from '../../extensions/dnatco';
+import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
+import { Mp4Export } from '../../extensions/mp4-export';
+import { GeometryExport } from '../../extensions/geo-export';
+import { PDBeStructureQualityReport } from '../../extensions/pdbe';
+import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
+import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
+import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
+import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
+import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
+import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
+import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
+import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { createPlugin } from '../../mol-plugin-ui';
+import { PluginUIContext } from '../../mol-plugin-ui/context';
+import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
+import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
+import { PluginCommands } from '../../mol-plugin/commands';
+import { PluginConfig } from '../../mol-plugin/config';
+import { PluginSpec } from '../../mol-plugin/spec';
+import { PluginState } from '../../mol-plugin/state';
+import { StateObjectSelector } from '../../mol-state';
+import { Asset } from '../../mol-util/assets';
+import { Color } from '../../mol-util/color';
+import '../../mol-util/polyfill';
+import { ObjectKeys } from '../../mol-util/type-helpers';
+import './embedded.html';
+import './favicon.ico';
+import './index.html';
+
+require('mol-plugin-ui/skin/light.scss');
+
+export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
+export { setDebugMode, setProductionMode } from '../../mol-util/debug';
+
+const CustomFormats = [
+    ['g3d', G3dProvider] as const
+];
+
+const Extensions = {
+    'cellpack': PluginSpec.Behavior(CellPack),
+    'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
+    'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
+    'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
+    'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
+    'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
+    'g3d': PluginSpec.Behavior(G3DFormat),
+    'mp4-export': PluginSpec.Behavior(Mp4Export),
+    'geo-export': PluginSpec.Behavior(GeometryExport)
+};
+
+const DefaultViewerOptions = {
+    customFormats: CustomFormats as [string, DataFormatProvider][],
+    extensions: ObjectKeys(Extensions),
+    layoutIsExpanded: true,
+    layoutShowControls: true,
+    layoutShowRemoteState: true,
+    layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
+    layoutShowSequence: true,
+    layoutShowLog: true,
+    layoutShowLeftPanel: true,
+    collapseLeftPanel: false,
+    disableAntialiasing: false,
+    pixelScale: 1,
+    enableWboit: 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,
+    volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
+    pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
+    emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
+};
+type ViewerOptions = typeof DefaultViewerOptions;
+
+export class Viewer {
+    plugin: PluginUIContext
+
+    constructor(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
+        const o = { ...DefaultViewerOptions, ...options };
+        const defaultSpec = DefaultPluginUISpec();
+
+        const spec: PluginUISpec = {
+            actions: defaultSpec.actions,
+            behaviors: [
+                ...defaultSpec.behaviors,
+                ...o.extensions.map(e => Extensions[e]),
+            ],
+            animations: [...defaultSpec.animations || []],
+            customParamEditors: defaultSpec.customParamEditors,
+            customFormats: o?.customFormats,
+            layout: {
+                initial: {
+                    isExpanded: o.layoutIsExpanded,
+                    showControls: o.layoutShowControls,
+                    controlsDisplay: o.layoutControlsDisplay,
+                    regionState: {
+                        bottom: 'full',
+                        left: o.collapseLeftPanel ? 'collapsed' : 'full',
+                        right: 'full',
+                        top: 'full',
+                    }
+                },
+            },
+            components: {
+                ...defaultSpec.components,
+                controls: {
+                    ...defaultSpec.components?.controls,
+                    top: o.layoutShowSequence ? undefined : 'none',
+                    bottom: o.layoutShowLog ? undefined : 'none',
+                    left: o.layoutShowLeftPanel ? undefined : 'none',
+                },
+                remoteState: o.layoutShowRemoteState ? 'default' : 'none',
+            },
+            config: [
+                [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
+                [PluginConfig.General.PixelScale, o.pixelScale],
+                [PluginConfig.General.EnableWboit, o.enableWboit],
+                [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.VolumeStreaming.Enabled, !o.volumeStreamingDisabled],
+                [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);
+    }
+
+    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, options?: LoadStructureOptions) {
+        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, representationParams: options?.representationParams as any },
+                }
+            }
+        }));
+    }
+
+    async loadAllModelsOrAssemblyFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
+        const plugin = this.plugin;
+
+        const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
+        const trajectory = await plugin.builders.structure.parseTrajectory(data, format);
+
+        await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'all-models', { useDefaultIfSingleModel: true, representationPresetParams: options?.representationParams });
+    }
+
+    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, options?: LoadStructureOptions) {
+        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, representationParams: options?.representationParams as any },
+                }
+            }
+        }));
+    }
+
+    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, options?: { detail?: number }) {
+        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: options?.detail ?? 3,
+                }
+            }
+        }));
+    }
+
+    /**
+     * @example Load X-ray density from volume server
+        viewer.loadVolumeFromUrl({
+            url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/cell?detail=3',
+            format: 'dscif',
+            isBinary: true
+        }, [{
+            type: 'relative',
+            value: 1.5,
+            color: 0x3362B2
+        }, {
+            type: 'relative',
+            value: 3,
+            color: 0x33BB33,
+            volumeIndex: 1
+        }, {
+            type: 'relative',
+            value: -3,
+            color: 0xBB3333,
+            volumeIndex: 1
+        }], {
+            entryId: ['2FO-FC', 'FO-FC'],
+            isLazy: true
+        });
+     * *********************
+     * @example Load EM density from volume server
+        viewer.loadVolumeFromUrl({
+            url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6',
+            format: 'dscif',
+            isBinary: true
+        }, [{
+            type: 'relative',
+            value: 1,
+            color: 0x3377aa
+        }], {
+            entryId: 'EMD-30210',
+            isLazy: true
+        });
+     */
+    async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string | string[], isLazy?: boolean }) {
+        const plugin = this.plugin;
+
+        if (!plugin.dataFormats.get(format)) {
+            throw new Error(`Unknown density format: ${format}`);
+        }
+
+        if (options?.isLazy) {
+            const update = this.plugin.build();
+            update.toRoot().apply(StateTransforms.Data.LazyVolume, {
+                url,
+                format,
+                entryId: options?.entryId,
+                isBinary,
+                isovalues: isovalues.map(v => ({ alpha: 1, volumeIndex: 0, ...v }))
+            });
+            return update.commit();
+        }
+
+        return plugin.dataTransaction(async () => {
+            const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
+
+            const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId });
+            const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
+            if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
+
+            const repr = plugin.build();
+            for (const iso of isovalues) {
+                repr
+                    .to(parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume)
+                    .apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
+                        type: 'isosurface',
+                        typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
+                        color: 'uniform',
+                        colorParams: { value: iso.color }
+                    }));
+            }
+
+            await repr.commit();
+        });
+    }
+
+    handleResize() {
+        this.plugin.layout.events.updated.next();
+    }
+}
+
+export interface LoadStructureOptions {
+    representationParams?: StructureRepresentationPresetProvider.CommonParams
+}
+
+export interface VolumeIsovalueInfo {
+    type: 'absolute' | 'relative',
+    value: number,
+    color: Color,
+    alpha?: number,
+    volumeIndex?: number
+}

+ 1 - 0
webpack.config.production.js

@@ -4,6 +4,7 @@ const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting', 'alpha-orb
 
 module.exports = [
     createApp('viewer', 'molstar'),
+    createApp('tm-viewer', 'tm-molstar'),
     createApp('docking-viewer', 'molstar'),
     ...examples.map(createExample)
 ];

+ 5 - 0
webpack.config.tm-viewer.js

@@ -0,0 +1,5 @@
+const common = require('./webpack.config.common.js');
+const createApp = common.createApp;
+module.exports = [
+    createApp('tm-viewer', 'tm-molstar')
+];