Browse Source

Merge branch 'master' of https://github.com/molstar/molstar into export-extension

dsehnal 3 năm trước cách đây
mục cha
commit
0ccb045f4e

+ 9 - 2
CHANGELOG.md

@@ -12,16 +12,23 @@ Note that since we don't clearly distinguish between a public and private interf
     - Special case for ``structAsymMap`` if Mol* asym id operator mapping is present
 - Support for opening ZIP files with multiple entries
 - Add Model Export extension
+- Bugfix: Automatically treat empty string as "non-present" value in BinaryCIF writer.
+
+## [v3.0.0-dev.10] - 2022-01-17
+
 - Fix ``getOperatorsForIndex``
 - Pass animation info (current frame & count) to state animations
     - Fix camera stutter for "camera spin" animation
-- Add partial charge parsing support for MOL/SDF files (thanks @ptourlas)
+- Add formal charge parsing support for MOL/SDF files (thanks @ptourlas)
 - [Breaking] Cleaner looking ``MembraneOrientationVisuals`` defaults
+- [Breaking] Add rock animation to trackball controls
+    - Add ``animate`` to ``TrackballControlsParams``, remove ``spin`` and ``spinSpeed``
+    - Add ``animate`` to ``SimpleSettingsParams``, remove ``spin``
+- Add "camera rock" state animation
 - Add support for custom colors to "molecule-type" theme
 - [Breaking] Add style parameter to "illustrative" color theme
     - Defaults to "entity-id" style instead of "chain-id"
 - Add "illustrative" representation preset
-- Bugfix: Automatically treat empty string as "non-present" value in BinaryCIF writer.
 
 ## [v3.0.0-dev.9] - 2022-01-09
 

+ 7 - 0
README.md

@@ -11,6 +11,13 @@ When using Mol*, please cite:
 
 David Sehnal, Sebastian Bittrich, Mandar Deshpande, Radka Svobodová, Karel Berka, Václav Bazgier, Sameer Velankar, Stephen K Burley, Jaroslav Koča, Alexander S Rose: [Mol* Viewer: modern web app for 3D visualization and analysis of large biomolecular structures](https://doi.org/10.1093/nar/gkab314), *Nucleic Acids Research*, 2021; https://doi.org/10.1093/nar/gkab314.
 
+### Protein Data Bank Integrations
+
+- The [pdbe-molstar](https://github.com/molstar/pdbe-molstar) library is the Mol* implementation used by EMBL-EBI data resources such as [PDBe](https://pdbe.org/), [PDBe-KB](https://pdbe-kb.org/) and [AlphaFold DB](https://alphafold.ebi.ac.uk/). This implementation can be used as a JS plugin and a Web component and supports property/attribute-based easy customisation. It provides helper methods to facilitate programmatic interactions between the web application and the 3D viewer. It also provides a superposition view for overlaying all the observed ligand molecules on representative protein conformations.
+
+- [rcsb-molstar](https://github.com/molstar/rcsb-molstar) is the Mol* plugin used by [RCSB PDB](https://www.rcsb.org). The project provides additional presets for the visualization of structure alignments and structure motifs such as ligand binding sites. Furthermore, [rcsb-molstar](https://github.com/molstar/rcsb-molstar) allows to interactively add or hide of (parts of) chains, as seen in the [3D Protein Feature View](https://www.rcsb.org/3d-sequence/4hhb).
+
+
 ## Project Structure Overview
 
 The core of Mol* consists of these modules (see under `src/`):

+ 9 - 26
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "molstar",
-  "version": "3.0.0-dev.9",
+  "version": "3.0.0-dev.10",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "molstar",
-      "version": "3.0.0-dev.9",
+      "version": "3.0.0-dev.10",
       "license": "MIT",
       "dependencies": {
         "@types/argparse": "^2.0.10",
@@ -25,7 +25,7 @@
         "express": "^4.17.2",
         "h264-mp4-encoder": "^1.0.12",
         "immer": "^9.0.12",
-        "immutable": "^3.8.2",
+        "immutable": "^4.0.0",
         "node-fetch": "^2.6.2",
         "rxjs": "^7.5.2",
         "swagger-ui-dist": "^4.1.3",
@@ -7502,12 +7502,9 @@
       }
     },
     "node_modules/immutable": {
-      "version": "3.8.2",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
-      "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=",
-      "engines": {
-        "node": ">=0.10.0"
-      }
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
+      "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw=="
     },
     "node_modules/import-fresh": {
       "version": "3.3.0",
@@ -11906,12 +11903,6 @@
         }
       }
     },
