mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 22:31:26 +08:00
Compare commits
26 Commits
v5.0.0-dev
...
v5.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4b09d3a0c | ||
|
|
6e488b0f80 | ||
|
|
6164281a50 | ||
|
|
2db7171e2a | ||
|
|
edfc094952 | ||
|
|
b3e1e2900b | ||
|
|
1e498d535a | ||
|
|
6ed969cd1b | ||
|
|
27bb4f4bca | ||
|
|
6ce2139272 | ||
|
|
13cf6613a6 | ||
|
|
c5bb13e295 | ||
|
|
34c8257848 | ||
|
|
fcbf39c935 | ||
|
|
46c8150b2b | ||
|
|
af1a864daa | ||
|
|
3babd9399a | ||
|
|
e57564486f | ||
|
|
464a91ac29 | ||
|
|
27fa50a5de | ||
|
|
1e323f18f7 | ||
|
|
2685b2b77d | ||
|
|
d71b47a515 | ||
|
|
88cc720dd2 | ||
|
|
201433cc91 | ||
|
|
8582303491 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -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 `` 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:
|
||||
|
||||
@@ -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 `
|
||||
- `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
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
Binary file not shown.
24
package-lock.json
generated
24
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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> <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> <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' });
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
185
src/examples/mvs-stories/stories/audio.ts
Normal file
185
src/examples/mvs-stories/stories/audio.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
958
src/examples/mvs-stories/stories/motm1.ts
Normal file
958
src/examples/mvs-stories/stories/motm1.ts
Normal 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)**.
|
||||
|
||||

|
||||
|
||||
*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(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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: {} },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
/* **************************************************** */
|
||||
|
||||
|
||||
36
src/extensions/mvs/components/trajectory.ts
Normal file
36
src/extensions/mvs/components/trajectory.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
26
src/extensions/mvs/export.ts
Normal file
26
src/extensions/mvs/export.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
70
src/mol-plugin/behavior/dynamic/state.ts
Normal file
70
src/mol-plugin/behavior/dynamic/state.ts
Normal 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' }
|
||||
});
|
||||
@@ -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),
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
23
src/mol-util/produce.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user