Compare commits

...

26 Commits

Author SHA1 Message Date
dsehnal
a4b09d3a0c 5.0.0-dev.9 2025-08-25 17:00:54 +02:00
David Sehnal
6e488b0f80 MotM1 Story tweaks (#1627)
* tweak story

* bugfixes & tweaks

* linting

* support "discrete" scalar transform

* tweak audio path

* tweak ui
2025-08-25 08:22:36 +02:00
ludovic autin
6164281a50 initial work on MOM number 1 with audio comments (#1624)
* initial work on MOM number 1 with audio comments

* add some TODO comments

* separate audio as mp3. Do coloring with DG scheme

* move audio in root example folder. test some query in mol-script

* salt bridge.

* better coloring

* support for entry-id test in MolScriptBuilder.

* lint

* update audio, sync animation

* cleanup

* clean up and sync audio/anim

* add reference to MOM1
2025-08-25 07:14:46 +02:00
Alexander Rose
2db7171e2a Merge pull request #1625 from molstar/trackball-state-tweaks
Trackball & Snapshot handling tweaks
2025-08-24 13:42:24 -07:00
Alexander Rose
edfc094952 re-add the !isBusy check
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2025-08-24 13:35:37 -07:00
David Sehnal
b3e1e2900b Fix Markdown Commands query focus (#1626) 2025-08-24 13:18:36 +02:00
Alexander Rose
1e498d535a add SnapshotControls behavior 2025-08-23 10:44:50 -07:00
Alexander Rose
6ed969cd1b don't save behaviors in me snapshots 2025-08-23 10:42:05 -07:00
Alexander Rose
27bb4f4bca remove unused trackball param 2025-08-23 10:41:21 -07:00
Alexander Rose
6ce2139272 add canvas3d/trackball attribs
- attribs are configurable but are not saved in the state like props
2025-08-23 10:41:04 -07:00
dsehnal
13cf6613a6 fix typo 2025-08-23 19:18:21 +02:00
David Sehnal
c5bb13e295 Execute markdown commands on snapshot load (#1622) 2025-08-22 18:25:24 +02:00
dsehnal
34c8257848 5.0.0-dev.8 2025-08-22 16:05:31 +02:00
David Sehnal
fcbf39c935 Markdown & MVS: Audio support (#1621)
* audio playback markdown commands

* trigger markdown commands from MVS primitives

* docs

* fix usage of mutative
2025-08-22 16:00:30 +02:00
dsehnal
46c8150b2b 5.0.0-dev.7 2025-08-21 14:05:04 +02:00
David Sehnal
af1a864daa replace immer with mutative, optimize MVS animation loading (#1618) 2025-08-21 14:03:22 +02:00
David Sehnal
3babd9399a MVS: Add backbone and line representation types (#1617) 2025-08-20 17:33:03 +02:00
David Sehnal
e57564486f mol-state: fix param normalization (#1616)
* mol-state: fix param normalization

* headers
2025-08-19 19:33:01 +02:00
midlik
464a91ac29 Add PluginConfig.Viewport.ShowReset (#1615)
* Add PluginConfig.Viewport.ShowReset

* Update CHANGELOG
2025-08-18 12:02:31 +02:00
Alexander Rose
27fa50a5de Merge pull request #1610 from giagitom/dpoit-monolayer-transparency
Add Monolayer transparency (exploiting dpoit)
2025-08-16 14:37:34 -07:00
David Sehnal
1e323f18f7 MVS: additional formats and trajectory support (#1613)
* MVS: additional formats and trajectory support

* refactoring

* support lammpstrj

* tweaks

* remove test code
2025-08-15 20:10:13 +02:00
midlik
2685b2b77d MVSX use Murmur hash (#1612) 2025-08-15 14:11:53 +02:00
David Sehnal
d71b47a515 MVS Stories and related updates (#1609)
* mvs-stories updates, better snapshot playback if transition is present

* Add createMVSX

* mvs: support trackball animation

* tweak

* more fine grained speed of camera spin animation

* update TrackballControlsParams.animate.spin.speed

* story-session-url arg support
2025-08-15 13:27:34 +02:00
giagitom
88cc720dd2 fix render without postprocessing 2025-08-14 23:34:45 +02:00
giagitom
201433cc91 Lint fix 2025-08-14 19:53:36 +02:00
giagitom
8582303491 Add Monolayer transparency (exploiting dpoit) 2025-08-14 19:40:24 +02:00
71 changed files with 2070 additions and 247 deletions

View File

@@ -12,6 +12,7 @@ Note that since we don't clearly distinguish between a public and private interf
- [Breaking] Add `Volume.instances` support and a `VolumeInstances` transform to dynamically assign it
- This change is breaking because all volume objects require the `instances` field now.
- [Breaking] `Canvas3D.identify` now expects `Vec2` or `Ray3D`
- [Breaking] `TrackballControlsParams.animate.spin.speed` now means "Number of rotations per second" instead of "radians per second"
- Update production build to use `esbuild`
- Emit explicit paths in `import`s in `lib/`
- Fix outlines on opaque elements using illumination mode
@@ -19,9 +20,10 @@ Note that since we don't clearly distinguish between a public and private interf
- MolViewSpec extension:
- Generic color schemes (`palette` parameter for color_from_* nodes)
- Annotation field remapping (`field_remapping` parameter for color_from_* nodes)
- Representation node: support custom property `molstar_reprepresentation_params`
- Primitives node: support custom property `molstar_mesh/label/line_params`
- Canvas node: support custom property `molstar_postprocessing` with the ability to customize outline, depth of field, bloom, shadow, occlusion (SSAO), and fog
- `representation` node: support custom property `molstar_representation_params`
- Add `backbone` and `line` representation types
- `primitives` node: support custom property `molstar_mesh/label/line_params`
- `canvas` node: support custom property `molstar_postprocessing` with the ability to customize outline, depth of field, bloom, shadow, occlusion (SSAO), and fog
- `clip` node support for structure and volume representations
- `grid_slice` representation support for volumes
- Support tethers and background for primitive labels
@@ -34,14 +36,23 @@ Note that since we don't clearly distinguish between a public and private interf
- Support transforming and instancing of structures, components, and volumes
- Use params hash for node version for more performant tree diffs
- Add `Snapshot.animation` support that enables animating almost every property in a given tree
- Add `createMVSX` helper function
- Support Mol* trackball animation via `animation.custom.molstar_trackball`
- MVSX - use Murmur hash instead of FNV in archive URI
- Support additional file formats (pdbqt, gro, xyz, mol, sdf, mol2, xtc, lammpstrj)
- Support loading trajectory coordinates from separate nodes
- Trigger markdown commands from primitives using `molstar_markdown_commands` custom extensions
- Support `molstar_on_load_markdown_commands` custom state on the `root` node
- Added new color schemes, synchronized with D3.js ('inferno', 'magma', 'turbo', 'rainbow', 'sinebow', 'warm', 'cool', 'cubehelix-default', 'category-10', 'observable-10', 'tableau-10')
- Snapshot Markdown improvements
- Add `MarkdownExtensionManager` (`PluginContext.managers.markdownExtensions`)
- Support custom markdown commands to control the plugin via the `[link](!command)` pattern
- Support rendering custom elements via the `![alt](!parameters)` pattern
- Support tables
- Support loading images from MVSX files
- Support loading images and audio from MVSX files
- Indicate external links with ⤴
- Audio support
- Add `PluginState.Snapshot.onLoadMarkdownCommands`
- Avoid calculating rings for coarse-grained structures
- Fix isosurface compute shader normals when transformation matrix is applied to volume
- Symmetry operator naming for spacegroup symmetry - parenthesize multi-character indices (1_111-1 -> 1_(11)1(-1))
@@ -57,7 +68,10 @@ Note that since we don't clearly distinguish between a public and private interf
- Add `StructureInstances` transform
- `mvs-stories` app
- Add `story-id` URL arg support
- Add `story-session-url` URL arg support
- Add "Download MVS State" link
- Add "Open in Mol*" link
- Add "Edit in MolViewStories" link for story states
- Add ray-based picking
- Render narrow view of scene scene from ray origin & direction to a few pixel sized viewport
- Cast ray on every input as opposed to the standard "whole screen" picking
@@ -81,6 +95,10 @@ Note that since we don't clearly distinguish between a public and private interf
- Add `Hsl` and (normalized) `Rgb` color spaces
- Add `Color.interpolateHsl`
- Add `rotationCenter` property to `TransformParam`
- Add Monolayer transparency (exploiting dpoit).
- Add plugin config item ShowReset (shows/hides "Reset Zoom" button)
- Fix transform params not being normalized when used together with param hash version
- Replace `immer` with `mutative`
## [v4.18.0] - 2025-06-08
- MolViewSpec extension:

View File

@@ -23,11 +23,12 @@ Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `en
- `focus-refs=ref1,ref2,...` - On click, focuses nodes with the provided refs
- `highlight-refs=ref1,ref2,...` - On mouse over, highlights the provided refs
- `query=...&lang=...&action=highlight,focus&focus-radius=...`
- `query` is an expression (e.g., `resn HEM` when using PyMol syntax)
- (optional) `lang` is one of `mol-script` (default), `pymol`, `vmd`, `jmol`
- (optional) `action` is an array of `highlight` (default), `focus` (multiple actions can be specified)
- (optional) `focus-radius` is extra distance applied when focusing the selection (default is `3`)
- Example: `[HEM](!query%3Dresn%20HEM%26lang%3Dpymol%26action%3Dhighlight%2Cfocus)` highlights or focuses the HEM residue (the command must be URL encoded because it contains spaces and possibly other special characters)
- `query` is an expression (e.g., `resn HEM` when using PyMol syntax)
- (optional) `lang` is one of `mol-script` (default), `pymol`, `vmd`, `jmol`
- (optional) `action` is an array of `highlight` (default), `focus` (multiple actions can be specified)
- (optional) `focus-radius` is extra distance applied when focusing the selection (default is `3`)
- Example: `[HEM](!query=resn%20HEM%26lang=pymol&action=highlight,focus)` highlights or focuses the HEM residue (the query must be URL encoded because it contains spaces and possibly other special characters)
- `play-audio=src`, `toggle-audio[=str]`, `stop-audio`, `pause-audio` - Audio playback support
## Custom Content
@@ -36,11 +37,11 @@ Extends Markdown Image syntax to support expressions of the form `![alt](!c1=v1&
### Built-in Custom Content
- `color-swatch=color` - Renders a box with the provided color
- Color palettes:
- `color-palette-name=name` - Renders a gradient with the provided named color palette (see `mol-util/color/lists.ts` for supported color schemes)
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
- `color-palette-discrete` - Renders discrete color list instead of interpolating
- `color-palette-name=name` - Renders a gradient with the provided named color palette (see `mol-util/color/lists.ts` for supported color schemes)
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
- `color-palette-discrete` - Renders discrete color list instead of interpolating
## Example

View File

@@ -39,6 +39,7 @@ export default defineConfig([{
"comma-spacing": "off",
"space-infix-ops": "off",
"comma-dangle": "off",
quotes: ["warn", "single", { "allowTemplateLiterals": true, "avoidEscape": true }],
eqeqeq: ["error", "smart"],
"import/order": "off",
"no-eval": "warn",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

24
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "molstar",
"version": "5.0.0-dev.6",
"version": "5.0.0-dev.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "molstar",
"version": "5.0.0-dev.6",
"version": "5.0.0-dev.9",
"license": "MIT",
"dependencies": {
"@types/argparse": "^2.0.17",
@@ -21,9 +21,9 @@
"cors": "^2.8.5",
"express": "^5.1.0",
"h264-mp4-encoder": "^1.0.12",
"immer": "^10.1.1",
"immutable": "^5.1.2",
"io-ts": "^2.2.22",
"mutative": "^1.2.0",
"node-fetch": "^2.7.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
@@ -5557,15 +5557,6 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
@@ -7990,6 +7981,15 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mutative": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mutative/-/mutative-1.2.0.tgz",
"integrity": "sha512-1muFw45Lwjso6TSBGiXfbjKS01fVSD/qaqBfTo/gXgp79e8KM4Sa1XP/S4iN2/DvSdIZgjFJI+JIhC7eKf3GTg==",
"license": "MIT",
"engines": {
"node": ">=14.0"
}
},
"node_modules/mylas": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.0.0-dev.6",
"version": "5.0.0-dev.9",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -166,9 +166,9 @@
"cors": "^2.8.5",
"express": "^5.1.0",
"h264-mp4-encoder": "^1.0.12",
"immer": "^10.1.1",
"immutable": "^5.1.2",
"io-ts": "^2.2.22",
"mutative": "^1.2.0",
"node-fetch": "^2.7.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",

View File

@@ -7,14 +7,14 @@
const git = require('simple-git');
const path = require('path');
const fs = require("fs");
const fse = require("fs-extra");
const fs = require('fs');
const fse = require('fs-extra');
const argparse = require('argparse');
const VERSION = require(path.resolve(__dirname, '../package.json')).version;
const MVS_STORIES_VERSION = require(path.resolve(__dirname, '../src/apps/mvs-stories/version.ts')).VERSION;
const remoteUrl = "https://github.com/molstar/molstar.github.io.git";
const remoteUrl = 'https://github.com/molstar/molstar.github.io.git';
const dataDir = path.resolve(__dirname, '../data/');
const buildDir = path.resolve(__dirname, '../build/');
const deployDir = path.resolve(__dirname, '../deploy/');

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -147,6 +147,7 @@ export class MesoscaleExplorer {
behaviors: [
PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
PluginSpec.Behavior(PluginBehaviors.Camera.CameraControls),
PluginSpec.Behavior(PluginBehaviors.State.SnapshotControls),
PluginSpec.Behavior(MesoFocusLoci),
PluginSpec.Behavior(MesoSelectLoci),
@@ -261,7 +262,6 @@ export class MesoscaleExplorer {
image: true,
componentManager: false,
structureSelection: true,
behavior: true,
});
plugin.managers.lociLabels.clearProviders();

View File

@@ -53,6 +53,10 @@
text-decoration: none;
}
#links .sep {
color: #aaa;
}
@media (orientation:portrait) {
#viewer {
position: absolute;
@@ -90,13 +94,16 @@
</div>
<div id="links">
<a href="#" id="mvs-data">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
<span id="open-in-stories"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a>&nbsp;<span class="sep"></span></span>
<span id="open-in-molstar"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a>&nbsp;<span class="sep"></span></span>
<a href="#" id="mvs-data" title="MolViewSpec State for this story. Can be opened in the Mol* app.">Download MVS</a> <span class="sep"></span> <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>
<script>
var urlParams = new URLSearchParams(window.location.search);
var storyId = urlParams.get('story-id');
var storyUrl = urlParams.get('story-url');
var storySessionUrl = urlParams.get('story-session-url');
var format = urlParams.get('data-format');
// For testing purposes:
@@ -104,12 +111,32 @@
// storyUrl = 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/kinase-story.mvsj';
// }
var molstarDataLink = storyUrl;
var editInStoriesUrl = undefined;
if (storyId) {
mvsStories.loadFromID(storyId, { format: format || 'mvsj', contextName: 'story1' });
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?published-session-id=' + storyId;
molstarDataLink = 'https://stories.molstar.org/api/story/' + storyId + '/data';
} else if (storyUrl) {
mvsStories.loadFromURL(storyUrl, { format: format || 'mvsj', contextName: 'story1' });
}
if (!editInStoriesUrl && storySessionUrl) {
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?session-url=' + encodeURIComponent(storySessionUrl);
}
if (molstarDataLink) {
var molstarLink = 'https://molstar.org/viewer?mvs-url=' + encodeURIComponent(molstarDataLink) + '&mvs-format=' + encodeURIComponent(format || 'mvsj');
document.getElementById('open-in-molstar-link').setAttribute('href', molstarLink);
document.getElementById('open-in-molstar').style.display = 'inline';
}
if (editInStoriesUrl) {
document.getElementById('open-in-stories-link').setAttribute('href', editInStoriesUrl);
document.getElementById('open-in-stories').style.display = 'inline';
}
document.getElementById('mvs-data').addEventListener('click', (e) => {
e.preventDefault();
mvsStories.downloadCurrentStory({ contextName: 'story1' });

View File

@@ -109,8 +109,10 @@ const DefaultViewerOptions = {
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
illumination: false,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
@@ -187,8 +189,10 @@ export class Viewer {
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
[PluginConfig.General.PowerPreference, o.powerPreference],
[PluginConfig.General.ResolutionMode, o.resolutionMode],
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowReset, o.viewportShowReset],
[PluginConfig.Viewport.ShowScreenshotControls, o.viewportShowScreenshotControls],
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],

View File

@@ -37,7 +37,14 @@ A story showcasing MolViewSpec animation capabilities.`,
});
prims.label({ text: 'Animation Demo', position: { label_asym_id: 'A' }, label_size: 10 });
const anim = builder.animation();
const anim = builder.animation({
custom: {
molstar_trackball: {
name: 'rock',
params: { speed: 1.5 },
}
}
});
anim.interpolate({
kind: 'scalar',
ref: 'prims-opacity',
@@ -175,7 +182,7 @@ A story showcasing MolViewSpec animation capabilities.`,
ref: 'repr',
type: 'ball_and_stick',
custom: {
molstar_reprepresentation_params: {
molstar_representation_params: {
emissive: 0,
}
}
@@ -202,7 +209,7 @@ A story showcasing MolViewSpec animation capabilities.`,
kind: 'scalar',
target_ref: 'repr',
duration_ms: 1000,
property: ['custom', 'molstar_reprepresentation_params', 'emissive'],
property: ['custom', 'molstar_representation_params', 'emissive'],
end: 0.2,
});

File diff suppressed because one or more lines are too long

View File

@@ -7,9 +7,13 @@
import { buildStory as kinase } from './kinase';
import { buildStory as tbp } from './tbp';
import { buildStory as animation } from './animation';
import { buildStory as audio } from './audio';
import { buildStory as motm1 } from './motm1';
export const Stories = [
{ id: 'kinase', name: 'BCR-ABL: A Kinase Out of Control', buildStory: kinase },
{ id: 'tata', name: 'TATA-Binding Protein and its Role in Transcription Initiation ', buildStory: tbp },
{ id: 'animation', name: 'Molecular Animation', buildStory: animation },
{ id: 'motm1', name: 'RCSB Molecule of the Month #1', buildStory: motm1 },
{ id: 'animation-example', name: 'Molecular Animation Example', buildStory: animation },
{ id: 'audio-example', name: 'Audio Playback Example', buildStory: audio },
] as const;

View File

@@ -0,0 +1,958 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Ludovic Autin <autin@scripps.edu>
*/
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
import { createMVSBuilder, Structure as MVSStructure, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
import { Mat4 } from '../../../mol-math/linear-algebra/3d/mat4';
import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
import { formatMolScript } from '../../../mol-script/language/expression-formatter';
// 1pmb->1mbn
const align = Mat4.fromArray(Mat4.zero(), [0.4634187130865737, -0.7131589697034304, 0.5259728687171936, 0, -0.22944227902330105, -0.6698811108214233, -0.7061273127008398, 0, 0.8559202154942049, 0.2065522332899299, -0.4740643150728161, 0, -52.54880970106205, 37.49099778180445, -6.133850309914719, 1], 0);
// 1mbo->1myf
const alignmbo = Mat4.fromArray(Mat4.zero(), [-0.8334619943964441, -0.512838061396133, -0.20576353166796402, 0, -0.20145089001561267, 0.628743285359846, -0.7510655776229758, 0, 0.5145474196737698, -0.5845332204089626, -0.6273453801378679, 0, 11.864847328611186, -1.5261713438028912, 23.638919347623467, 1], 0);
const ill_color = (color: string, carbonLightness: number) => ({
molstar_color_theme_name: 'illustrative',
molstar_color_theme_params: {
style: {
name: 'uniform',
params: {
value: decodeColor(color),
saturation: 0,
lightness: 0,
}
},
carbonLightness: carbonLightness // required parameter
}
});
const GColors2 = ill_color('#947c7c', 0.8);
/* from David Goodsell style
in his illustrate software
HETATM-H-------- 0,9999, 1.1,1.1,1.1, 0.0
HETATMH--------- 0,9999, 1.0,1.0,1.0, 0.0
ATOM -H-------- 0,9999, 1.0,1.0,1.0, 0.0
ATOM H--------- 0,9999, 1.0,1.0,1.0, 0.0
HETATM-----HOH-- 0,9999, 1.0,1.0,0.0, 0.0
ATOM -OD--ASP A 0,9999 1.00, 0.20, 0.20, 1.6
ATOM -OE--GLU A 0,9999 1.00, 0.20, 0.20, 1.6
ATOM -NZ--LYS A 0,9999 0.10, 0.70, 1.00, 1.6
ATOM -NH--ARG A 0,9999 0.10, 0.70, 1.00, 1.6
ATOM -NE--ARG A 0,9999 0.10, 0.70, 1.00, 1.6
ATOM -ND--HIS A 0,9999 0.10, 0.70, 1.00, 1.6
ATOM -NE--HIS A 0,9999 0.10, 0.70, 1.00, 1.6
ATOM -N------ A 0,9999 0.80, 0.90, 1.00, 1.5
ATOM -O------ A 0,9999 1.00, 0.80, 0.80, 1.5
ATOM -C------ A 0,9999 1.00, 1.00, 1.00, 1.6
ATOM -S------ A 0,9999 1.00, 0.90, 0.50, 1.8
HETATM-C------ - 0,9999 0.60, 0.90, 0.60, 1.5
HETATM-------- - 0,9999 0.40, 0.90, 0.40, 1.5
*/
const GColors3 = {
schema: 'all_atomic', // or maybe just 'atom'
category_name: 'atom_site',
field_name: 'type_symbol',
palette: {
kind: 'categorical',
// missing_color: ...
colors: {
'C': '#FFFFFF',
'N': '#CCE6FF',
'O': '#FFCCCC',
'S': '#FFE680',
}
}
} as unknown as MVSNodeParams<'color_from_source'>;
const audioPathBase = 'https://raw.githubusercontent.com/molstar/molstar/master';
// For local debug
// const audioPathBase = '';
const _Audio1 = audioPathBase + '/examples/audio/AudioMOM1_A.mp3';
const _Audio2 = audioPathBase + '/examples/audio/AudioMOM1_B.mp3';
const _Audio3 = audioPathBase + '/examples/audio/AudioMOM1_C.mp3';
const _Audio4 = audioPathBase + '/examples/audio/AudioMOM1_D.mp3';
const q = (expr: string, lang = 'pymol') =>
`!query=${encodeURIComponent(expr)}&lang=${lang}&action=highlight,focus`;
const description_intro = `
# Molecule of the Month: Myoglobin
A story based on the orginal [first Molecule of the Month](https://pdb101.rcsb.org/motm/1) made by David Goodsell in January 2000.
Basic controls for the audio comments:
[Play](${encodeURIComponent(`!play-audio=${_Audio1}`)})
[Pause](!pause-audio)
[Stop](!stop-audio)
Myoglobin was the first protein to have its atomic structure determined, revealing how it stores oxygen in muscle cells.
---
## The First Protein Structure
Any discussion of protein structure must necessarily begin with **myoglobin**, because it is where the science of protein structure began. After years of arduous work, *John Kendrew* and his coworkers determined the atomic structure of myoglobin, laying the foundation for an era of biological understanding.
You can take a close look at this protein structure yourself, in **PDB entry [1mbn](https://www.rcsb.org/structure/1MBN)**. You will be amazed—just like the world was in 1960—at the beautiful intricacy of this protein.
---
## Myoglobin and Muscles
[Myoglobin](!query%3Dchain%20A%26lang%3Dpymol%26action%3Dhighlight%2Cfocus) is a **small, bright red protein**. It is very common in muscle cells and gives meat much of its red color. Its job is to **store oxygen**, for use when muscles are hard at work.
To do this, it uses a special chemical tool to capture slippery oxygen molecules: a **[heme group](!query%3Dresn%20HEM%26lang%3Dpymol%26action%3Dhighlight%2Cfocus)**. Heme is a disk-shaped molecule with a hole in the center that is perfect for holding an iron ion. The iron then forms a strong interaction with the **[oxygen molecule](!query%3Dresn%20OH%26lang%3Dpymol%26action%3Dhighlight%2Cfocus)**. As you can see in the structure, the heme group is held tightly in a deep pocket on one side of the protein.
---
## Visualizing Protein Structure
When the structure of myoglobin was solved, it posed a great challenge. The structure is so complex that **new methods** needed to be developed to display and understand it.
- *John Kendrew* used a huge wire model to build the structure based on the experimental electron density.
- Then, the artist *Irving Geis* was employed to create a picture of myoglobin for a prominent article in *Scientific American*.
- Computer graphics were still many years in the future, so he created this illustration entirely by hand—one atom at a time.
You can learn more about the work of Irving Geis at the **[Geis Archive on PDB-101](https://pdb101.rcsb.org/learn/GeisArchive)**.
![Alt Text](https://cdn.rcsb.org/pdb101/motm/1/1-Myoglobin-geis-0218-myoglobin.png)
*Illustration of myoglobin by Irving Geis. You can learn more about this painting at the Geis Archive on PDB-101.
Used with permission from the Howard Hughes Medical Institute, Copyright 2015.*
`;
const query1 = MS.struct.generator.atomGroups({
'entity-test': MS.core.rel.eq([
MS.struct.atomProperty.core.modelEntryId(),
'1MBN'
])
});
const firstEntity1 = q(formatMolScript(query1), 'mol-script');
const query2 = MS.struct.generator.atomGroups({
'entity-test': MS.core.rel.eq([
MS.struct.atomProperty.core.modelEntryId(),
'1PMB'
])
});
const firstEntity2 = q(formatMolScript(query2), 'mol-script');
const query3 = MS.struct.generator.atomGroups({
'entity-test': MS.core.rel.eq([
MS.struct.atomProperty.core.modelEntryId(),
'1MBN'
]),
'residue-test': MS.core.set.has([
MS.set(12, 140, 87),
MS.struct.atomProperty.macromolecular.auth_seq_id()
])
});
const charged_residues = q(formatMolScript(query3), 'mol-script');
const description_p1 = `
# Myoglobin and Whales
Basic controls for the audio comments:
[Play](${encodeURIComponent(`!play-audio=${_Audio2}`)})
[Pause](!pause-audio)
[Stop](!stop-audio)
If you look at John Kendrew's PDB file, you'll notice that the myoglobin he used was taken
from sperm whale muscles. Whales and dolphin have a great need for myoglobin, so that they can
store extra oxygen for use in their deep undersea dives. Typically, they have about 30
times more than in animals that live on land. A recent study revealed that a few special
modifications are needed to make this possible.
Comparing [whale myoglobin](${firstEntity1})
(PDB entry [1mbn](https://www.rcsb.org/structure/1mbn)) with
[pig myoglobin](${firstEntity2})
(PDB entry [1pmb](https://www.rcsb.org/structure/1pmb)), we find that there are
several mutations that add [extra positively-charged
amino acids](${charged_residues}) to the surface. Marine animals typically have these extra charges on
the surface of their myoglobin
to help repel neighboring molecules and prevent aggregation when myoglobin is at
high concentrations.
`;
const description_p2 = `
# Oxygen Bound to Myoglobin
Basic controls for the audio comments:
[Play](${encodeURIComponent(`!play-audio=${_Audio3}`)})
[Pause](!pause-audio)
[Stop](!stop-audio)
A later structure of myoglobin, PDB entry [1mbo](https://www.rcsb.org/structure/1mbo),
shows that [oxygen](${q('index 1276+1277')}) binds to
the [iron](${q('index 1275')}) atom deep inside the protein.
So how does it get in and out? The
answer is that the structure in the PDB is only one snapshot of the
protein, caught when it is in a tightly-closed form. In reality,
myoglobin (and all other proteins) is constantly in motion, performing
small flexing and breathing motions (illustrated here by PDB entry [1myf](https://www.rcsb.org/structure/1myf)). So, temporary openings constantly
appear and disappear, allowing oxygen in and out.
`;
const description_p3 = `
# Molecule of the Month: Myoglobin
Basic controls for the audio comments:
[Play](${encodeURIComponent(`!play-audio=${_Audio1}`)})
[Pause](!pause-audio)
[Stop](!stop-audio)
The atomic structure of myoglobin revealed many of the basic principles
of protein structure and stability. For instance, the structure showed
that when the protein chain folds into a globular structure,
[carbon-rich amino acids](${q('resn ALA+VAL+LEU+ILE+MET+PHE+TRP+PRO')}) are sheltered
inside and charged amino acids [positively](${q('resn LYS+ARG+HIS')})
and [negatively](${q('resn GLU+ASP')}) are most often found on the surface,
occasionally forming [salt bridges](${q('chain A and resi 44+47+77+18')})
that pair two opposite charges (shown here with circles).
To explore some of these principles, eplxore freely in the interactive view.
# Topics for Further Discussion
You can use the sequence comparison tool to align the sequences of different
myoglobins, looking for mutations. For instance, [here is the alignment of whale and pig myoglobin](https://www.rcsb.org/alignment?request-body=eJyljrsOwjAMRf%2FFcxgqxNKN7kXsqKpC4pZIeZTEVamq%2FDtOGRAzm%2BPje3I3eM4YV6g3CBOZ4FMZI9IcfZ%2BQoVfYa0kS6kHahFmACp7wReXQBY1QwyRNXExCEOCQHkEX5qUrjNxBWjN64GSiOCtWI%2F9y2wA9xbU3fA1V21w4ndCiKjWKQKbVfeiZ0R3HUmhfVIKz%2Bvs8HXMWv75r2%2Fzn61gJg7F71y6%2FARZEZL8%3D&response-body=eJzlU11vmzAU%2FS88Q2RsjKFv7iCA7GRJQFRVVUU0uClSQjo%2BtkVV%2FvuuyUdTadMeurcJIXF97j33HN%2FLm1HVzzvj5s3o%2B6o0bgzlUaZoyaynwvUshznPll8UpVVgRUr1hAkrmWEabVd0fQv5X75OZjLMQuNgGlvVFZqq2FTreqvqbrndlQqSXouq%2BVG1CgqvMNW97HTLbmsNp5qiUW2%2F6YD44Q16NP2q6%2BFoCKGm2S8HkfbkdqpFqI1addWuHpq2%2B%2B0R5QA9qfWyVd%2BGA9uE2vI9pORwMD%2FyzSa3n%2BN7NN%2FlLi8eB92NWgOl9vDwF1YdVnWpfho3yDQ2ql53L0f%2BR%2FMTtaCta4q6fd4126I7a7FNdHp%2B%2BwUd0chxCXY9xjA2LTRiNkWMONT2TDSimDi%2Bz1yHQDaAmGCbeTbxXR25rodsG%2FvHiCGGEHE9D2vukUcpdgnBlEGAEfU9RrFPdKbDqOMT2x8yLYpH1MWOQxzP9U3CRi5lBCGKoKlFR77j2Rg5iNkgVw%2Bg326LZq%2BH165257X5Xmx62EFo68I97F%2F1PsK99apeKasqYUpVtzf0QlwyXeeSuZikwUfQ9y5gNrGGRoYefw1jr5fQtSp7tdQb3w73rxH9G2xUeUa1MI3Aq2XDEHUpzOxUoKPV7rtqirXSqQjmgdDjcctO0v%2B4ZJ%2FYsQs5lOYyDaPwbi5zGed3XOQhD3IexdE8SGSykGORxrMwk6EYB4uxiKXIQh5OBE%2FDQAoRR3mWy4zLiCcQiiiOAZZiJvk8jXkmYpHMEnEvw3GShjxJYuiTLuJZFIwjHvB5xCdTwWUoxwsRJJyLexHK6H4eDdP453YjmQYnu9P8LrqyG%2BZHu9HFrhjsgs9ru9OT3ejabvbBbn5ld57LeSo%2B2E1PdqfB5Gx3rO3%2BH5t9%2BAU7Kf5z&encoded=true) used to create the illustration in this column.
PDB entry [2jho](https://www.rcsb.org/structure/2jho) includes myoglobin poisoned by cyanide. Take a look and you'll see that the cyanide blocks the binding site for oxygen.
# References
- 1mbn: J. C. Kendrew, R. E. Dickerson, B. E. Strandberg, R. G. Hart, D. R. Davies, D. C. Phillips & V. C. Shore (1960) Structure of Myoglobin. Nature 185, 422-427.
- J. C. Kendrew (1961) The three-dimensional structure of a protein molecule. Scientific American 205(6), 96-110.
- 1mbo: S. E. Phillips (1980) Structure and refinement of oxymyoglobin at 1.6 A resolution. Journal of Molecular Biology 142, 531-554.
- 1pmb: S. J. Smerdon, T. J. Oldfield, E. J. Dodson, G. G. Dodson, R. E. Hubbard & A. J. Wilkinson (1990) Determination of the crystal structure of recombinant pig myoglobin by molecular replacement and its refinement. Acta Crystallographica B, 46, 370-377.
- 1myf: Osapay K, Theriault Y, Wright PE, Case DA. Solution structure of carbonmonoxy myoglobin determined from nuclear magnetic resonance distance and chemical shift constraints. J Mol Biol. 1994;244(2):183-197. doi:10.1006/jmbi.1994.1718
- S. Mirceta, A. V. Signore, J. M. Burns, A. R. Cossins, K. L. Campbell & M. Berenbrink (2013) Evolution of mammalian diving capacity traced by myoglobin net surface charge. Science 340, 1234192.
`;
const Steps = [
{
header: 'Molecule of the Month: Myoglobin',
key: 'intro',
description: description_intro,
linger_duration_ms: 45000,
transition_duration_ms: 500,
state: (): Root => {
const builder = createMVSBuilder();
// no outline here
builder.canvas({ custom: { molstar_postprocessing: { enable_outline: false } } });
const _1mbn = structure(builder, '1MBN');
_1mbn.component({ selector: 'ligand' })
.representation({ ref: 'ligand', type: 'ball_and_stick' })
.color({ color: 'orange' });
// FE and O should be spacefill
_1mbn.component({ selector: { auth_seq_id: 155, label_atom_id: 'F' } })
.representation({ type: 'spacefill' })
.color({ color: 'yellow' });
_1mbn.component({ selector: { auth_seq_id: 154 } })
.representation({ type: 'spacefill' })
.color({ color: 'blue' });
_1mbn.component({ selector: { auth_seq_id: 154 } })
.representation({ type: 'spacefill' })
.color({ color: 'blue' });
const chA = _1mbn.component({ selector: { label_asym_id: 'A' } });
chA.representation({ type: 'surface', surface_type: 'gaussian' })
.color({ color: '#ff0303' })
.opacity({ ref: 'surfopa', opacity: 0.0 });
chA.representation({ type: 'line' })
.color({ custom: { molstar_color_theme_name: 'element-symbol' } })
.opacity({ ref: 'lineopa', opacity: 0.0 });
chA.representation({ type: 'cartoon' })
.color({ custom: { molstar_color_theme_name: 'secondary-structure' } });
// whale
_1mbn.component({ selector: { label_asym_id: 'A' } })
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
.colorFromSource({
schema: 'all_atomic',
category_name: 'atom_site',
field_name: 'type_symbol',
palette: {
kind: 'categorical',
colors: {
'C': '#FFFFFF',
'N': '#CCE6FF',
'O': '#FFCCCC',
'S': '#FFE680',
}
}
}).opacity({ ref: 'cpkopa1', opacity: 0.0 });
_1mbn.component({ selector: { auth_seq_id: 155 } })
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
.color({ custom: GColors2 }).opacity({ ref: 'cpkopa2', opacity: 0.0 });
const prims = _1mbn.primitives({
ref: 'prims',
label_opacity: 1,
label_background_color: 'grey',
custom: {
molstar_markdown_commands: {
// 'apply-snapshot': 'interlude',
'play-audio': _Audio1,
}
}
});
prims.label({
text: 'Start Comments',
position: [13.5, 45.1, 7.7],
label_size: 5
});
addNextButton(builder, 'whale', [13.5, 0, 7.7]);
// doesnt work for first slide, but work afterward
builder.extendRootCustomState({
molstar_on_load_markdown_commands: {
'play-audio': _Audio1,
}
});
const anim = builder.animation(
{
custom: {
molstar_trackball: {
name: 'spin',
params: { speed: -0.05 },
}
}
}
);
anim.interpolate({
kind: 'scalar',
target_ref: 'lineopa',
duration_ms: 2000,
start_ms: 0,
property: 'opacity',
start: 0.0,
end: 1.0,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'ligand',
start_ms: 22000,
duration_ms: 10000,
frequency: 6,
alternate_direction: true,
property: ['custom', 'molstar_representation_params', 'emissive'],
end: 1.0,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'cpkopa1',
duration_ms: 5000,
start_ms: 40000,
property: 'opacity',
start: 0.0,
end: 1.0,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'cpkopa2',
duration_ms: 5000,
start_ms: 40000,
property: 'opacity',
start: 0.0,
end: 1.0,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'next',
duration_ms: 2000,
start_ms: 43000,
property: 'label_opacity',
start: 0.0,
end: 1.0,
});
return builder;
},
camera: {
position: [13.5, 21.1, 73.1],
target: [13.5, 21.1, 7.7],
up: [0, 1, 0],
} satisfies MVSNodeParams<'camera'>,
},
{
header: 'Myoglobin and Whales',
key: 'whale',
description: description_p1,
linger_duration_ms: 41000,
transition_duration_ms: 500,
state: (): Root => {
const builder = createMVSBuilder();
const _1mbn = structure(builder, '1mbn').transform({ ref: 'whalex', translation: [-30, 0, 0] });
// whale
_1mbn.component({ selector: { label_asym_id: 'A' } })
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
.colorFromSource({
schema: 'all_atomic', // or maybe just 'atom'
category_name: 'atom_site',
field_name: 'type_symbol',
palette: {
kind: 'categorical',
colors: {
'C': '#FFFFFF',
'N': '#CCE6FF',
'O': '#FFCCCC',
'S': '#FFE680',
}
}
});
_1mbn.component({ selector: { auth_seq_id: 155 } })
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
.color({ custom: GColors2 });
_1mbn.primitives({
ref: 'prims',
label_opacity: 1,
label_attachment: 'top-center',
label_show_tether: true,
label_tether_length: 1.0,
})
.label({
text: 'whale',
position: { label_asym_id: 'A', auth_seq_id: 8 },
label_size: 10
});
_1mbn.primitives({
ref: 'startres',
label_opacity: 0,
})
.label({
text: '★', label_offset: 4,
position: { label_asym_id: 'A', auth_seq_id: 12, atom_id: 96 }, label_size: 5
})
.label({
text: '★', label_offset: 4,
position: { label_asym_id: 'A', auth_seq_id: 140, auth_atom_id: 'NZ' }, label_size: 5
})
.label({
text: '★', label_offset: 4,
position: { label_asym_id: 'A', auth_seq_id: 87, auth_atom_id: 'NZ' }, label_size: 5
});
// the following doesnt work
const seld = _1mbn.component({
selector: [
{ label_asym_id: 'A', auth_seq_id: 12 },
{ label_asym_id: 'A', auth_seq_id: 140 },
{ label_asym_id: 'A', auth_seq_id: 87 }
]
});
seld.representation({ ref: 'scharged', type: 'surface', surface_type: 'gaussian', custom: { molstar_representation_params: { emissive: 0.0, ignoreLight: true } } })
.colorFromSource(GColors3);
// pig
const _1pmb = structure(builder, '1pmb').transform({ ref: 'pig', matrix: align });
_1pmb.component({ selector: { label_asym_id: 'A' } })
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
.colorFromSource(GColors3);
_1pmb.component({ selector: { label_asym_id: 'C', auth_seq_id: 154 } })
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
.color({ custom: GColors2 });
_1pmb.primitives({
ref: 'labelpig',
label_opacity: 1,
label_attachment: 'top-center',
label_show_tether: true,
label_tether_length: 1.0,
})
.label({
text: 'pig',
position: { label_asym_id: 'A', auth_seq_id: 8 },
label_size: 10
});
builder.extendRootCustomState({
molstar_on_load_markdown_commands: {
'play-audio': _Audio2,
}
});
const anim = builder.animation(
{
custom: {
molstar_trackball: {
name: 'spin',
params: { speed: -0.05 },
}
}
});
anim.interpolate({
kind: 'vec3',
target_ref: 'whalex',
duration_ms: 10000,
start_ms: 16000,
property: 'translation',
start: [-30, 0, 0],
end: [-60, 0, 0],
});
anim.interpolate({
kind: 'scalar',
target_ref: 'startres',
duration_ms: 1000,
start_ms: 20000,
property: 'label_opacity',
start: 0.0,
end: 1.0,
});
// pig appear at 18s
anim.interpolate({
kind: 'transform_matrix',
target_ref: 'pig',
duration_ms: 5000,
start_ms: 18000,
property: 'matrix',
translation_start: [-82.54880970106205, 37.49099778180445, -6.133850309914719],
translation_end: [-52.54880970106205, 37.49099778180445, -6.133850309914719],
});
anim.interpolate({
kind: 'scalar',
target_ref: 'labelpig',
duration_ms: 2000,
start_ms: 18000,
property: 'label_opacity',
start: 0.0,
end: 1.0,
});
addNextButton(builder, 'oxygen', [-18.9, 10, 7.3]);
anim.interpolate({
kind: 'scalar',
target_ref: 'next',
duration_ms: 2000,
start_ms: 38000,
property: 'label_opacity',
start: 0.0,
end: 1.0,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'scharged',
start_ms: 20000,
duration_ms: 6000,
frequency: 6,
alternate_direction: true,
property: ['custom', 'molstar_representation_params', 'emissive'],
start: 0.0,
end: 1.0,
});
return builder;
},
camera: {
position: [-14.6, 116.1, 66.5],
target: [-18.9, 21.1, 7.3],
up: [-0.0, 0.5, -0.8],
} satisfies MVSNodeParams<'camera'>,
},
{
header: 'Oxygen Bound',
key: 'oxygen',
description: description_p2,
linger_duration_ms: 18000,
transition_duration_ms: 500,
state: (): Root => {
const builder = createMVSBuilder();
// NMR 1MYF
// 1A6N unbound
// 1A6M bound
// series 2G0R
const _1mbo = structure(builder, '1mbo')
.transform({ matrix: alignmbo });
const _1myf = builder
.download({ url: pdbUrl('1myf') })
.parse({ format: 'bcif' })
.modelStructure({ ref: '1myf' });
const red1 = '#d3a4a6';
const red2 = '#d75354';
const blue1 = '#02d1d1';
_1myf.component({ selector: { label_asym_id: 'A' } })
.transform({ translation: [0, 0, 0] })
.representation({ type: 'spacefill' })
.color({ color: red1 })
.opacity({ ref: 'spo', opacity: 1.0 });
// OXYY
// should animate in-out in loop
_1mbo.component({ selector: { label_asym_id: 'C', auth_seq_id: 155 } })
.representation({ type: 'spacefill' })
.color({
custom: {
molstar_color_theme_name: 'element-symbol',
molstar_color_theme_params: {
carbonColor: {
name: 'uniform',
params: { value: decodeColor(red2) }
},
}
}
});
_1myf.component({ selector: { label_asym_id: 'A' } })
.representation({ type: 'backbone' })
.color({ color: red1 });
_1mbo.component({ selector: { label_asym_id: 'D', auth_seq_id: 555 } })
.representation({
ref: 'oxy', type: 'spacefill', custom: {
molstar_representation_params: {
emissive: 0.0
}
}
})
.color({ color: blue1 });
_1mbo.component({ selector: { label_asym_id: 'D', auth_seq_id: 555 } })
.transform({ ref: 'oxyy', translation: [0, 0, 0] })
.representation({ type: 'spacefill' })
.color({ color: blue1 })
.opacity({ ref: 'oxop', opacity: 0.0 });
builder.extendRootCustomState({
molstar_on_load_markdown_commands: {
'play-audio': _Audio3,
}
});
const anim = builder.animation(
{
custom: {
molstar_trackball: {
name: 'spin',
params: { speed: -0.05 },
}
}
});
anim.interpolate({
kind: 'scalar',
target_ref: 'spo',
duration_ms: 5000,
start_ms: 0,
property: 'opacity',
start: 1.0,
end: 0.05,
});
anim.interpolate({
kind: 'scalar',
target_ref: '1myf',
start_ms: 11000,
duration_ms: 10000,
frequency: 4,
alternate_direction: true,
property: 'model_index',
discrete: true,
start: 0,
end: 11,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'oxy',
start_ms: 3000,
duration_ms: 10000,
frequency: 7,
alternate_direction: true,
property: ['custom', 'molstar_representation_params', 'emissive'],
end: 1.0,
});
anim.interpolate({
kind: 'vec3',
target_ref: 'oxyy',
duration_ms: 5000,
start_ms: 16000,
property: 'translation',
frequency: 4,
alternate_direction: false,
start: [5, -5, -20],
end: [0, 0, 0],
noise_magnitude: 1,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'oxop',
duration_ms: 1000,
start_ms: 15000,
property: 'opacity',
start: 0.0,
end: 1.0,
});
addNextButton(builder, 'end', [0, -25, 0.0]);
anim.interpolate({
kind: 'scalar',
target_ref: 'next',
duration_ms: 2000,
start_ms: 18000,
property: 'label_opacity',
start: 0.0,
end: 1.0,
});
return builder;
},
camera: {
position: [-2.2, 0.7, -78.5],
target: [-0.1, 0.7, 0.6],
up: [0, 1, 0],
} satisfies MVSNodeParams<'camera'>,
},
{
header: 'Conclusion',
key: 'end',
description: description_p3,
linger_duration_ms: 20000,
transition_duration_ms: 500,
state: (): Root => {
const builder = createMVSBuilder();
const _1mbn = structure(builder, '1mbn');
// resn ALA+VAL+LEU+ILE+MET+PHE+TRP+PRO
const carb = ['ALA', 'VAL', 'LEU', 'ILE', 'MET', 'PHE', 'TRP', 'PRO'].map(amk => ({ label_comp_id: amk }));
// resn LYS+ARG+HIS+ASP+GLU
const chargedp = ['LYS', 'ARG', 'HIS'].map(amk => ({ label_comp_id: amk }));
const chargedn = ['ASP', 'GLU'].map(amk => ({ label_comp_id: amk }));
// salt bridge
// ASP44-OD1-356-LYS47-NZ-388
// LYS77-NZ-613-GLU18-OE1-149
// use primitve distance_measurement
// and ellipse or ellipsoid with transparancy
_1mbn.primitives({ ref: 'dist', label_opacity: 0.0 })
.distance({
start: { label_asym_id: 'A', auth_seq_id: 44, atom_id: 356 },
end: { label_asym_id: 'A', auth_seq_id: 47, atom_id: 388 },
radius: 0.1, dash_length: 0.1,
label_size: 2
})
.distance({
start: { label_asym_id: 'A', auth_seq_id: 77, atom_id: 613 },
end: { label_asym_id: 'A', auth_seq_id: 18, atom_id: 149 },
radius: 0.1, dash_length: 0.1,
label_size: 2
});
// 44 OD1 22.300 33.300 -6.200
// 47 NZ 23.200 32.000 -8.400
const r44 = Vec3.create(22.300, 33.300, -6.200);
const r47 = Vec3.create(23.200, 32.000, -8.400);
getEllipse(builder, r44, r47, 'salt1');
// 18 OE1 16.600 22.500 20.500
// 77 NZ 14.100 23.600 22.200
const r18 = Vec3.create(16.600, 22.500, 20.500);
const r77 = Vec3.create(14.100, 23.600, 22.200);
getEllipse(builder, r18, r77, 'salt2');
const a = _1mbn.component({ selector: carb });
a.representation({ type: 'ball_and_stick' })
.color({ color: '#bec0f2' })
.opacity({ ref: 'carb', opacity: 1.0 });
const b = _1mbn.component({ selector: chargedp });
b.representation({ type: 'ball_and_stick' })
.color({ custom: ill_color('blue', 3.0) })
.opacity({ ref: 'chargedp', opacity: 1.0 });
const c = _1mbn.component({ selector: chargedn });
c.representation({ type: 'ball_and_stick' })
.color({ custom: ill_color('red', 3.0) })
.opacity({ ref: 'chargedn', opacity: 1.0 });
_1mbn.component({ selector: { label_asym_id: 'A' } })
.representation({ type: 'backbone' })
.color({ color: '#919191' });
_1mbn.component({ selector: 'ligand' })
.representation({
ref: 'ligand', type: 'ball_and_stick',
custom: {
molstar_representation_params: {
emissive: 0.0
}
}
})
.color({ color: 'orange' });
builder.extendRootCustomState({
molstar_on_load_markdown_commands: {
'play-audio': _Audio4,
}
});
const anim = builder.animation({});
anim.interpolate({
kind: 'scalar',
target_ref: 'carb',
duration_ms: 2000,
start_ms: 8000,
frequency: 2,
alternate_direction: true,
property: 'opacity',
start: 0.0,
end: 1.0,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'chargedp',
duration_ms: 1000,
start_ms: 10000,
property: 'opacity',
start: 0.0,
end: 1.0,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'chargedn',
duration_ms: 1000,
start_ms: 10000,
property: 'opacity',
start: 0.0,
end: 1.0,
});
// show salt bridge
anim.interpolate({
kind: 'scalar',
target_ref: 'salt1',
duration_ms: 1000,
start_ms: 11000,
property: 'opacity',
start: 0.0,
end: 0.3,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'salt2',
duration_ms: 1000,
start_ms: 11000,
property: 'opacity',
start: 0.0,
end: 0.3,
});
anim.interpolate({
kind: 'scalar',
target_ref: 'dist',
duration_ms: 1000,
start_ms: 11000,
property: 'label_opacity',
start: 0.0,
end: 1.0,
});
addNextButton(builder, 'intro', [13.5, -10.0, 7.7]);
anim.interpolate({
kind: 'scalar',
target_ref: 'next',
duration_ms: 2000,
start_ms: 20000,
property: 'label_opacity',
start: 0.0,
end: 1.0,
});
return builder;
},
camera: {
position: [16.0, 47.2, 67.8],
target: [13.6, 21.1, 7.6],
up: [0.1, 0.9, -0.4],
} satisfies MVSNodeParams<'camera'>,
},
];
function addNextButton(builder: any, snapshotKey: string, position: [number, number, number]) {
builder.primitives({
ref: 'next',
tooltip: 'Click for next part',
label_opacity: 0,
label_background_color: 'grey',
snapshot_key: snapshotKey
})
.label({
ref: 'next_label',
position: position,
text: 'Click me to go next',
label_color: 'white',
label_size: 5
});
}
function structure(builder: Root, id: string): MVSStructure {
return builder
.download({ url: pdbUrl(id) })
.parse({ format: 'bcif' })
.modelStructure();
}
function getEllipse(builder: Root, pos1: Vec3, pos2: Vec3, ref: string) {
const center = Vec3.add(Vec3(), pos1, pos2);
Vec3.scale(center, center, 0.5);
const major_axis = Vec3.sub(Vec3(), pos2, pos1);
const z_axis = Vec3.create(0, 0, 1);
// cross to get minor
const minor_axis = Vec3.cross(Vec3(), major_axis, z_axis);
return builder.primitives({ ref: ref, opacity: 0.33 }).ellipsoid({
center: center as any,
major_axis: major_axis as any,
minor_axis: minor_axis as any,
radius: [5.0, 3.0, 3.0],
color: '#cccccc',
});
}
function pdbUrl(id: string) {
return `https://www.ebi.ac.uk/pdbe/entry-files/download/${id.toLowerCase()}.bcif`;
}
export function buildStory(): MVSData_States {
const snapshots = Steps.map((s, i) => {
const builder = s.state();
if (s.camera) builder.camera(s.camera);
const description = i > 0 ? `${s.description}\n\n[Go to start](#intro)` : s.description;
return builder.getSnapshot({
title: s.header,
key: s.key,
description,
description_format: 'markdown',
linger_duration_ms: s.linger_duration_ms ?? 500,
transition_duration_ms: s.transition_duration_ms ?? 1000,
});
});
return {
kind: 'multiple',
snapshots,
metadata: {
title: 'RCSB Molecule of the Month 1',
version: '1.0',
timestamp: new Date().toISOString(),
}
};
}

View File

@@ -7,6 +7,7 @@
import { Camera } from '../../mol-canvas3d/camera';
import { CameraFogParams, Canvas3DParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
import { TrackballControlsParams } from '../../mol-canvas3d/controls/trackball';
import { BloomParams } from '../../mol-canvas3d/passes/bloom';
import { DofParams } from '../../mol-canvas3d/passes/dof';
import { OutlineParams } from '../../mol-canvas3d/passes/outline';
@@ -24,6 +25,7 @@ import { ParamDefinition } from '../../mol-util/param-definition';
import { decodeColor } from './helpers/utils';
import { MolstarLoadingContext } from './load';
import { SnapshotMetadata } from './mvs-data';
import { MVSAnimationNode } from './tree/animation/animation-tree';
import { MolstarNode, MolstarNodeParams } from './tree/molstar/molstar-tree';
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
@@ -121,22 +123,17 @@ export function createPluginStateSnapshotCamera(plugin: PluginContext, context:
return camera;
}
/** Set canvas properties based on a canvas node. */
export function setCanvas(plugin: PluginContext, node: MolstarNode<'canvas'> | undefined) {
plugin.canvas3d?.setProps(old => modifyCanvasProps(old, node));
}
function optionalParams(enable: boolean | undefined, values: any, params: ParamDefinition.Params, fallback: any) {
if (typeof enable === 'boolean') {
return enable
? { name: 'on', params: { ...ParamDefinition.getDefaultValues(params), ...values } }
: { name: 'off', params: { } };
: { name: 'off', params: {} };
}
return fallback;
}
/** Create a deep copy of `oldCanvasProps` with values modified according to a canvas node params. */
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, custom?: Record<string, any>): Canvas3DProps {
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, animationNode: MVSAnimationNode<'animation'> | undefined): Canvas3DProps {
const params = canvasNode?.params;
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
@@ -160,6 +157,10 @@ export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: Mol
const bloom = molstar_postprocessing?.enable_bloom;
const bloomParams = molstar_postprocessing?.bloom_params;
const trackballAnimation = animationNode?.custom?.molstar_trackball;
const trackballAnimationName = trackballAnimation?.name;
const trackballAnimationParams = trackballAnimation?.params ?? {};
return {
...oldCanvasProps,
postprocessing: {
@@ -175,6 +176,21 @@ export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: Mol
...oldCanvasProps.renderer,
backgroundColor: backgroundColor,
},
trackball: {
...oldCanvasProps?.trackball,
...(trackballAnimationName
? {
animate: {
name: trackballAnimationName,
params: {
...TrackballControlsParams.animate.map(trackballAnimationName)?.defaultValue,
...trackballAnimationParams
}
}
}
: {}
),
}
};
}
@@ -191,5 +207,9 @@ export function resetCanvasProps(plugin: PluginContext) {
bloom: DefaultCanvas3DParams.postprocessing.bloom,
},
cameraFog: DefaultCanvas3DParams.cameraFog,
trackball: {
...old?.trackball,
animate: { name: 'off', params: {} },
}
});
}

View File

@@ -5,7 +5,7 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { hashFnv32a } from '../../../mol-data/util';
import { murmurHash3_128_fromBytes } from '../../../mol-data/util';
import { StringLike } from '../../../mol-io/common/string-like';
import { DataFormatProvider } from '../../../mol-plugin-state/formats/provider';
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
@@ -118,7 +118,7 @@ export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext
// states.
clearMVSXFileAssets(plugin);
const archiveId = `ni,fnv1a;${hashFnv32a(data)}`;
const archiveId = `ni,MurmurHash3_128;${murmurHash3_128_fromBytes(data, 42)}`;
let files: { [path: string]: Uint8Array };
try {
files = await unzip(runtimeCtx, data) as typeof files;
@@ -182,7 +182,7 @@ function tryGetDownloadUrl(pso: SO.Data.String, plugin: PluginContext): string |
}
/** Return a URI referencing a file within an archive, using ARCP scheme (https://arxiv.org/pdf/1809.06935.pdf).
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,fnv1a;938511930')
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,MurmurHash3_128;e6494f6be71f34c556f3de73d306780c')
* `path` corresponds to the path to a file within the archive */
function arcpUri(archiveId: string, path: string): string {
return new URL(path, `arcp://${archiveId}/`).href;

View File

@@ -149,6 +149,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
const context: PrimitiveBuilderContext = { ...a.data, structureRefs };
const snapshotKey = { snapshotKey: { ...SnapshotKey, defaultValue: a.data.options?.snapshot_key ?? '' } };
const markdownCommands = { markdownCommands: { ...MarkdownCommands, defaultValue: a.data.node?.custom?.molstar_markdown_commands } };
const label = capitalize(params.kind);
if (params.kind === 'mesh') {
@@ -161,6 +162,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
params: {
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, ...customMeshParams }),
...snapshotKey,
...markdownCommands,
},
getShape: (_, data, __, prev: any) => buildPrimitiveMesh(data, prev?.geometry),
geometryUtils: Mesh.Utils,
@@ -185,6 +187,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
...customLabelParams,
}),
...snapshotKey,
...markdownCommands,
},
getShape: (_, data, props, prev: any) => buildPrimitiveLabels(data, prev?.geometry, props),
geometryUtils: Text.Utils,
@@ -199,6 +202,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
params: {
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, ...customLineParams }),
...snapshotKey,
...markdownCommands,
},
getShape: (_, data, __, prev: any) => buildPrimitiveLines(data, prev?.geometry),
geometryUtils: Lines.Utils,
@@ -227,7 +231,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils);
await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
const pickable = !!(params as any).snapshotKey?.trim();
const pickable = !!(params as any).snapshotKey?.trim() || !!(params as any).markdownCommands;
if (pickable) {
repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
}
@@ -241,7 +245,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx);
b.data.sourceData = a.data;
const pickable = !!(newParams as any).snapshotKey?.trim();
const pickable = !!(newParams as any).snapshotKey?.trim() || !!(newParams as any).markdownCommands;
if (pickable) {
b.data.repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
}
@@ -252,6 +256,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
});
const SnapshotKey = PD.Text('', { isEssential: true, disableInteractiveUpdates: true, description: 'Activate the snapshot with the provided key when clicking on the label' });
const MarkdownCommands = PD.Value<any>(undefined, { isHidden: true });
/* **************************************************** */

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { getTrajectory } from '../../../mol-plugin-state/transforms/model';
import { Task } from '../../../mol-task';
import { ParamDefinition } from '../../../mol-util/param-definition';
import { getMVSReferenceObject } from '../helpers/utils';
import { MVSTransform } from './annotation-structure-component';
export const MVSTrajectoryWithCoordinates = MVSTransform({
name: 'trajectory-with-coordinates',
display: { name: 'Trajectory with Coordinates', description: 'Create a trajectory from existing model and the provided coordinates.' },
from: PluginStateObject.Molecule.Model,
to: PluginStateObject.Molecule.Trajectory,
params: {
coordinatesRef: ParamDefinition.Text('', { isHidden: true }),
}
})({
apply({ a, params, dependencies }) {
return Task.create('Create trajectory from model/topology and coordinates', async ctx => {
const coordinates = getMVSReferenceObject([PluginStateObject.Molecule.Coordinates], dependencies, params.coordinatesRef);
if (!coordinates) {
throw new Error('Coordinates not found.');
}
const trajectory = await getTrajectory(ctx, a, coordinates.data);
const props = { label: 'Trajectory', description: `${trajectory.frameCount} model${trajectory.frameCount === 1 ? '' : 's'}` };
return new PluginStateObject.Molecule.Trajectory(trajectory, props);
});
}
});

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Zip } from '../../mol-util/zip/zip';
import { MVSData } from './mvs-data';
/**
* Creates an MVSX zip file with from the provided data and assets
*/
export async function createMVSX(data: MVSData, assets: { name: string, content: string | Uint8Array }[]) {
const encoder = new TextEncoder();
const files: Record<string, Uint8Array> = {
'index.mvsj': encoder.encode(JSON.stringify(data)),
};
for (const asset of assets) {
files[asset.name] = typeof asset.content === 'string'
? encoder.encode(asset.content)
: asset.content;
}
const zip = await Zip(files).run();
return new Uint8Array(zip);
}

View File

@@ -5,7 +5,6 @@
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { produce } from 'immer';
import { Snapshot } from '../mvs-data';
import { Tree } from '../tree/generic/tree-schema';
import { clamp, lerp } from '../../../mol-math/interpolate';
@@ -20,9 +19,9 @@ import { makeContinuousPaletteCheckpoints, MVSContinuousPaletteProps, MVSDiscret
import { palettePropsFromMVSPalette } from '../load-helpers';
import { SortedArray } from '../../../mol-data/int';
import { ColorT } from '../tree/mvs/param-types';
import { produce } from '../../../mol-util/produce';
export async function generateStateTransition(ctx: RuntimeContext, snapshot: Snapshot) {
export async function generateStateTransition(ctx: RuntimeContext, snapshot: Snapshot, snapshotIndex: number, snapshotCount: number) {
if (!snapshot.animation) return undefined;
const tree = addDefaults(snapshot.animation, MVSAnimationSchema);
@@ -38,15 +37,16 @@ export async function generateStateTransition(ctx: RuntimeContext, snapshot: Sna
const dt = tree.params?.frame_time_ms ?? (1000 / 60);
const N = Math.ceil(duration / dt);
const nodeMap = makeNodeMap(snapshot.root, new Map(), []);
const cache = new Map<any, InterpolationCacheEntry>();
for (let i = 0; i <= N; i++) {
const t = i * dt;
const root = createSnapshot(snapshot.root, transitions, t, cache);
const root = createSnapshot(snapshot.root, transitions, t, cache, nodeMap);
frames.push(root);
if (ctx.shouldUpdate) {
await ctx.update({ message: 'Generating transition...', current: i + 1, max: N });
await ctx.update({ message: `Generating transition for snapshot ${snapshotIndex + 1}/${snapshotCount}`, current: i + 1, max: N });
}
}
@@ -80,12 +80,13 @@ interface InterpolationCacheEntry {
rotation?: { axis: Vec3, angle: number, start: Quat, end: Quat },
}
function createSnapshot(tree: MVSTree, transitions: MVSAnimationNode<'interpolate'>[], time: number, cache: Map<any, InterpolationCacheEntry>) {
function createSnapshot(tree: MVSTree, transitions: MVSAnimationNode<'interpolate'>[], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
return produce(tree, (draft) => {
for (const transition of transitions) {
const node = findNode(draft, transition.params.target_ref);
if (!node) continue;
const nodePath = nodeMap.get(transition.params.target_ref);
if (!nodePath) continue;
const node = select(draft, nodePath, 0);
const target = transition.params.property[0] === 'custom' ? node?.custom : node?.params;
if (!target) continue;
@@ -131,7 +132,7 @@ function createSnapshot(tree: MVSTree, transitions: MVSAnimationNode<'interpolat
let next: any;
if (transition.params.kind === 'scalar') {
next = interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0);
next = interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.discrete);
} else if (transition.params.kind === 'vec3') {
next = interpolateVectors(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.spherical);
} else if (transition.params.kind === 'rotation_matrix') {
@@ -215,18 +216,18 @@ function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, tar
assign(target, transition.params.property, result, offset);
}
function interpolateScalars(start: number | number[], end: number | number[] | undefined, t: number, noise: number) {
function interpolateScalars(start: number | number[], end: number | number[] | undefined, t: number, noise: number, discrete: boolean) {
if (Array.isArray(start)) {
const ret = Array.from<number>({ length: start.length }).fill(0.1);
if (!end || !Array.isArray(end)) {
for (let i = 0; i < start.length; i++) {
ret[i] = interpolateScalar(start[i], end, t, noise);
ret[i] = interpolateScalar(start[i], end, t, noise, discrete);
}
return ret;
}
for (let i = 0; i < start.length; i++) {
ret[i] = interpolateScalar(start[i], end[i], t, noise);
ret[i] = interpolateScalar(start[i], end[i], t, noise, discrete);
}
return ret;
}
@@ -234,19 +235,22 @@ function interpolateScalars(start: number | number[], end: number | number[] | u
if (Array.isArray(end)) {
const ret = Array.from<number>({ length: end.length }).fill(0.1);
for (let i = 0; i < end.length; i++) {
ret[i] = interpolateScalar(start, end[i], t, noise);
ret[i] = interpolateScalar(start, end[i], t, noise, discrete);
}
return ret;
}
return interpolateScalar(start, end, t, noise);
return interpolateScalar(start, end, t, noise, discrete);
}
function interpolateScalar(start: number, end: number | undefined, t: number, noise: number) {
function interpolateScalar(start: number, end: number | undefined, t: number, noise: number, discrete: boolean) {
let v = typeof end === 'number' ? lerp(start, end, t) : start;
if (noise) {
v += (Math.random() - 0.5) * noise;
}
if (discrete) {
v = Math.round(v);
}
return v;
}
@@ -380,14 +384,23 @@ function assign(params: any, path: string | (string | number)[], value: any, off
}
}
function findNode(tree: Tree, ref: string): Tree | undefined {
if (tree.ref === ref) return tree;
if (!tree.children) return undefined;
for (const child of tree.children) {
const result = findNode(child, ref);
if (result) return result;
function makeNodeMap(tree: Tree, map: Map<string, (string | number)[]>, currentPath: (string | number)[]) {
if (tree.ref) {
map.set(tree.ref, [...currentPath]);
}
return undefined;
if (!tree.children) return map;
currentPath.push('children');
for (let i = 0; i < tree.children.length; i++) {
const child = tree.children[i];
currentPath.push(i);
makeNodeMap(child, map, currentPath);
currentPath.pop();
}
currentPath.pop();
return map;
}
function makePaletteFunction(props: MVSAnimationNode<'interpolate'>, start: ColorT | undefined | null, end: ColorT | undefined | null): ((value: number) => Color) | undefined {

View File

@@ -152,4 +152,25 @@ export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], depe
}
return ret;
}
export function getMVSReferenceObject<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject> | undefined, ref: string): StateObject | undefined {
if (!dependencies) return undefined;
for (const key of Object.keys(dependencies)) {
const o = dependencies[key];
let okType = false;
for (const t of type) {
if (t.is(o)) {
okType = true;
break;
}
}
if (!okType || !o.tags) continue;
for (const tag of o.tags) {
if (tag.startsWith('mvs-ref:')) {
if (tag.substring(8) === ref) return o;
}
}
}
}

View File

@@ -369,10 +369,20 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
type: { name: 'cartoon', params: { alpha, tubularHelices: params.tubular_helices } },
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
};
case 'backbone':
return {
type: { name: 'backbone', params: { alpha } },
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
};
case 'ball_and_stick':
return {
type: { name: 'ball-and-stick', params: { sizeFactor: (params.size_factor ?? 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
};
case 'line':
return {
type: { name: 'line', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
};
case 'spacefill':
return {
type: { name: 'spacefill', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
@@ -402,8 +412,8 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
if (clip) {
base.type!.params = { ...base.type?.params, clip };
}
if (node.custom?.molstar_reprepresentation_params) {
base.type!.params = { ...base.type!.params, ...node.custom.molstar_reprepresentation_params };
if (node.custom?.molstar_representation_params) {
base.type!.params = { ...base.type!.params, ...node.custom.molstar_representation_params };
}
return base;
}

View File

@@ -8,8 +8,8 @@
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { Download, ParseCcp4, ParseCif } from '../../mol-plugin-state/transforms/data';
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB } from '../../mol-plugin-state/transforms/model';
import { Download, ParseCif, ParseCcp4 } from '../../mol-plugin-state/transforms/data';
import { CoordinatesFromLammpstraj, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { VolumeFromCcp4, VolumeFromDensityServerCif } from '../../mol-plugin-state/transforms/volume';
import { PluginCommands } from '../../mol-plugin/commands';
@@ -26,16 +26,18 @@ import { CustomLabelProps, CustomLabelRepresentationProvider } from './component
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-prop';
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData, MVSShapeRepresentation3D } from './components/primitives';
import { MVSTrajectoryWithCoordinates } from './components/trajectory';
import { generateStateTransition } from './helpers/animation';
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
import { MVSAnimationNode } from './tree/animation/animation-tree';
import { validateTree } from './tree/generic/tree-schema';
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
import { type MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
export interface MVSLoadOptions {
@@ -64,6 +66,9 @@ async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSDat
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
// Stop any currently running audio
plugin.managers.markdownExtensions.audio.dispose();
// Reset canvas props to default so that modifyCanvasProps works as expected
resetCanvasProps(plugin);
@@ -80,11 +85,11 @@ async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSDat
const entry = molstarTreeToEntry(
plugin,
molstarTree,
snapshot.root,
snapshot.animation,
{ ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms },
options
);
await assignStateTransition(ctx, plugin, entry, snapshot, options);
await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length);
entries.push(entry);
if (ctx.shouldUpdate) {
@@ -119,8 +124,8 @@ async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSDat
}
}
async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext, parentEntry: PluginStateSnapshotManager.Entry, parent: Snapshot, options: MVSLoadOptions = {}) {
const transitions = await generateStateTransition(ctx, parent);
async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext, parentEntry: PluginStateSnapshotManager.Entry, parent: Snapshot, options: MVSLoadOptions, snapshotIndex: number, snapshotCount: number) {
const transitions = await generateStateTransition(ctx, parent, snapshotIndex, snapshotCount);
if (!transitions?.frames.length) return;
const animation: PluginState.StateTransition = {
@@ -135,7 +140,7 @@ async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext,
const entry = molstarTreeToEntry(
plugin,
molstarTree,
frame,
parent.animation,
{ ...parent.metadata, previousTransitionDurationMs: transitions.frametimeMs },
options
);
@@ -150,7 +155,7 @@ async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext,
});
if (ctx.shouldUpdate) {
await ctx.update({ message: 'Loading animation...', current: i + 1, max: transitions.frames.length });
await ctx.update({ message: `Loading animation for snapshot ${snapshotIndex + 1}/${snapshotCount}...`, current: i + 1, max: transitions.frames.length });
}
}
@@ -160,20 +165,24 @@ async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext,
function molstarTreeToEntry(
plugin: PluginContext,
tree: MolstarTree,
mvsTree: MVSTree,
animation: MVSAnimationNode<'animation'> | undefined,
metadata: SnapshotMetadata & { previousTransitionDurationMs?: number },
options: { keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }
) {
const context = MolstarLoadingContext.create();
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { replaceExisting: true, extensions: options?.extensions ?? BuiltinLoadingExtensions });
snapshot.canvas3d = {
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, mvsTree.custom) : undefined,
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, animation) : undefined,
};
if (!options?.keepCamera) {
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
}
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
if (tree.custom?.molstar_on_load_markdown_commands) {
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
}
const entryParams: PluginStateSnapshotManager.EntryParams = {
key: metadata.key,
name: metadata.title,
@@ -220,31 +229,72 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
},
parse(updateParent: UpdateTarget, node: MolstarNode<'parse'>): UpdateTarget | undefined {
const format = node.params.format;
if (format === 'cif') {
return UpdateTarget.apply(updateParent, ParseCif, {});
} else if (format === 'pdb') {
return updateParent;
} else if (format === 'map') {
return UpdateTarget.apply(updateParent, ParseCcp4, {});
} else {
console.error(`Unknown format in "parse" node: "${format}"`);
return undefined;
switch (format) {
case 'cif':
return UpdateTarget.apply(updateParent, ParseCif, {});
case 'pdb':
case 'pdbqt':
case 'gro':
case 'xyz':
case 'mol':
case 'sdf':
case 'mol2':
case 'xtc':
case 'lammpstrj':
return updateParent;
case 'map':
return UpdateTarget.apply(updateParent, ParseCcp4, {});
default:
console.error(`Unknown format in "parse" node: "${format}"`);
return undefined;
}
},
coordinates(updateParent: UpdateTarget, node: MolstarNode<'coordinates'>): UpdateTarget | undefined {
const format = node.params.format;
switch (format) {
case 'xtc':
return UpdateTarget.apply(updateParent, CoordinatesFromXtc);
case 'lammpstrj':
return UpdateTarget.apply(updateParent, CoordinatesFromLammpstraj);
default:
console.error(`Unknown format in "coordinates" node: "${format}"`);
return undefined;
}
},
trajectory(updateParent: UpdateTarget, node: MolstarNode<'trajectory'>): UpdateTarget | undefined {
const format = node.params.format;
if (format === 'cif') {
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
blockIndex: node.params.block_index ?? undefined,
});
} else if (format === 'pdb') {
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, {});
} else {
console.error(`Unknown format in "trajectory" node: "${format}"`);
return undefined;
switch (format) {
case 'cif':
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
blockIndex: node.params.block_index ?? undefined,
});
case 'pdb':
case 'pdbqt':
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { isPdbqt: format === 'pdbqt' });
case 'gro':
return UpdateTarget.apply(updateParent, TrajectoryFromGRO);
case 'xyz':
return UpdateTarget.apply(updateParent, TrajectoryFromXYZ);
case 'mol':
return UpdateTarget.apply(updateParent, TrajectoryFromMOL);
case 'sdf':
return UpdateTarget.apply(updateParent, TrajectoryFromSDF);
case 'mol2':
return UpdateTarget.apply(updateParent, TrajectoryFromMOL2);
case 'lammpstrj':
return UpdateTarget.apply(updateParent, TrajectoryFromLammpsTrajData);
default:
console.error(`Unknown format in "trajectory" node: "${format}"`);
return undefined;
}
},
trajectory_with_coordinates(updateParent: UpdateTarget, node: MolstarNode<'trajectory_with_coordinates'>): UpdateTarget | undefined {
const result = UpdateTarget.apply(updateParent, MVSTrajectoryWithCoordinates, {
coordinatesRef: node.params.coordinates_ref,
});
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
},
model(updateParent: UpdateTarget, node: MolstarSubtree<'model'>, context: MolstarLoadingContext): UpdateTarget {
const annotations = collectAnnotationReferences(node, context);
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {

View File

@@ -48,6 +48,7 @@ const ScalarInterpolation = {
..._Easing,
start: OptionalField(nullable(union(float, list(float))), null, 'Start value. If a list of values is provided, each element will be interpolated separately. If unset, parent state value is used.'),
end: OptionalField(nullable(union(float, list(float))), null, 'End value. If a list of values is provided, each element will be interpolated separately. If unset, only noise is applied.'),
discrete: OptionalField(bool, false, 'Whether to round the values to the closest integer. Useful for example for trajectory animation.'),
..._Noise,
};

View File

@@ -86,7 +86,7 @@ export function copyTree<T extends Tree>(root: T): T {
* nodes of kind `C` will be converted to `Y` with a child `Z` (original children moved to `Z`),
* nodes of other kinds will just be copied. */
export type ConversionRules<A extends Tree, B extends Tree> = {
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => Subtree<B>[]
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => { subtree: Subtree<B>[] }
};
/** Apply a set of conversion rules to a tree to change to a different schema. */
@@ -94,12 +94,12 @@ export function convertTree<A extends Tree, B extends Tree>(root: A, conversions
const mapping = new Map<Subtree<A>, Subtree<B>>();
let convertedRoot: Subtree<B>;
dfs<A>(root, (node, parent) => {
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => Subtree<B>[]) | undefined;
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => { subtree: Subtree<B>[] }) | undefined;
if (conversion) {
const convertidos = conversion(node, parent);
if (!parent && convertidos.length === 0) throw new Error('Cannot convert root to empty path');
const converted = conversion(node, parent);
if (!parent && converted?.subtree.length === 0) throw new Error('Cannot convert root to empty path');
let convParent = parent ? mapping.get(parent) : undefined;
for (const conv of convertidos) {
for (const conv of converted.subtree) {
if (convParent) {
(convParent.children ??= []).push(conv);
} else {
@@ -153,12 +153,14 @@ export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, treeSchema:
type TTree = TreeFor<S>;
const rules: ConversionRules<TTree, TTree> = {};
for (const kind in treeSchema.nodes) {
rules[kind as Kind<Subtree<TTree>>] = node => [{
kind: node.kind,
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
custom: node.custom,
ref: node.ref,
} as Node as any];
rules[kind as Kind<Subtree<TTree>>] = node => ({
subtree: [{
kind: node.kind,
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
custom: node.custom,
ref: node.ref,
} as Node as any]
});
}
return convertTree(tree, rules) as any;
}

View File

@@ -15,37 +15,76 @@ import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
/** Convert `format` parameter of `parse` node in `MolstarTree`
* into `format` and `is_binary` parameters in `MolstarTree` */
export const ParseFormatMvsToMolstar = {
// trajectory
mmcif: { format: 'cif', is_binary: false },
bcif: { format: 'cif', is_binary: true },
pdb: { format: 'pdb', is_binary: false },
pdbqt: { format: 'pdbqt', is_binary: false },
gro: { format: 'gro', is_binary: false },
xyz: { format: 'xyz', is_binary: false },
mol: { format: 'mol', is_binary: false },
sdf: { format: 'sdf', is_binary: false },
mol2: { format: 'mol2', is_binary: false },
lammpstrj: { format: 'lammpstrj', is_binary: false },
// coordinates
xtc: { format: 'xtc', is_binary: true },
// maps
map: { format: 'map', is_binary: true },
} satisfies { [p in ParseFormatT]: { format: MolstarParseFormatT, is_binary: boolean } };
/** Conversion rules for conversion from `MVSTree` (with all parameter values) to `MolstarTree` */
const mvsToMolstarConversionRules: ConversionRules<FullMVSTree, MolstarTree> = {
'download': node => [],
'download': node => ({ subtree: [] }),
'parse': (node, parent) => {
const { format, is_binary } = ParseFormatMvsToMolstar[node.params.format];
const convertedNode: MolstarNode<'parse'> = { kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref };
if (parent?.kind === 'download') {
return [
{ kind: 'download', params: { ...parent.params, is_binary }, custom: parent.custom, ref: parent.ref },
convertedNode,
] satisfies MolstarNode[];
return {
subtree: [
{ kind: 'download', params: { ...parent.params, is_binary }, custom: parent.custom, ref: parent.ref },
{ kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref }
] satisfies MolstarNode[]
};
} else {
console.warn('"parse" node is not being converted, this is suspicious');
return [convertedNode] satisfies MolstarNode[];
return {
subtree: [
{ kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref }
] satisfies MolstarNode[]
};
}
},
'coordinates': (node, parent) => {
if (parent?.kind !== 'parse') throw new Error(`Parent of "coordinates" must be "parse", not "${parent?.kind}".`);
const { format } = ParseFormatMvsToMolstar[parent.params.format];
return {
subtree: [
{ kind: 'coordinates', params: { format }, custom: node.custom, ref: node.ref }
] satisfies MolstarNode[]
};
},
'structure': (node, parent) => {
if (parent?.kind !== 'parse') throw new Error(`Parent of "structure" must be "parse", not "${parent?.kind}".`);
const { format } = ParseFormatMvsToMolstar[parent.params.format];
return [
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index']), custom: node.custom, ref: node.ref },
] satisfies MolstarNode[];
if (node.params.coordinates_ref) {
return {
subtree: [
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
{ kind: 'model', params: { model_index: 0 } },
{ kind: 'trajectory_with_coordinates', params: { coordinates_ref: node.params.coordinates_ref } },
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index', 'coordinates_ref']), custom: node.custom, ref: node.ref },
] satisfies MolstarNode[]
};
} else {
return {
subtree: [
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index', 'coordinates_ref']), custom: node.custom, ref: node.ref },
] satisfies MolstarNode[]
};
}
},
};
@@ -70,9 +109,20 @@ function fileExtensionMatches(filename: string, extensions: (FileExtension | '*'
}
const StructureFormatExtensions: Record<ParseFormatT, (FileExtension | '*')[]> = {
// trajectory
mmcif: ['.cif', '.mmif'],
bcif: ['.bcif'],
pdb: ['.pdb', '.ent'],
pdbqt: ['.pdbqt'],
gro: ['.gro'],
xyz: ['.xyz'],
mol: ['.mol'],
sdf: ['.sdf'],
mol2: ['.mol2'],
lammpstrj: ['.lammpstrj'],
// coordinates
xtc: ['.xtc'],
// volumes
map: ['.map', '.ccp4', '.mrc', '.mrcs'],
};

View File

@@ -5,7 +5,7 @@
*/
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
import { RequiredField, bool } from '../generic/field-schema';
import { RequiredField, bool, str } from '../generic/field-schema';
import { SimpleParamsSchema } from '../generic/params-schema';
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
import { FullMVSTreeSchema } from '../mvs/mvs-tree';
@@ -30,6 +30,14 @@ export const MolstarTreeSchema = TreeSchema({
format: RequiredField(MolstarParseFormatT, 'File format'),
}),
},
/** Auxiliary node corresponding to Molstar's CoordinatesFrom*. */
coordinates: {
description: "Auxiliary node corresponding to Molstar's CoordinatesFrom*.",
parent: ['parse'],
params: SimpleParamsSchema({
format: RequiredField(MolstarParseFormatT, 'File format'),
}),
},
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
trajectory: {
description: "Auxiliary node corresponding to Molstar's TrajectoryFrom*.",
@@ -39,10 +47,18 @@ export const MolstarTreeSchema = TreeSchema({
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index'] as const),
}),
},
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
trajectory_with_coordinates: {
description: 'Auxiliary node corresponding to assigning a separate coordinates to a trajectory.',
parent: ['model'],
params: SimpleParamsSchema({
coordinates_ref: RequiredField(str, 'Coordinates reference'),
}),
},
/** Auxiliary node corresponding to Molstar's ModelFromTrajectory. */
model: {
description: "Auxiliary node corresponding to Molstar's ModelFromTrajectory.",
parent: ['trajectory'],
parent: ['trajectory', 'trajectory_with_coordinates'],
params: SimpleParamsSchema(
pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['model_index'] as const)
),
@@ -52,7 +68,7 @@ export const MolstarTreeSchema = TreeSchema({
...FullMVSTreeSchema.nodes.structure,
parent: ['model'],
params: SimpleParamsSchema(
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index'] as const)
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index', 'coordinates_ref'] as const)
),
},
}

View File

@@ -98,6 +98,12 @@ export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
this._animation ??= new Animation(params);
return this._animation;
}
/** Modifies custom state of the root */
extendRootCustomState(custom: Record<string, any>): this {
this._node.custom = { ...this._node.custom, ...custom };
return this;
}
}
export class Animation {
@@ -138,10 +144,10 @@ export class Download extends _Base<'download'> {
/** Subsets of 'structure' node params which will be passed to individual builder functions. */
const StructureParamsSubsets = {
model: ['block_header', 'block_index', 'model_index'],
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id'],
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max'],
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius'],
model: ['block_header', 'block_index', 'model_index', 'coordinates_ref'],
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id', 'coordinates_ref'],
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max', 'coordinates_ref'],
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius', 'coordinates_ref'],
} satisfies { [kind in MVSNodeParams<'structure'>['type']]: (keyof MVSNodeParams<'structure'>)[] };
@@ -191,6 +197,11 @@ export class Parse extends _Base<'parse'> {
volume(params: MVSNodeParams<'volume'> & CustomAndRef = {}): Volume {
return new Volume(this._root, this.addChild('volume', params));
}
/** Add a 'coordinates' node indicating the parsed data type */
coordinates(params: MVSNodeParams<'coordinates'> & CustomAndRef = {}): Parse {
this.addChild('coordinates', params);
return this;
}
}

View File

@@ -15,6 +15,11 @@ const Cartoon = {
tubular_helices: OptionalField(bool, false, 'Simplify corkscrew helices to tubes.'),
};
const Backbone = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
};
const BallAndStick = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
@@ -22,6 +27,13 @@ const BallAndStick = {
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
};
const Line = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
/** Controls whether hydrogen atoms are drawn. */
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
};
const Spacefill = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
@@ -48,7 +60,9 @@ export const MVSRepresentationParams = UnionParamsSchema(
'Representation type',
{
cartoon: SimpleParamsSchema(Cartoon),
backbone: SimpleParamsSchema(Backbone),
ball_and_stick: SimpleParamsSchema(BallAndStick),
line: SimpleParamsSchema(Line),
spacefill: SimpleParamsSchema(Spacefill),
carbohydrate: SimpleParamsSchema(Carbohydrate),
surface: SimpleParamsSchema(Surface),

View File

@@ -92,6 +92,12 @@ export const MVSTreeSchema = TreeSchema({
format: RequiredField(ParseFormatT, 'Format of the input data resource.'),
}),
},
/** This node instructs to retrieve molecular coordinates from a parsed data resource. */
coordinates: {
description: 'This node instructs to retrieve molecular coordinates from a parsed data resource.',
parent: ['parse'],
params: SimpleParamsSchema({}),
},
/** This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation. */
structure: {
description: 'This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.',
@@ -113,6 +119,8 @@ export const MVSTreeSchema = TreeSchema({
ijk_min: OptionalField(tuple([int, int, int]), [-1, -1, -1], 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
/** Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`). */
ijk_max: OptionalField(tuple([int, int, int]), [1, 1, 1], 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
/** Reference to a specific set of coordinates. */
coordinates_ref: OptionalField(nullable(str), null, 'Reference to a specific set of coordinates.')
}),
},
/** This node instructs to rotate and/or translate structure coordinates. */

View File

@@ -11,11 +11,42 @@ import { ValueFor, bool, dict, float, int, list, literal, nullable, object, part
/** `format` parameter values for `parse` node in MVS tree */
export const ParseFormatT = literal('mmcif', 'bcif', 'pdb', 'map');
export const ParseFormatT = literal(
// trajectory
'mmcif',
'bcif', // +volumes
'pdb',
'pdbqt',
'gro',
'xyz',
'mol',
'sdf',
'mol2',
'lammpstrj', // + coordinates
// coordinates
'xtc',
// volumes
'map',
);
export type ParseFormatT = ValueFor<typeof ParseFormatT>
/** `format` parameter values for `parse` node in Molstar tree */
export const MolstarParseFormatT = literal('cif', 'pdb', 'map');
export const MolstarParseFormatT = literal(
// trajectory
'cif', // +volumes
'pdb',
'pdbqt',
'gro',
'xyz',
'mol',
'sdf',
'mol2',
'lammpstrj',
// coordinates
'xtc',
// volumes
'map'
);
export type MolstarParseFormatT = ValueFor<typeof MolstarParseFormatT>
/** `kind` parameter values for `structure` node in MVS tree */

View File

@@ -13,7 +13,7 @@ import { Vec3, Vec2 } from '../mol-math/linear-algebra';
import { InputObserver, ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
import { Renderer, RendererStats, RendererParams } from '../mol-gl/renderer';
import { GraphicsRenderObject } from '../mol-gl/render-object';
import { TrackballControls, TrackballControlsParams } from './controls/trackball';
import { DefaultTrackballControlsAttribs, TrackballControls, TrackballControlsParams } from './controls/trackball';
import { Viewport } from './camera/util';
import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
import { Representation } from '../mol-repr/representation';
@@ -34,7 +34,6 @@ import { ImagePass, ImageProps } from './passes/image';
import { Sphere3D } from '../mol-math/geometry';
import { addConsoleStatsProvider, isDebugMode, isTimingMode, removeConsoleStatsProvider } from '../mol-util/debug';
import { CameraHelperParams } from './helper/camera-helper';
import { produce } from 'immer';
import { HandleHelperParams } from './helper/handle-helper';
import { StereoCamera, StereoCameraParams } from './camera/stereo';
import { Helper } from './helper/helper';
@@ -49,6 +48,7 @@ import { IlluminationParams } from './passes/illumination';
import { isMobileBrowser } from '../mol-util/browser';
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
import { RayHelper } from './helper/ray-helper';
import { produce } from '../mol-util/produce';
export const CameraFogParams = {
intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
@@ -114,6 +114,11 @@ export type PartialCanvas3DProps = {
[K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
}
export const DefaultCanvas3DAttribs = {
trackball: DefaultTrackballControlsAttribs,
};
export type Canvas3DAttribs = typeof DefaultCanvas3DAttribs
export { Canvas3DContext };
/** Can be used to create multiple Canvas3D objects */
@@ -360,6 +365,7 @@ interface Canvas3D {
/** Returns a copy of the current Canvas3D instance props */
readonly props: Readonly<Canvas3DProps>
readonly attribs: Readonly<Canvas3DAttribs>
readonly input: InputObserver
readonly stats: RendererStats
readonly interaction: Canvas3dInteractionHelper['events']
@@ -379,9 +385,10 @@ namespace Canvas3D {
export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
export function create(ctx: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
export function create(ctx: Canvas3DContext, props: Partial<Canvas3DProps> = {}, attribs: Partial<Canvas3DAttribs> = {}): Canvas3D {
const { webgl, input, passes, assetManager, canvas, contextLost } = ctx;
const p: Canvas3DProps = { ...deepClone(DefaultCanvas3DParams), ...deepClone(props) };
const a = { ...deepClone(DefaultCanvas3DAttribs), ...deepClone(attribs) };
const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
@@ -421,7 +428,7 @@ namespace Canvas3D {
}, { x, y, width, height });
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
const controls = TrackballControls.create(input, camera, scene, p.trackball);
const controls = TrackballControls.create(input, camera, scene, p.trackball, a.trackball);
const helper = new Helper(webgl, scene, p);
const hiZ = new HiZPass(webgl, passes.draw, canvas, p.hiZ);
@@ -1180,6 +1187,9 @@ namespace Canvas3D {
get props() {
return getProps();
},
get attribs() {
return a;
},
get input() {
return input;
},

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 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>
@@ -56,8 +56,6 @@ export const DefaultTrackballBindings = {
};
export const TrackballControlsParams = {
noScroll: PD.Boolean(true, { isHidden: true }),
rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
@@ -68,10 +66,10 @@ export const TrackballControlsParams = {
animate: PD.MappedStatic('off', {
off: PD.EmptyGroup(),
spin: PD.Group({
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }, { description: 'Rotation speed in radians per second' }),
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }, { description: 'Number of rotations per second' }),
}, { 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 }),
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }, { description: 'Number of oscilations per second' }),
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' })
}),
@@ -85,8 +83,6 @@ export const TrackballControlsParams = {
gestureScaleFactor: PD.Numeric(1, {}, { isHidden: true }),
maxWheelDelta: PD.Numeric(0.02, {}, { isHidden: true }),
bindings: PD.Value(DefaultTrackballBindings, { isHidden: true }),
/**
* minDistance = minDistanceFactor * boundingSphere.radius + minDistancePadding
* maxDistance = max(maxDistanceFactor * boundingSphere.radius, maxDistanceMin)
@@ -103,6 +99,11 @@ export const TrackballControlsParams = {
};
export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
export const DefaultTrackballControlsAttribs = {
bindings: DefaultTrackballBindings,
};
export type TrackballControlsAttribs = typeof DefaultTrackballControlsAttribs
export { TrackballControls };
interface TrackballControls {
readonly viewport: Viewport
@@ -112,20 +113,25 @@ interface TrackballControls {
readonly props: Readonly<TrackballControlsProps>
setProps: (props: Partial<TrackballControlsProps>) => void
readonly attribs: Readonly<TrackballControlsAttribs>
setAttribs: (attribs: Partial<TrackballControlsAttribs>) => void
start: (t: number) => void
update: (t: number) => void
reset: () => void
dispose: () => void
}
namespace TrackballControls {
export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}): TrackballControls {
export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}, attribs: Partial<TrackballControlsAttribs> = {}): TrackballControls {
const p: TrackballControlsProps = {
...PD.getDefaultValues(TrackballControlsParams),
...props,
// include default bindings for backwards state compatibility
bindings: { ...DefaultTrackballBindings, ...props.bindings }
};
const b = p.bindings;
const a: TrackballControlsAttribs = {
...DefaultTrackballControlsAttribs,
...attribs
};
const b = a.bindings;
const viewport = Viewport.clone(camera.viewport);
@@ -825,7 +831,7 @@ namespace TrackballControls {
function spin(deltaT: number) {
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
const radPerMs = p.animate.params.speed / 1000;
const radPerMs = 2 * Math.PI * p.animate.params.speed / 1000;
_spinSpeed[0] = deltaT * radPerMs / getRotateFactor();
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
}
@@ -885,7 +891,12 @@ namespace TrackballControls {
}
}
Object.assign(p, props);
Object.assign(b, props.bindings);
},
get attribs() { return a as Readonly<TrackballControlsAttribs>; },
setAttribs: (attribs: Partial<TrackballControlsAttribs>) => {
Object.assign(a, attribs);
Object.assign(b, a.bindings);
},
start,

View File

@@ -4,7 +4,7 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { produce } from 'immer';
import { produce } from '../../mol-util/produce';
import { Interval } from '../../mol-data/int/interval';
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';

View File

@@ -16,7 +16,7 @@ import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
import { ValueCell } from '../../mol-util';
import { Sphere3D } from '../../mol-math/geometry';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { produce } from 'immer';
import { produce } from '../../mol-util/produce';
import { Shape } from '../../mol-model/shape';
import { PickingId } from '../../mol-geo/geometry/picking';
import { Camera } from '../camera';

View File

@@ -167,6 +167,7 @@ export class DpoitPass {
if (isTimingMode) this.webgl.timer.mark('DpoitPass.render');
const { state, gl } = this.webgl;
state.blendEquation(gl.FUNC_ADD);
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
ValueCell.update(this.renderable.values.tDpoitFrontColor, this.colorFrontTextures[this.writeId]);

View File

@@ -198,8 +198,10 @@ export class DrawPass {
const dpoitTextures = this.dpoit.bindDualDepthPeeling();
renderer.renderDpoitTransparent(scene.primitives, camera, this.depthTextureOpaque, dpoitTextures);
target.bind();
this.dpoit.renderBlendBack();
if (iterations > 1) {
target.bind();
this.dpoit.renderBlendBack();
}
if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
}

View File

@@ -171,13 +171,15 @@ export class IlluminationPass {
const dpoitTextures = this.drawPass.dpoit.bind();
renderer.renderDpoitTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque, dpoitTextures);
for (let i = 0, il = props.dpoitIterations; i < il; i++) {
for (let i = 0, iterations = props.dpoitIterations; i < iterations; i++) {
if (isTimingMode) this.webgl.timer.mark('DpoitPass.layer');
const dpoitTextures = this.drawPass.dpoit.bindDualDepthPeeling();
renderer.renderDpoitTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque, dpoitTextures);
this.transparentTarget.bind();
this.drawPass.dpoit.renderBlendBack();
if (iterations > 1) {
this.transparentTarget.bind();
this.drawPass.dpoit.renderBlendBack();
}
if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
}

View File

@@ -3,6 +3,7 @@
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/
// from http://burtleburtle.net/bob/hash/integer.html
@@ -365,6 +366,21 @@ export function murmurHash3_32(key: string, seed: number): number {
return h >>> 0;
}
/**
* MurmurHash3 128-bit implementation
* @param key - The input data to hash
* @param seed - The seed value (default: 0)
* @returns The 128-bit hash as a hexadecimal string
*/
export function murmurHash3_128_fromBytes(key: Uint8Array, seed: number): string {
// This fakeString approach is much faster than `new TextDecoder('ascii').decode(key)`
const fakeString = {
length: key.length,
charCodeAt(i: number) { return key[i]; },
};
return murmurHash3_128(fakeString as string, seed);
}
/**
* MurmurHash3 128-bit implementation
* @param key - The input string to hash

View File

@@ -13,7 +13,7 @@ import { Loci } from '../../../mol-model/loci';
import { Structure } from '../../../mol-model/structure';
import { PluginContext } from '../../../mol-plugin/context';
import { PluginState } from '../../../mol-plugin/state';
import { StateObject, StateTransform } from '../../../mol-state';
import { StateObjectCell, StateSelection, StateTransform } from '../../../mol-state';
import { PluginStateObject } from '../../objects';
@@ -65,7 +65,7 @@ export function getCellBoundingSphere(plugin: PluginContext, cellRef: StateTrans
/** Push bounding spheres within cell `cellRef` to `out`. If a cell does not define bounding spheres, collect bounding spheres from subtree. */
function collectCellBoundingSpheres(out: Sphere3D[], plugin: PluginContext, cellRef: StateTransform.Ref): Sphere3D[] {
const cell = plugin.state.data.cells.get(cellRef);
const spheres = getStateObjectBoundingSpheres(cell?.obj);
const spheres = getStateObjectBoundingSpheres(plugin, cell);
if (spheres) {
out.push(...spheres);
} else {
@@ -76,14 +76,17 @@ function collectCellBoundingSpheres(out: Sphere3D[], plugin: PluginContext, cell
}
/** Return a set of bounding spheres of a plugin state object. Return `undefined` if this plugin state object type does not define bounding spheres. */
function getStateObjectBoundingSpheres(obj: StateObject | undefined): Sphere3D[] | undefined {
function getStateObjectBoundingSpheres(plugin: PluginContext, cell: StateObjectCell | undefined): Sphere3D[] | undefined {
const obj = cell?.obj;
if (!obj) return undefined;
if (!obj.data) {
console.warn('Focus: no data');
return undefined;
}
if (obj.data instanceof Structure) {
const sphere = Loci.getBoundingSphere(Structure.Loci(obj.data));
const decorated = StateSelection.getDecorated<PluginStateObject.Molecule.Structure>(plugin.state.data, cell.transform.ref);
const data = decorated?.obj?.data ?? obj?.data;
const sphere = Loci.getBoundingSphere(Structure.Loci(data));
return sphere ? [sphere] : [];
} else if (PluginStateObject.isRepresentation3D(obj)) {
const out: Sphere3D[] = [];

View File

@@ -6,10 +6,11 @@
import { getCellBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateObjectCell } from '../../mol-state';
import { StateObjectCell, StateSelection } from '../../mol-state';
import { PluginContext } from '../../mol-plugin/context';
import { Script } from '../../mol-script/script';
import { QueryContext, QueryFn, StructureElement, StructureSelection } from '../../mol-model/structure';
import { BehaviorSubject } from 'rxjs';
export type MarkdownExtensionEvent = 'click' | 'mouse-enter' | 'mouse-leave';
@@ -135,7 +136,8 @@ export const BuiltInMarkdownExtension: MarkdownExtension[] = [
if (!action.includes('focus')) {
return;
}
const spheres = structures.map(s => {
const decorated = structures.map(s => StateSelection.getDecorated<PluginStateObject.Molecule.Structure>(manager.plugin.state.data, s.transform.ref));
const spheres = decorated.map(s => {
if (!s.obj?.data) return undefined;
const selection = query(new QueryContext(s.obj.data));
if (StructureSelection.isEmpty(selection)) return;
@@ -150,9 +152,46 @@ export const BuiltInMarkdownExtension: MarkdownExtension[] = [
}
},
},
{
name: 'play-audio',
execute: ({ event, args, manager }) => {
if (event !== 'click') return;
const src = args['play-audio'];
if (!src?.length) return;
manager.audio.play(src);
}
},
{
name: 'toggle-audio',
execute: ({ event, args, manager }) => {
if (event !== 'click' || !('toggle-audio' in args)) return;
const src = args['toggle-audio'];
manager.audio.play(src, { toggle: true });
}
},
{
name: 'pause-audio',
execute: ({ event, args, manager }) => {
if (event !== 'click' || !('pause-audio' in args)) return;
manager.audio.pause();
}
},
{
name: 'stop-audio',
execute: ({ event, args, manager }) => {
if (event !== 'click' || !('stop-audio' in args)) return;
manager.audio.stop();
}
},
];
export class MarkdownExtensionManager {
state = {
audioPlayer: new BehaviorSubject<HTMLAudioElement | null>(null),
};
private extension: MarkdownExtension[] = [];
private refResolvers: Record<string, (plugin: PluginContext, refs: string[]) => StateObjectCell[]> = {
default: (plugin: PluginContext, refs: string[]) => refs
@@ -286,6 +325,76 @@ export class MarkdownExtensionManager {
return ret;
}
private resolveAudioPlayer() {
if (this.state.audioPlayer.value) {
return this.state.audioPlayer.value;
}
const audio = document.createElement('audio');
audio.controls = true;
audio.preload = 'auto';
audio.style.width = '100%';
audio.style.height = '32px';
this.state.audioPlayer.next(audio);
return audio;
}
get audioPlayer() {
return this.state.audioPlayer.value;
}
audio = {
play: async (src: string, options?: { toggle?: boolean }) => {
try {
const audio = this.resolveAudioPlayer();
let newSource = false;
if (src?.trim()) {
const resolved = this.tryResolveUri(src);
let uri: string = src;
if (typeof (resolved as Promise<string>)?.then === 'function') {
uri = (await resolved) as string;
} else if (resolved) {
uri = resolved as string;
}
newSource = audio.src !== uri;
if (newSource) {
audio.src = uri;
audio.load();
}
}
if (!newSource && options?.toggle) {
if (audio.paused) {
await audio.play();
} else {
audio.pause();
}
} else {
audio.currentTime = 0;
await audio.play();
}
} catch (e) {
console.error('Failed to play audio', e);
}
},
pause: () => {
this.audioPlayer?.pause();
},
stop: () => {
if (!this.audioPlayer) return;
this.audioPlayer.pause();
this.audioPlayer.currentTime = 0;
},
dispose: () => {
if (this.audioPlayer) {
this.audioPlayer.pause();
this.audioPlayer.currentTime = 0;
this.state.audioPlayer.next(null);
}
}
};
constructor(public plugin: PluginContext) {
for (const command of BuiltInMarkdownExtension) {
this.registerExtension(command);

View File

@@ -365,6 +365,21 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
if (this.state.isPlaying) this.timeoutHandle = setTimeout(this.next, delay);
};
private async startPlayback() {
const { current } = this;
if (!current) return;
// If there is a transition associated with the current snapshot, replay it
if (current.snapshot.transition) {
const snapshot = this.setCurrent(this.state.current!)!;
await this.plugin.state.setSnapshot(snapshot);
const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
if (this.state.isPlaying) this.timeoutHandle = setTimeout(this.next, delay);
} else {
return this.next();
}
}
play(delayFirst: boolean = false) {
this.updateState({ isPlaying: true });
@@ -379,11 +394,12 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
this.timeoutHandle = setTimeout(this.next, delay);
} else {
this.next();
this.startPlayback();
}
}
stop() {
this.plugin.managers.animation.stop();
this.updateState({ isPlaying: false });
if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
this.timeoutHandle = void 0;
@@ -394,6 +410,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
if (this.state.isPlaying) {
this.stop();
this.plugin.managers.animation.stop();
this.plugin.managers.markdownExtensions.audio.pause();
} else {
this.play();
}

View File

@@ -225,7 +225,7 @@ const TopologyFromTop = PluginStateTransform.BuiltIn({
}
});
async function getTrajectory(ctx: RuntimeContext, obj: StateObject, coordinates: Coordinates) {
export async function getTrajectory(ctx: RuntimeContext, obj: StateObject, coordinates: Coordinates) {
if (obj.type === SO.Molecule.Topology.type) {
const topology = obj.data as Topology;
return await Model.trajectoryFromTopologyAndCoordinates(topology, coordinates).runInContext(ctx);
@@ -578,7 +578,7 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
isApplicable: a => a.data.frameCount > 0,
apply({ a, params }) {
return Task.create('Model from Trajectory', async ctx => {
let modelIndex = params.modelIndex % a.data.frameCount;
let modelIndex = Math.round(params.modelIndex) % a.data.frameCount;
if (modelIndex < 0) modelIndex += a.data.frameCount;
const model = await Task.resolveInContext(a.data.getFrameAtIndex(modelIndex), ctx);
const label = `Model ${modelIndex + 1}`;

View File

@@ -61,7 +61,7 @@ export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: bo
count++;
if (!label) {
const idx = (m.transform.params! as StateTransformer.Params<ModelFromTrajectory>).modelIndex;
label = `Model ${idx + 1} / ${parent.data.frameCount}`;
label = `Model ${Math.round(idx + 1)} / ${parent.data.frameCount}`;
}
}
}
@@ -111,37 +111,12 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
this.subscribe(this.plugin.managers.snapshot.events.changed, () => this.forceUpdate());
this.subscribe(this.plugin.behaviors.state.isBusy, isBusy => this.setState({ isBusy }));
this.subscribe(this.plugin.behaviors.state.isAnimating, isBusy => this.setState({ isBusy }));
window.addEventListener('keyup', this.keyUp, false);
}
componentWillUnmount() {
super.componentWillUnmount();
window.removeEventListener('keyup', this.keyUp, false);
}
keyUp = (e: KeyboardEvent) => {
if (!e.ctrlKey || this.state.isBusy || e.target !== document.body) return;
const snapshots = this.plugin.managers.snapshot;
if (e.keyCode === 37 || e.key === 'ArrowLeft') {
if (snapshots.state.isPlaying) snapshots.stop();
this.prev();
} 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)!;
this.update(e.snapshot.id);
} else if (e.keyCode === 39 || e.key === 'ArrowRight') {
if (snapshots.state.isPlaying) snapshots.stop();
this.next();
} 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)!;
this.update(e.snapshot.id);
}
};
async update(id: string) {
this.setState({ isBusy: true });
await PluginCommands.State.Snapshots.Apply(this.plugin, { id });
@@ -176,6 +151,7 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
toggleStateAnimation = () => {
if (this.state.isBusy) {
this.plugin.managers.animation.stop();
this.plugin.managers.markdownExtensions.audio.pause();
} else {
this.plugin.managers.animation.play(AnimateStateSnapshotTransition, {});
}
@@ -210,7 +186,7 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
{!isPlaying && <>
{count > 1 && <IconButton svg={NavigateBeforeSvg} title='Previous State' onClick={this.prev} disabled={disabled} />}
{count > 1 && <IconButton svg={NavigateNextSvg} title='Next State' onClick={this.next} disabled={disabled} />}
{hasAnimation && <IconButton svg={AnimationSvg} className='msp-state-snapshot-animation-button' title='Animation' onClick={this.toggleShowAnimation} disabled={!hasAnimation} toggleState={this.state.showAnimation} />}
{hasAnimation && <IconButton svg={AnimationSvg} className='msp-state-snapshot-animation-button' title='Snapshot Transition' onClick={this.toggleShowAnimation} disabled={!hasAnimation} toggleState={this.state.showAnimation} />}
</>}
{hasAnimation && this.state.showAnimation && !isPlaying && <>
<div className='msp-state-snapshot-animation-slider msp-form-control'>

View File

@@ -4,7 +4,7 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useRef, useState } from 'react';
import ReactMarkdown, { Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { PluginReactContext } from '../base';
@@ -13,9 +13,11 @@ import { PluginContext } from '../../mol-plugin/context';
import { MarkdownExtension } from '../../mol-plugin-state/manager/markdown-extensions';
import { ColorLists } from '../../mol-util/color/lists';
import { getColorGradient, getColorGradientBanded, parseColorList } from '../../mol-util/color/utils';
import { useBehavior } from '../hooks/use-behavior';
export function Markdown({ children, components }: { children?: string, components?: Components }) {
return <div className='msp-markdown'>
<MarkdownAudioPlayer />
<ReactMarkdown
skipHtml
components={{ a: MarkdownAnchor, img: MarkdownImg, ...components }}
@@ -26,6 +28,21 @@ export function Markdown({ children, components }: { children?: string, componen
</div>;
}
export function MarkdownAudioPlayer() {
const parent = useRef<HTMLDivElement>(null);
const plugin: PluginUIContext | undefined = useContext(PluginReactContext);
const audio = useBehavior(plugin?.managers.markdownExtensions.state.audioPlayer);
useEffect(() => {
if (!parent.current) return;
parent.current.appendChild(audio!);
return () => { audio?.remove(); };
}, [audio]);
if (!audio) return null;
return <div className='msp-markdown-audio-player' ref={parent} />;
}
export function MarkdownImg({ src, element, alt }: { src?: string, element?: any, alt?: string }) {
const plugin: PluginUIContext | undefined = useContext(PluginReactContext);

View File

@@ -96,30 +96,32 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
render() {
return <div className={'msp-viewport-controls'}>
<div className='msp-viewport-controls-buttons'>
<div className='msp-hover-box-wrapper'>
<div className='msp-semi-transparent-background' />
{this.icon(AutorenewSvg, this.resetCamera, 'Reset Zoom')}
<div className='msp-hover-box-body'>
<div className='msp-flex-column'>
<div className='msp-flex-row'>
<Button onClick={() => this.resetCamera()} disabled={!this.state.isCameraResetEnabled} title='Set camera zoom to fit the visible scene into view'>
Reset Zoom
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.OrientAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align principal component axes of the loaded structures to the screen axes (“lay flat”)'>
Orient Axes
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.ResetAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align Cartesian axes to the screen axes'>
Reset Axes
</Button>
{this.plugin.config.get(PluginConfig.Viewport.ShowReset) &&
<div className='msp-hover-box-wrapper'>
<div className='msp-semi-transparent-background' />
{this.icon(AutorenewSvg, this.resetCamera, 'Reset Zoom')}
<div className='msp-hover-box-body'>
<div className='msp-flex-column'>
<div className='msp-flex-row'>
<Button onClick={() => this.resetCamera()} disabled={!this.state.isCameraResetEnabled} title='Set camera zoom to fit the visible scene into view'>
Reset Zoom
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.OrientAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align principal component axes of the loaded structures to the screen axes (“lay flat”)'>
Orient Axes
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.ResetAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align Cartesian axes to the screen axes'>
Reset Axes
</Button>
</div>
</div>
</div>
<div className='msp-hover-box-spacer'></div>
</div>
<div className='msp-hover-box-spacer'></div>
</div>
}
{this.plugin.config.get(PluginConfig.Viewport.ShowScreenshotControls) && <div>
<div className='msp-semi-transparent-background' />
{this.icon(CameraOutlinedSvg, this.toggleScreenshotExpanded, 'Screenshot / State Snapshot', this.state.isScreenshotExpanded)}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -97,7 +97,7 @@ export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolea
return <>
{(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
<BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
<BindingsHelp bindings={this.plugin.canvas3d.attribs.trackball.bindings} />
</HelpGroup>}
{!!interactionBindings && <HelpGroup key='interactions' header='Mouse & Key Controls'>
<BindingsHelp bindings={interactionBindings} />

View File

@@ -5,7 +5,7 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { produce } from 'immer';
import { produce } from '../../mol-util/produce';
import { throttleTime } from 'rxjs';
import { Canvas3DContext, Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
import { PluginCommands } from '../../mol-plugin/commands';

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 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>
*/
export * from './behavior/behavior';
@@ -13,6 +14,7 @@ import * as StaticMisc from './behavior/static/misc';
import * as DynamicRepresentation from './behavior/dynamic/representation';
import * as DynamicCamera from './behavior/dynamic/camera';
import * as DynamicState from './behavior/dynamic/state';
import * as DynamicCustomProps from './behavior/dynamic/custom-props';
export const BuiltInPluginBehaviors = {
@@ -25,5 +27,6 @@ export const BuiltInPluginBehaviors = {
export const PluginBehaviors = {
Representation: DynamicRepresentation,
Camera: DynamicCamera,
State: DynamicState,
CustomProps: DynamicCustomProps
};

View File

@@ -79,6 +79,13 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
return;
}
// Prevent when interaction props are set
const snapshotKey = current.repr?.props?.snapshotKey?.trim() ?? '';
const markdownCommands = current.repr?.props?.markdownCommands;
if (snapshotKey || (typeof markdownCommands === 'object' && Object.keys(markdownCommands).length > 0)) {
return;
}
if (Binding.match(binding, button, modifiers)) {
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
this.ctx.managers.camera.focusLoci(loci, this.params);

View File

@@ -290,6 +290,15 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
return;
}
// Support executing markdown commands associated with a visual
const markdownCommands = current.repr?.props?.markdownCommands;
if (!this.ctx.selectionMode && matched && typeof markdownCommands === 'object') {
if (Object.keys(markdownCommands).length > 0) {
this.ctx.managers.markdownExtensions.tryExecute('click', markdownCommands);
return;
}
}
// only apply structure focus for appropriate granularity
const { granularity } = this.ctx.managers.interactivity.props;
if (granularity !== 'residue' && granularity !== 'element') return;

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { PluginBehavior } from '../behavior';
import { Binding } from '../../../mol-util/binding';
import { ModifiersKeys } from '../../../mol-util/input/input-observer';
const M = ModifiersKeys;
const Key = Binding.TriggerKey;
const DefaultSnapshotControlsBindings = {
next: Binding([
Key('ArrowRight', M.create({ control: true })),
]),
previous: Binding([
Key('ArrowLeft', M.create({ control: true })),
]),
first: Binding([
Key('ArrowUp', M.create({ control: true })),
]),
last: Binding([
Key('ArrowDown', M.create({ control: true })),
]),
};
const SnapshotControlsParams = {
bindings: PD.Value(DefaultSnapshotControlsBindings, { isHidden: true }),
};
type SnapshotControlsProps = PD.Values<typeof SnapshotControlsParams>
export const SnapshotControls = PluginBehavior.create<SnapshotControlsProps>({
name: 'snapshot-controls',
category: 'interaction',
ctor: class extends PluginBehavior.Handler<SnapshotControlsProps> {
register(): void {
this.subscribeObservable(this.ctx.behaviors.interaction.keyReleased, ({ code, modifiers, key }) => {
if (!this.ctx.canvas3d || this.ctx.isBusy) return;
// include defaults for backwards state compatibility
const b = this.params.bindings;
const { snapshot } = this.ctx.managers;
if (Binding.matchKey(b.next, code, modifiers, key)) {
snapshot.applyNext(1);
}
if (Binding.matchKey(b.previous, code, modifiers, key)) {
snapshot.applyNext(-1);
}
if (Binding.matchKey(b.first, code, modifiers, key)) {
const e = snapshot.state.entries.get(0)!;
const s = snapshot.setCurrent(e.snapshot.id);
if (s) return this.ctx.state.setSnapshot(s);
}
if (Binding.matchKey(b.last, code, modifiers, key)) {
const e = snapshot.state.entries.get(snapshot.state.entries.size - 1)!;
const s = snapshot.setCurrent(e.snapshot.id);
if (s) return this.ctx.state.setSnapshot(s);
}
});
}
},
params: () => SnapshotControlsParams,
display: { name: 'Snapshot Controls' }
});

View File

@@ -52,6 +52,7 @@ export const PluginConfig = {
EmdbHeaderServer: item('volume-streaming.emdb-header-server', 'https://files.wwpdb.org/pub/emdb/structures'),
},
Viewport: {
ShowReset: item('viewer.show-reset-button', true),
ShowExpand: item('viewer.show-expand-button', true),
ShowControls: item('viewer.show-controls-button', true),
ShowSettings: item('viewer.show-settings-button', true),

View File

@@ -5,7 +5,7 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { produce, setAutoFreeze } from 'immer';
import { produce } from '../mol-util/produce';
import { List } from 'immutable';
import { merge, Subscription } from 'rxjs';
import { debounceTime, filter, take, throttleTime } from 'rxjs/operators';
@@ -382,6 +382,7 @@ export class PluginContext {
}
this.subs = [];
this.managers.markdownExtensions.audio.dispose();
this.animationLoop.stop();
this.commands.dispose();
this.canvas3d?.dispose();
@@ -529,11 +530,6 @@ export class PluginContext {
}
constructor(public spec: PluginSpec) {
// the reason for this is that sometimes, transform params get modified inline (i.e. palette.valueLabel)
// and freezing the params object causes "read-only exception"
// TODO: is this the best place to do it?
setAutoFreeze(false);
setSaccharideCompIdMapType(this.config.get(PluginConfig.Structure.SaccharideCompIdMapType) ?? 'default');
}
}

View File

@@ -124,6 +124,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
PluginSpec.Behavior(PluginBehaviors.Camera.CameraControls),
PluginSpec.Behavior(PluginBehaviors.State.SnapshotControls),
PluginSpec.Behavior(StructureFocusRepresentation),
PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo),

View File

@@ -6,7 +6,7 @@
* @author Adam Midlik <midlik@gmail.com>
*/
import { produce } from 'immer';
import { produce } from '../mol-util/produce';
import { merge } from 'rxjs';
import { Camera } from '../mol-canvas3d/camera';
import { Canvas3DContext, Canvas3DParams, Canvas3DProps } from '../mol-canvas3d/canvas3d';
@@ -118,6 +118,11 @@ class PluginState extends PluginComponent {
durationMs: snapshot.camera.transitionStyle === 'animate' ? snapshot.camera.transitionDurationInMs : undefined,
});
}
if (typeof snapshot?.onLoadMarkdownCommands === 'object' && Object.keys(snapshot.onLoadMarkdownCommands).length > 0) {
this.plugin.managers.markdownExtensions.tryExecute('click', snapshot.onLoadMarkdownCommands);
}
if (snapshot.startAnimation) {
this.animation.start();
return;
@@ -147,6 +152,10 @@ class PluginState extends PluginComponent {
durationMs: frame.camera.transitionStyle === 'animate' ? frame.camera.transitionDurationInMs : undefined,
});
}
if (!frameIndex && typeof snapshot?.onLoadMarkdownCommands === 'object' && Object.keys(snapshot.onLoadMarkdownCommands).length > 0) {
this.plugin.managers.markdownExtensions.tryExecute('click', snapshot.onLoadMarkdownCommands);
}
}
updateTransform(state: State, a: StateTransform.Ref, params: any, canUndo?: string | boolean) {
@@ -241,6 +250,7 @@ namespace PluginState {
},
durationInMs?: number,
transition?: StateTransition,
onLoadMarkdownCommands?: Record<string, any>
}
export interface StateTransition {

View File

@@ -507,7 +507,7 @@ namespace Representation {
}
let _EmptyRepresentation: Representation.Any | undefined = undefined;
Object.defineProperty(Representation, "Empty", {
Object.defineProperty(Representation, 'Empty', {
get: () => {
return _EmptyRepresentation ??= Representation.createEmpty();
}

View File

@@ -276,7 +276,8 @@ const atomProperty = {
instanceId: atomProp(Type.Str, 'Canonical name of the symmetry operator applied to this element.'),
operatorKey: atomProp(Type.Num, 'Key of the symmetry operator applied to this element.'),
modelIndex: atomProp(Type.Num, 'Index of the model in the input file.'),
modelLabel: atomProp(Type.Str, 'Label/header of the model in the input file.')
modelLabel: atomProp(Type.Str, 'Label/header of the model in the input file.'),
modelEntryId: atomProp(Type.Str, 'Entry ID of the model (e.g., PDB ID).')
},
topology: {

View File

@@ -309,6 +309,7 @@ const symbols = [
D(MolScript.structureQuery.atomProperty.core.operatorKey, atomProp(StructureProperties.unit.operator_key)),
D(MolScript.structureQuery.atomProperty.core.modelIndex, atomProp(StructureProperties.unit.model_index)),
D(MolScript.structureQuery.atomProperty.core.modelLabel, atomProp(StructureProperties.unit.model_label)),
D(MolScript.structureQuery.atomProperty.core.modelEntryId, atomProp(StructureProperties.unit.model_entry_id)),
D(MolScript.structureQuery.atomProperty.core.atomKey, (ctx, xs) => {
const e = (xs && xs[0] && xs[0](ctx) as any) || ctx.element;
return cantorPairing(e.unit.id, e.element);

View File

@@ -215,6 +215,7 @@ export const SymbolTable = [
Alias(MolScript.structureQuery.atomProperty.core.operatorKey, 'atom.op-key'),
Alias(MolScript.structureQuery.atomProperty.core.modelIndex, 'atom.model-index'),
Alias(MolScript.structureQuery.atomProperty.core.modelLabel, 'atom.model-label'),
Alias(MolScript.structureQuery.atomProperty.core.modelEntryId, 'atom.model-entry-id'),
Alias(MolScript.structureQuery.atomProperty.core.atomKey, 'atom.key'),
Alias(MolScript.structureQuery.atomProperty.core.bondCount, 'atom.bond-count'),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -80,8 +80,6 @@ interface StateObjectCell<T extends StateObject = StateObject, F extends StateTr
values: any
} | undefined,
paramsNormalizedVersion: string,
dependencies: {
dependentBy: StateObjectCell[],
dependsOn: StateObjectCell[]

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -381,7 +381,6 @@ class State {
definition: {},
values: {}
},
paramsNormalizedVersion: root.version,
dependencies: { dependentBy: [], dependsOn: [] },
cache: { }
});
@@ -666,7 +665,6 @@ function addCellsVisitor(transform: StateTransform, _: any, { ctx, added, visite
state: { ...transform.state },
errorText: void 0,
params: void 0,
paramsNormalizedVersion: '',
dependencies: { dependentBy: [], dependsOn: [] },
cache: void 0
};
@@ -849,9 +847,9 @@ function resolveParams(ctx: UpdateContext, transform: StateTransform, src: State
const prms = transform.transformer.definition.params;
const definition = prms ? prms(src, ctx.parent.globalContext) : {};
if (cell.paramsNormalizedVersion !== transform.version) {
if (transform.version !== (transform as any)._normalized_param_version) {
(transform.params as any) = ParamDefinition.normalizeParams(definition, transform.params, 'all');
cell.paramsNormalizedVersion = transform.version;
(transform as any)._normalized_param_version = transform.version;
} else {
const defaultValues = ParamDefinition.getDefaultValues(definition);
(transform.params as any) = transform.params

View File

@@ -11,7 +11,7 @@ import { StateObject, StateObjectCell, StateObjectSelector, StateObjectRef } fro
import { StateTransform } from '../transform';
import { StateTransformer } from '../transformer';
import { State } from '../state';
import { produce } from 'immer';
import { produce } from '../../mol-util/produce';
export { StateBuilder };

View File

@@ -375,6 +375,14 @@ namespace StateSelection {
const first = children.first();
if (first && state.transforms.get(first).transformer.definition.isDecorator) return tryFindDecorator(state, first, transformer);
}
export function getDecorated<T extends StateObject>(state: State, root: StateTransform.Ref): StateObjectCell<T> {
const children = state.tree.children.get(root);
if (children.size !== 1) return state.cells.get(root) as any;
const first = children.first();
if (first && state.transforms.get(first).transformer.definition.isDecorator) return getDecorated(state, first);
return state.cells.get(root) as any;
}
}
export { StateSelection };

View File

@@ -7,6 +7,7 @@
export async function fileToDataUri(file: File): Promise<string> {
const filename = file.name.toLowerCase() || 'file';
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].some(ext => filename.endsWith(`.${ext}`));
const isAudio = ['mp3', 'wav', 'ogg'].some(ext => filename.endsWith(`.${ext}`));
let type = 'application/octet-stream';
if (isImage) {
@@ -19,6 +20,16 @@ export async function fileToDataUri(file: File): Promise<string> {
type = `image/${ext}`;
break;
}
} else if (isAudio) {
const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'mp3':
type = 'audio/mpeg';
break;
default:
type = `audio/${ext}`;
break;
}
}
const bytes = await file.arrayBuffer();

View File

@@ -534,7 +534,7 @@ export namespace ParamDefinition {
if (a === undefined) return { ...b };
if (b === undefined) return { ...a };
const o = Object.create(null);
const o = {} as any;
for (const k of Object.keys(params)) {
o[k] = mergeParam(params[k], a[k], b[k]);
}
@@ -587,7 +587,7 @@ export namespace ParamDefinition {
if (p.type === 'value') {
return value;
} else if (p.type === 'group') {
const ret = Object.create(null);
const ret = {} as any;
for (const key of Object.keys(p.params)) {
const param = p.params[key];
if (value[key] === void 0) {
@@ -638,7 +638,7 @@ export namespace ParamDefinition {
return defaultIfUndefined ? getDefaultValues(p) : value;
}
const ret = Object.create(null);
const ret = {} as any;
for (const key of Object.keys(p)) {
const param = p[key];
if (value[key] === void 0) {

View File

@@ -5,7 +5,7 @@
*/
import { ParamDefinition as PD } from './param-definition';
import { produce } from 'immer';
import { produce } from './produce';
import { Mutable } from './type-helpers';
export interface ParamMapping<S, T, Ctx> {

23
src/mol-util/produce.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { create, rawReturn } from 'mutative';
let currentRecipe: any = undefined;
function recipeWrapper(draft: any) {
const r = currentRecipe(draft);
if (r !== undefined && r !== draft) return rawReturn(r);
return r;
}
/** Apply changes to an immutable-like object */
export function produce<T>(base: T, recipe: (draft: T) => T | void): T {
currentRecipe = recipe;
if (typeof base === 'object' && !('prototype' in (base as any))) {
return create({ ...base }, recipeWrapper) as T;
}
return create(base, recipeWrapper) as T;
}