-    "node_modules/sass/node_modules/immutable": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
-      "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
-      "dev": true
-    },
     "node_modules/saxes": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@@ -19732,9 +19723,9 @@
       "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA=="
     },
     "immutable": {
-      "version": "3.8.2",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
-      "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
+      "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw=="
     },
     "import-fresh": {
       "version": "3.3.0",
@@ -23096,14 +23087,6 @@
         "chokidar": ">=3.0.0 <4.0.0",
         "immutable": "^4.0.0",
         "source-map-js": ">=0.6.2 <2.0.0"
-      },
-      "dependencies": {
-        "immutable": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
-          "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
-          "dev": true
-        }
       }
     },
     "sass-loader": {

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "3.0.0-dev.9",
+  "version": "3.0.0-dev.10",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -145,7 +145,7 @@
     "express": "^4.17.2",
     "h264-mp4-encoder": "^1.0.12",
     "immer": "^9.0.12",
-    "immutable": "^3.8.2",
+    "immutable": "^4.0.0",
     "node-fetch": "^2.6.2",
     "rxjs": "^7.5.2",
     "swagger-ui-dist": "^4.1.3",

+ 11 - 3
src/examples/basic-wrapper/index.ts

@@ -74,12 +74,20 @@ class BasicWrapper {
     toggleSpin() {
         if (!this.plugin.canvas3d) return;
 
+        const trackball = this.plugin.canvas3d.props.trackball;
         PluginCommands.Canvas3D.SetSettings(this.plugin, {
-            settings: props => {
-                props.trackball.spin = !props.trackball.spin;
+            settings: {
+                trackball: {
+                    ...trackball,
+                    animate: trackball.animate.name === 'spin'
+                        ? { name: 'off', params: {} }
+                        : { name: 'spin', params: { speed: 1 } }
+                }
             }
         });
-        if (!this.plugin.canvas3d.props.trackball.spin) PluginCommands.Camera.Reset(this.plugin, {});
+        if (this.plugin.canvas3d.props.trackball.animate.name !== 'spin') {
+            PluginCommands.Camera.Reset(this.plugin, {});
+        }
     }
 
     private animateModelIndexTargetFps() {

+ 10 - 1
src/examples/proteopedia-wrapper/index.ts

@@ -256,7 +256,16 @@ class MolStarProteopediaWrapper {
     toggleSpin() {
         if (!this.plugin.canvas3d) return;
         const trackball = this.plugin.canvas3d.props.trackball;
-        PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
+        PluginCommands.Canvas3D.SetSettings(this.plugin, {
+            settings: {
+                trackball: {
+                    ...trackball,
+                    animate: trackball.animate.name === 'spin'
+                        ? { name: 'off', params: {} }
+                        : { name: 'spin', params: { speed: 1 } }
+                }
+            }
+        });
     }
 
     viewport = {

+ 1 - 1
src/mol-canvas3d/canvas3d.ts

@@ -404,7 +404,7 @@ namespace Canvas3D {
 
                 const ctx = { renderer, camera: cam, scene, helper };
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
-                    const forceOn = !cameraChanged && allowMulti && !controls.props.spin;
+                    const forceOn = !cameraChanged && allowMulti && !controls.isAnimating;
                     multiSampleHelper.render(ctx, p, true, forceOn);
                 } else {
                     passes.draw.render(ctx, p, true);

+ 55 - 11
src/mol-canvas3d/controls/trackball.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -13,7 +13,7 @@ import { Viewport } from '../camera/util';
 import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Camera } from '../camera';
-import { absMax } from '../../mol-math/misc';
+import { absMax, degToRad } from '../../mol-math/misc';
 import { Binding } from '../../mol-util/binding';
 
 const B = ButtonsType;
@@ -40,8 +40,16 @@ export const TrackballControlsParams = {
     zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
     panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
 
-    spin: PD.Boolean(false, { description: 'Spin the 3D scene around the x-axis in view space' }),
-    spinSpeed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
+    animate: PD.MappedStatic('off', {
+        off: PD.EmptyGroup(),
+        spin: PD.Group({
+            speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
+        }, { description: 'Spin the 3D scene around the x-axis in view space' }),
+        rock: PD.Group({
+            speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
+            angle: PD.Numeric(10, { min: 0, max: 90, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
+        }, { description: 'Rock the 3D scene around the x-axis in view space' })
+    }),
 
     staticMoving: PD.Boolean(true, { isHidden: true }),
     dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
@@ -72,7 +80,8 @@ export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
 
 export { TrackballControls };
 interface TrackballControls {
-    viewport: Viewport
+    readonly viewport: Viewport
+    readonly isAnimating: boolean
 
     readonly props: Readonly<TrackballControlsProps>
     setProps: (props: Partial<TrackballControlsProps>) => void
@@ -144,6 +153,11 @@ namespace TrackballControls {
             );
         }
 
+        function getRotateFactor() {
+            const aspectRatio = input.width / input.height;
+            return p.rotateSpeed * input.pixelRatio * aspectRatio;
+        }
+
         const rotAxis = Vec3();
         const rotQuat = Quat();
         const rotEyeDir = Vec3();
@@ -156,8 +170,7 @@ namespace TrackballControls {
             const dy = _rotCurr[1] - _rotPrev[1];
             Vec3.set(rotMoveDir, dx, dy, 0);
 
-            const aspectRatio = input.width / input.height;
-            const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed * input.pixelRatio * aspectRatio;
+            const angle = Vec3.magnitude(rotMoveDir) * getRotateFactor();
 
             if (angle) {
                 Vec3.sub(_eye, camera.position, camera.target);
@@ -306,7 +319,10 @@ namespace TrackballControls {
         /** Update the object's position, direction and up vectors */
         function update(t: number) {
             if (lastUpdated === t) return;
-            if (p.spin && lastUpdated > 0) spin(t - lastUpdated);
+            if (lastUpdated > 0) {
+                if (p.animate.name === 'spin') spin(t - lastUpdated);
+                else if (p.animate.name === 'rock') rock(t - lastUpdated);
+            }
 
             Vec3.sub(_eye, camera.position, camera.target);
 
@@ -345,6 +361,7 @@ namespace TrackballControls {
             if (!isStart && !_isInteracting) return;
 
             _isInteracting = true;
+            resetRock(); // start rocking from the center after interactions
 
             const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
             const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
@@ -434,11 +451,34 @@ namespace TrackballControls {
 
         const _spinSpeed = Vec2.create(0.005, 0);
         function spin(deltaT: number) {
-            if (p.spinSpeed === 0) return;
+            if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
 
-            const frameSpeed = (p.spinSpeed || 0) / 1000;
+            const frameSpeed = p.animate.params.speed / 1000;
             _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
-            if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
+            Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
+        }
+
+        let _rockPhase = 0;
+        const _rockSpeed = Vec2.create(0.005, 0);
+        function rock(deltaT: number) {
+            if (p.animate.name !== 'rock' || p.animate.params.speed === 0 || _isInteracting) return;
+
+            const dt = deltaT / 1000 * p.animate.params.speed;
+            const maxAngle = degToRad(p.animate.params.angle) / getRotateFactor();
+            const angleA = Math.sin(_rockPhase * Math.PI * 2) * maxAngle;
+            const angleB = Math.sin((_rockPhase + dt) * Math.PI * 2) * maxAngle;
+
+            _rockSpeed[0] = angleB - angleA;
+            Vec2.add(_rotCurr, _rotPrev, _rockSpeed);
+
+            _rockPhase += dt;
+            if (_rockPhase >= 1) {
+                _rockPhase = 0;
+            }
+        }
+
+        function resetRock() {
+            _rockPhase = 0;
         }
 
         function start(t: number) {
@@ -448,9 +488,13 @@ namespace TrackballControls {
 
         return {
             viewport,
+            get isAnimating() { return p.animate.name !== 'off'; },
 
             get props() { return p as Readonly<TrackballControlsProps>; },
             setProps: (props: Partial<TrackballControlsProps>) => {
+                if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
+                    resetRock(); // start rocking from the center
+                }
                 Object.assign(p, props);
             },
 

+ 62 - 0
src/mol-plugin-state/animation/built-in/camera-rock.ts

@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Camera } from '../../../mol-canvas3d/camera';
+import { clamp } from '../../../mol-math/interpolate';
+import { Quat } from '../../../mol-math/linear-algebra/3d/quat';
+import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
+import { degToRad } from '../../../mol-math/misc';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { PluginStateAnimation } from '../model';
+
+const _dir = Vec3(), _axis = Vec3(), _rot = Quat();
+
+type State = { snapshot: Camera.Snapshot };
+
+export const AnimateCameraRock = PluginStateAnimation.create({
+    name: 'built-in.animate-camera-rock',
+    display: { name: 'Camera Rock', description: 'Rock the 3D scene around the x-axis in view space' },
+    isExportable: true,
+    params: () => ({
+        durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
+        speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to rock from side to side.' }),
+        angle: PD.Numeric(10, { min: 0, max: 180, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
+    }),
+    initialState: (p, ctx) => ({ snapshot: ctx.canvas3d!.camera.getSnapshot() }) as State,
+    getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
+    teardown: (_, state: State, ctx) => {
+        ctx.canvas3d?.requestCameraReset({ snapshot: state.snapshot, durationMs: 0 });
+    },
+
+    async apply(animState: State, t, ctx) {
+        if (t.current === 0) {
+            return { kind: 'next', state: animState };
+        }
+
+        const snapshot = animState.snapshot;
+        if (snapshot.radiusMax < 0.0001) {
+            return { kind: 'finished' };
+        }
+
+        const phase = t.animation
+            ? t.animation?.currentFrame / (t.animation.frameCount + 1)
+            : clamp(t.current / ctx.params.durationInMs, 0, 1);
+        const angle = Math.sin(phase * ctx.params.speed * Math.PI * 2) * degToRad(ctx.params.angle);
+
+        Vec3.sub(_dir, snapshot.position, snapshot.target);
+        Vec3.normalize(_axis, snapshot.up);
+        Quat.setAxisAngle(_rot, _axis, angle);
+        Vec3.transformQuat(_dir, _dir, _rot);
+        const position = Vec3.add(Vec3(), snapshot.target, _dir);
+        ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
+
+        if (phase >= 0.99999) {
+            return { kind: 'finished' };
+        }
+
+        return { kind: 'next', state: animState };
+    }
+});

+ 3 - 3
src/mol-plugin-state/animation/built-in/camera-spin.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
@@ -17,11 +17,11 @@ type State = { snapshot: Camera.Snapshot };
 
 export const AnimateCameraSpin = PluginStateAnimation.create({
     name: 'built-in.animate-camera-spin',
-    display: { name: 'Camera Spin' },
+    display: { name: 'Camera Spin', description: 'Spin the 3D scene around the x-axis in view space' },
     isExportable: true,
     params: () => ({
         durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
-        speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to spin in the specified dutation.' }),
+        speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to spin in the specified duration.' }),
         direction: PD.Select<'cw' | 'ccw'>('cw', [['cw', 'Clockwise'], ['ccw', 'Counter Clockwise']], { cycle: true })
     }),
     initialState: (_, ctx) => ({ snapshot: ctx.canvas3d?.camera.getSnapshot()! }) as State,

+ 3 - 3
src/mol-plugin-state/manager/snapshots.ts

@@ -84,7 +84,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
         const from = this.getIndex(e);
         let to = (from + dir) % len;
         if (to < 0) to += len;
-        const f = this.state.entries.get(to);
+        const f = this.state.entries.get(to)!;
 
         const entries = this.state.entries.asMutable();
         entries.set(to, e);
@@ -115,7 +115,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
         if (!id) {
             if (len === 0) return void 0;
             const idx = dir === -1 ? len - 1 : 0;
-            return this.state.entries.get(idx).snapshot.id;
+            return this.state.entries.get(idx)!.snapshot.id;
         }
 
         const e = this.getEntry(id);
@@ -126,7 +126,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
         idx = (idx + dir) % len;
         if (idx < 0) idx += len;
 
-        return this.state.entries.get(idx).snapshot.id;
+        return this.state.entries.get(idx)!.snapshot.id;
     }
 
     async setStateSnapshot(snapshot: PluginStateSnapshotManager.StateSnapshot): Promise<PluginState.Snapshot | undefined> {

+ 2 - 2
src/mol-plugin-state/transforms/model.ts

@@ -918,7 +918,7 @@ async function attachModelProps(model: Model, ctx: PluginContext, taskCtx: Runti
     const propertyCtx = { runtime: taskCtx, assetManager: ctx.managers.asset };
     const { autoAttach, properties } = params;
     for (const name of Object.keys(properties)) {
-        const property = ctx.customModelProperties.get(name);
+        const property = ctx.customModelProperties.get(name)!;
         const props = properties[name];
         if (autoAttach.includes(name) || property.isHidden) {
             try {
@@ -973,7 +973,7 @@ async function attachStructureProps(structure: Structure, ctx: PluginContext, ta
     const propertyCtx = { runtime: taskCtx, assetManager: ctx.managers.asset };
     const { autoAttach, properties } = params;
     for (const name of Object.keys(properties)) {
-        const property = ctx.customStructureProperties.get(name);
+        const property = ctx.customStructureProperties.get(name)!;
         const props = properties[name];
         if (autoAttach.includes(name) || property.isHidden) {
             try {

+ 2 - 2
src/mol-plugin-ui/controls.tsx

@@ -125,7 +125,7 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
         } else if (e.keyCode === 38 || e.key === 'ArrowUp') {
             if (snapshots.state.isPlaying) snapshots.stop();
             if (snapshots.state.entries.size === 0) return;
-            const e = snapshots.state.entries.get(0);
+            const e = snapshots.state.entries.get(0)!;
             this.update(e.snapshot.id);
         } else if (e.keyCode === 39 || e.key === 'ArrowRight') {
             if (snapshots.state.isPlaying) snapshots.stop();
@@ -133,7 +133,7 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
         } else if (e.keyCode === 40 || e.key === 'ArrowDown') {
             if (snapshots.state.isPlaying) snapshots.stop();
             if (snapshots.state.entries.size === 0) return;
-            const e = snapshots.state.entries.get(snapshots.state.entries.size - 1);
+            const e = snapshots.state.entries.get(snapshots.state.entries.size - 1)!;
             this.update(e.snapshot.id);
         }
     };

+ 4 - 8
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -46,10 +46,7 @@ const LayoutOptions = {
 type LayoutOptions = keyof typeof LayoutOptions
 
 const SimpleSettingsParams = {
-    spin: PD.Group({
-        spin: Canvas3DParams.trackball.params.spin,
-        speed: Canvas3DParams.trackball.params.spinSpeed
-    }, { pivot: 'spin' }),
+    animate: Canvas3DParams.trackball.params.animate,
     camera: Canvas3DParams.camera,
     background: PD.Group({
         color: PD.Color(Color(0xFCFBF9), { label: 'Background', description: 'Custom background color' }),
@@ -96,7 +93,7 @@ const SimpleSettingsMapping = ParamMapping({
 
         return {
             layout: props.layout,
-            spin: { spin: !!canvas.trackball.spin, speed: canvas.trackball.spinSpeed },
+            animate: canvas.trackball.animate,
             camera: canvas.camera,
             background: {
                 color: renderer.backgroundColor,
@@ -114,8 +111,7 @@ const SimpleSettingsMapping = ParamMapping({
     },
     update(s, props) {
         const canvas = props.canvas as Mutable<Canvas3DProps>;
-        canvas.trackball.spin = s.spin.spin;
-        canvas.trackball.spinSpeed = s.spin.speed;
+        canvas.trackball.animate = s.animate;
         canvas.camera = s.camera;
         canvas.transparentBackground = s.background.transparent;
         canvas.renderer.backgroundColor = s.background.color;

+ 3 - 1
src/mol-plugin/spec.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 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>
@@ -23,6 +23,7 @@ import { StateTransforms } from '../mol-plugin-state/transforms';
 import { BoxifyVolumeStreaming, CreateVolumeStreamingBehavior, InitVolumeStreaming } from '../mol-plugin/behavior/dynamic/volume-streaming/transformers';
 import { AnimateStateInterpolation } from '../mol-plugin-state/animation/built-in/state-interpolation';
 import { AnimateStructureSpin } from '../mol-plugin-state/animation/built-in/spin-structure';
+import { AnimateCameraRock } from '../mol-plugin-state/animation/built-in/camera-rock';
 
 export { PluginSpec };
 
@@ -131,6 +132,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
     animations: [
         AnimateModelIndex,
         AnimateCameraSpin,
+        AnimateCameraRock,
         AnimateStateSnapshots,
         AnimateAssemblyUnwind,
         AnimateStructureSpin,

+ 1 - 1
src/mol-plugin/util/toast.ts

@@ -72,7 +72,7 @@ export class PluginToastManager extends StatefulPluginComponent<{
 
         if (delay < 0) delay = 500;
         return <number><any>setTimeout(() => {
-            const e = this.state.entries.get(id);
+            const e = this.state.entries.get(id)!;
             e.timeout = void 0;
             this.hide(e);
         }, delay);

+ 30 - 8
src/mol-state/tree/immutable.ts

@@ -34,19 +34,38 @@ namespace StateTree {
         readonly forEach: OrderedSet<Ref>['forEach'],
         readonly map: OrderedSet<Ref>['map'],
         toArray(): Ref[],
-        first(): Ref
+        first(): Ref,
+        asMutable(): MutableChildSet
     }
 
+    export interface MutableChildSet extends ChildSet {
+        add(ref: Ref): MutableChildSet,
+        remove(ref: Ref): MutableChildSet,
+        asImmutable(): ChildSet
+    }
+
+
     interface _Map<T> {
         readonly size: number,
         has(ref: Ref): boolean,
-        get(ref: Ref): T
+        get(ref: Ref): T,
+        asImmutable(): _Map<T>,
+        asMutable(): MutableMap<T>
+    }
+
+    export interface MutableMap<T> extends _Map<T> {
+        set(ref: Ref, value: T): MutableMap<T>,
+        delete(ref: Ref): MutableMap<T>
     }
 
     export interface Transforms extends _Map<StateTransform> {}
     export interface Children extends _Map<ChildSet> { }
     export interface Dependencies extends _Map<ChildSet> { }
 
+    export interface MutableTransforms extends MutableMap<StateTransform> {}
+    export interface MutableChildren extends MutableMap<MutableChildSet> { }
+    export interface MutableDependencies extends MutableMap<MutableChildSet> { }
+
     class Impl implements StateTree {
         get root() { return this.transforms.get(StateTransform.RootRef)!; }
 
@@ -63,7 +82,10 @@ namespace StateTree {
      */
     export function createEmpty(customRoot?: StateTransform): StateTree {
         const root = customRoot || StateTransform.createRoot();
-        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap());
+        return create(
+            ImmutableMap([[root.ref, root] as [Ref, StateTransform]]) as Transforms,
+            ImmutableMap([[root.ref, OrderedSet()] as [Ref, ChildSet]]) as Children,
+            ImmutableMap() as Dependencies);
     }
 
     export function create(nodes: Transforms, children: Children, dependencies: Dependencies): StateTree {
@@ -148,13 +170,13 @@ namespace StateTree {
                 children.set(transform.ref, OrderedSet<Ref>().asMutable());
             }
 
-            if (transform.ref !== transform.parent) children.get(transform.parent).add(transform.ref);
+            if (transform.ref !== transform.parent) children.get(transform.parent)!.add(transform.ref);
         }
 
         const dependent = new Set<Ref>();
         for (const t of data.transforms) {
             const ref = t.ref;
-            children.set(ref, children.get(ref).asImmutable());
+            children.set(ref, children.get(ref)!.asImmutable());
 
             if (!t.dependsOn) continue;
 
@@ -163,16 +185,16 @@ namespace StateTree {
                 if (!dependencies.has(d)) {
                     dependencies.set(d, OrderedSet<Ref>([ref]).asMutable());
                 } else {
-                    dependencies.get(d).add(ref);
+                    dependencies.get(d)!.add(ref);
                 }
             }
         }
 
         dependent.forEach(d => {
-            dependencies.set(d, dependencies.get(d).asImmutable());
+            dependencies.set(d, dependencies.get(d)!.asImmutable());
         });
 
-        return create(nodes.asImmutable(), children.asImmutable(), dependencies.asImmutable());
+        return create(nodes.asImmutable() as Transforms, children.asImmutable() as Children, dependencies.asImmutable() as Dependencies);
     }
 
     export function dump(tree: StateTree) {

+ 6 - 7
src/mol-state/tree/transient.ts

@@ -13,16 +13,16 @@ import { arrayEqual } from '../../mol-util/array';
 export { TransientTree };
 
 class TransientTree implements StateTree {
-    transforms = this.tree.transforms as ImmutableMap<StateTransform.Ref, StateTransform>;
-    children = this.tree.children as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>;
-    dependencies = this.tree.dependencies as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>;
+    transforms = this.tree.transforms as StateTree.MutableTransforms;
+    children = this.tree.children as StateTree.MutableChildren;
+    dependencies = this.tree.dependencies as StateTree.MutableDependencies;
 
     private changedNodes = false;
     private changedChildren = false;
     private changedDependencies = false;
 
     private _childMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0;
-    private _dependencyMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0;
+    private _dependencyMutations: Map<StateTransform.Ref, StateTree.MutableChildSet> | undefined = void 0;
     private _stateUpdates: Set<StateTransform.Ref> | undefined = void 0;
 
     private get childMutations() {
@@ -99,12 +99,11 @@ class TransientTree implements StateTree {
     }
 
     private mutateDependency(parent: StateTransform.Ref, child: StateTransform.Ref, action: 'add' | 'remove') {
-        let set = this.dependencyMutations.get(parent);
+        let set: StateTree.MutableChildSet | undefined = this.dependencyMutations.get(parent);
 
         if (!set) {
             const src = this.dependencies.get(parent);
             if (!src && action === 'remove') return;
-
             this.changeDependencies();
             set = src ? src.asMutable() : OrderedSet<string>().asMutable();
             this.dependencyMutations.set(parent, set);
@@ -275,7 +274,7 @@ class TransientTree implements StateTree {
     asImmutable() {
         if (!this.changedNodes && !this.changedChildren && !this._childMutations) return this.tree;
         if (this._childMutations) this._childMutations.forEach(fixChildMutations, this.children);
-        if (this._dependencyMutations) this._dependencyMutations.forEach(fixDependencyMutations, this.dependencies);
+        if (this._dependencyMutations) this._dependencyMutations.forEach(fixDependencyMutations as any, this.dependencies);
         return StateTree.create(
             this.changedNodes ? this.transforms.asImmutable() : this.transforms,
             this.changedChildren ? this.children.asImmutable() : this.children,