mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 22:31:26 +08:00
Compare commits
20 Commits
v5.0.0-dev
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14e619d6d2 | ||
|
|
42d969bbeb | ||
|
|
fdc33e44dc | ||
|
|
b0aa889a0a | ||
|
|
4d7bd53231 | ||
|
|
c11cf665c9 | ||
|
|
a4b09d3a0c | ||
|
|
6e488b0f80 | ||
|
|
6164281a50 | ||
|
|
2db7171e2a | ||
|
|
edfc094952 | ||
|
|
b3e1e2900b | ||
|
|
1e498d535a | ||
|
|
6ed969cd1b | ||
|
|
27bb4f4bca | ||
|
|
6ce2139272 | ||
|
|
13cf6613a6 | ||
|
|
c5bb13e295 | ||
|
|
34c8257848 | ||
|
|
fcbf39c935 |
@@ -13,6 +13,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- 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"
|
||||
- [Breaking] `PluginStateSnapshotManager.play` now accepts an options object instead of a single boolean value
|
||||
- Update production build to use `esbuild`
|
||||
- Emit explicit paths in `import`s in `lib/`
|
||||
- Fix outlines on opaque elements using illumination mode
|
||||
@@ -20,7 +21,7 @@ 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`
|
||||
- `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
|
||||
@@ -41,14 +42,18 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- 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))
|
||||
|
||||
@@ -20,14 +20,19 @@ Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `en
|
||||
|
||||
- `center-camera` - Centers the camera
|
||||
- `apply-snapshot=key` - Loads snapshots with the provided key
|
||||
- `next-snapshot[=-1|1]` - Loads next/previous snapshot, the direction is optional and default to `1`
|
||||
- `play-snapshots` - Starts playback of state snapshots
|
||||
- `play-transition` - Plays an animation associated with the given snapshot
|
||||
- `stop-animation` - Stops currently playing animation
|
||||
- `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[=src]`, `stop-audio`, `pause-audio`, `dispose-audio` - Audio playback support
|
||||
|
||||
## Custom Content
|
||||
|
||||
@@ -36,11 +41,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.
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.7",
|
||||
"version": "5.0.0-dev.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.7",
|
||||
"version": "5.0.0-dev.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.7",
|
||||
"version": "5.0.0-dev.10",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -122,7 +122,8 @@
|
||||
"Lukáš Polák <admin@lukaspolak.cz>",
|
||||
"Chetan Mishra <chetan.s115@gmail.com>",
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>"
|
||||
"Kim Juho <juho_kim@outlook.com>",
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -203,4 +204,4 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -182,6 +182,15 @@
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1d4ed7;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
|
||||
@@ -22,7 +22,15 @@ const Steps = [
|
||||
header: 'Animation Demo',
|
||||
key: 'intro',
|
||||
description: `### Molecular Animation
|
||||
A story showcasing MolViewSpec animation capabilities.`,
|
||||
A story showcasing MolViewSpec animation capabilities.
|
||||
|
||||
[\[**🔄 Replay Intro**\]](!play-transition)
|
||||
[\[**⏵ Play Snapshots**\]](!play-snapshots)
|
||||
[\[**⏹ Stop Animation**\]](!stop-animation)
|
||||
|
||||
[\[**➡️ Next Snapshot**\]](!next-snapshot)
|
||||
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
@@ -41,7 +49,7 @@ A story showcasing MolViewSpec animation capabilities.`,
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'rock',
|
||||
params: { speed: 1.5 },
|
||||
params: { speed: 0.5 },
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -49,11 +57,23 @@ A story showcasing MolViewSpec animation capabilities.`,
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
duration_ms: 1000,
|
||||
start_ms: 500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
start: 1,
|
||||
end: 0.66,
|
||||
});
|
||||
|
||||
|
||||
// Uncomment this to make 2nd frame render much faster
|
||||
// It will cause shader compilation to happen during the 1st snapshot
|
||||
@@ -119,14 +139,32 @@ A story showcasing MolViewSpec animation capabilities.`,
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'clip-transition',
|
||||
target_ref: 'clip',
|
||||
duration_ms: 2000,
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
easing: 'sin-in',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 600,
|
||||
duration_ms: 800,
|
||||
property: ['point', 2],
|
||||
end: 0,
|
||||
easing: 'sin-out',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'vec3',
|
||||
target_ref: 'xform',
|
||||
@@ -182,7 +220,7 @@ A story showcasing MolViewSpec animation capabilities.`,
|
||||
ref: 'repr',
|
||||
type: 'ball_and_stick',
|
||||
custom: {
|
||||
molstar_reprepresentation_params: {
|
||||
molstar_representation_params: {
|
||||
emissive: 0,
|
||||
}
|
||||
}
|
||||
@@ -209,7 +247,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,
|
||||
});
|
||||
|
||||
|
||||
186
src/examples/mvs-stories/stories/audio.ts
Normal file
186
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;
|
||||
1020
src/examples/mvs-stories/stories/motm1.ts
Normal file
1020
src/examples/mvs-stories/stories/motm1.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 });
|
||||
|
||||
/* **************************************************** */
|
||||
|
||||
|
||||
@@ -5,20 +5,21 @@
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { create } from 'mutative';
|
||||
import { Snapshot } from '../mvs-data';
|
||||
import { Tree } from '../tree/generic/tree-schema';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { MVSAnimationEasing, MVSAnimationNode, MVSAnimationSchema } from '../tree/animation/animation-tree';
|
||||
import { MVSTree } from '../tree/mvs/mvs-tree';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import * as EasingFns from '../../../mol-math/easing';
|
||||
import { addDefaults } from '../tree/generic/tree-utils';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { EPSILON, Mat3, Mat4, Quat, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { produce } from '../../../mol-util/produce';
|
||||
import { makeContinuousPaletteCheckpoints, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from '../components/annotation-color-theme';
|
||||
import { palettePropsFromMVSPalette } from '../load-helpers';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { Snapshot } from '../mvs-data';
|
||||
import { MVSAnimationEasing, MVSAnimationNode, MVSAnimationSchema } from '../tree/animation/animation-tree';
|
||||
import { Tree } from '../tree/generic/tree-schema';
|
||||
import { addDefaults } from '../tree/generic/tree-utils';
|
||||
import { MVSTree } from '../tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
|
||||
export async function generateStateTransition(ctx: RuntimeContext, snapshot: Snapshot, snapshotIndex: number, snapshotCount: number) {
|
||||
@@ -33,17 +34,27 @@ export async function generateStateTransition(ctx: RuntimeContext, snapshot: Sna
|
||||
...transitions.map(t => (t.params.start_ms ?? 0) + t.params.duration_ms)
|
||||
);
|
||||
|
||||
const frames: MVSTree[] = [];
|
||||
const frames: [tree: MVSTree, time: number][] = [];
|
||||
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>();
|
||||
|
||||
const transitionGroups = groupTranstions(transitions);
|
||||
|
||||
let prevRoot: MVSTree | undefined;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
const t = i * dt;
|
||||
const root = createSnapshot(snapshot.root, transitions, t, cache, nodeMap);
|
||||
frames.push(root);
|
||||
const root = createSnapshot(snapshot.root, transitionGroups, t, cache, nodeMap);
|
||||
|
||||
if (root === prevRoot || (prevRoot && deepEqual(root, prevRoot))) {
|
||||
frames[frames.length - 1][1] += dt;
|
||||
} else {
|
||||
frames.push([root, dt]);
|
||||
}
|
||||
|
||||
prevRoot = root;
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Generating transition for snapshot ${snapshotIndex + 1}/${snapshotCount}`, current: i + 1, max: N });
|
||||
@@ -80,71 +91,91 @@ interface InterpolationCacheEntry {
|
||||
rotation?: { axis: Vec3, angle: number, start: Quat, end: Quat },
|
||||
}
|
||||
|
||||
function createSnapshot(tree: MVSTree, transitions: MVSAnimationNode<'interpolate'>[], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
|
||||
return create(tree, (draft) => {
|
||||
for (const transition of transitions) {
|
||||
const nodePath = nodeMap.get(transition.params.target_ref);
|
||||
function getTransitionKey(transition: MVSAnimationNode<'interpolate'>) {
|
||||
const prop = transition.params.property;
|
||||
if (Array.isArray(prop)) {
|
||||
return `${transition.params.target_ref}:${prop.join('.')}`;
|
||||
}
|
||||
return `${transition.params.target_ref}:${prop}`;
|
||||
}
|
||||
|
||||
function groupTranstions(transitions: MVSAnimationNode<'interpolate'>[]) {
|
||||
const map = new Map<string, MVSAnimationNode<'interpolate'>[]>();
|
||||
const groups: MVSAnimationNode<'interpolate'>[][] = [];
|
||||
for (const t of transitions) {
|
||||
const key = getTransitionKey(t);
|
||||
if (!map.has(key)) {
|
||||
const group: MVSAnimationNode<'interpolate'>[] = [];
|
||||
map.set(key, group);
|
||||
groups.push(group);
|
||||
}
|
||||
map.get(key)!.push(t);
|
||||
}
|
||||
for (const group of groups) {
|
||||
group.sort((a, b) => {
|
||||
const s = (a.params.start_ms ?? 0) - (b.params.start_ms ?? 0);
|
||||
if (s !== 0) return s;
|
||||
return a.params.duration_ms - b.params.duration_ms;
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function createSnapshot(tree: MVSTree, transitionGroups: MVSAnimationNode<'interpolate'>[][], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
|
||||
let modified = false;
|
||||
const ret = produce(tree, (draft) => {
|
||||
for (const transitionGroup of transitionGroups) {
|
||||
|
||||
const pivot = transitionGroup[0];
|
||||
const nodePath = nodeMap.get(pivot.params.target_ref);
|
||||
if (!nodePath) continue;
|
||||
|
||||
const node = select(draft, nodePath, 0);
|
||||
const target = transition.params.property[0] === 'custom' ? node?.custom : node?.params;
|
||||
const target = pivot.params.property[0] === 'custom' ? node?.custom : node?.params;
|
||||
if (!target) continue;
|
||||
|
||||
|
||||
const offset = pivot.params.property[0] === 'custom' ? 1 : 0;
|
||||
|
||||
let transition: MVSAnimationNode<'interpolate'> = pivot;
|
||||
let previous: MVSAnimationNode<'interpolate'> | undefined;
|
||||
|
||||
for (let i = transitionGroup.length - 1; i > 0; i--) {
|
||||
const current = transitionGroup[i];
|
||||
const currentStart = current.params.start_ms ?? 0;
|
||||
if (time >= currentStart) {
|
||||
transition = current;
|
||||
previous = i > 0 ? transitionGroup[i - 1] : undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.has(transition)) {
|
||||
cache.set(transition, {});
|
||||
}
|
||||
|
||||
const cacheEntry: InterpolationCacheEntry = cache.get(transition)!;
|
||||
|
||||
const startTime = transition.params.start_ms ?? 0;
|
||||
let t = clamp((time - startTime) / transition.params.duration_ms, 0, 1);
|
||||
|
||||
if (transition.params.kind === 'transform_matrix') {
|
||||
processTransformMatrix(transition, target, t, cacheEntry);
|
||||
continue;
|
||||
}
|
||||
|
||||
t = applyFrequency(t, transition.params.frequency ?? 1, !!transition.params.alternate_direction);
|
||||
|
||||
const offset = transition.params.property[0] === 'custom' ? 1 : 0;
|
||||
const startBase = transition.params.start ?? select(target, transition.params.property, offset);
|
||||
|
||||
if (transition.params.kind === 'color' && !cacheEntry.paletteFn) {
|
||||
cacheEntry.paletteFn = makePaletteFunction(transition, startBase, transition.params.end as ColorT | undefined);
|
||||
}
|
||||
|
||||
const paletteFn = cacheEntry.paletteFn!;
|
||||
|
||||
const startValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(0))
|
||||
: startBase;
|
||||
const endValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(1))
|
||||
: transition.params.end;
|
||||
|
||||
if (time <= startTime) {
|
||||
assign(target, transition.params.property, startValue, offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
const easing = EasingFnMap[transition.params.easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
t = easing(t);
|
||||
const startTime: number = transition.params.start_ms ?? 0;
|
||||
const durationMs: number = transition.params.duration_ms ?? 0;
|
||||
const t = (time - startTime) / durationMs;
|
||||
|
||||
let next: any;
|
||||
if (transition.params.kind === 'scalar') {
|
||||
next = interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0);
|
||||
} 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') {
|
||||
next = interpolateRotation(startValue, endValue, t, transition.params.noise_magnitude ?? 0, cacheEntry);
|
||||
} else if (transition.params.kind === 'color') {
|
||||
const color = paletteFn(t);
|
||||
next = Color.toHexStyle(color);
|
||||
if (transition.params.kind === 'transform_matrix') {
|
||||
next = processTransformMatrix(transition, target, clamp(t, 0, 1), cacheEntry, offset, previous);
|
||||
} else {
|
||||
next = processScalarLike(transition, target, t, cacheEntry, offset, previous);
|
||||
}
|
||||
|
||||
if (next === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modified = true;
|
||||
assign(target, transition.params.property, next, offset);
|
||||
}
|
||||
});
|
||||
return modified ? ret : tree;
|
||||
}
|
||||
|
||||
function applyFrequency(t: number, frequency: number, alternate: boolean) {
|
||||
@@ -163,6 +194,55 @@ function applyFrequency(t: number, frequency: number, alternate: boolean) {
|
||||
return v;
|
||||
}
|
||||
|
||||
function getPreviousScalarEnd(previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (!previous || previous.params.kind === 'transform_matrix') return undefined;
|
||||
return previous.params.end;
|
||||
}
|
||||
|
||||
function processScalarLike(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cacheEntry: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind === 'transform_matrix') return;
|
||||
if (previous && previous.params.kind === 'transform_matrix') return;
|
||||
|
||||
const startBase = transition.params.start ?? getPreviousScalarEnd(previous) ?? select(target, transition.params.property, offset);
|
||||
if (transition.params.kind === 'color' && !cacheEntry.paletteFn) {
|
||||
cacheEntry.paletteFn = makePaletteFunction(transition, startBase, transition.params.end as ColorT | undefined);
|
||||
}
|
||||
|
||||
const paletteFn = cacheEntry.paletteFn!;
|
||||
|
||||
const startValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(0))
|
||||
: startBase;
|
||||
const endValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(1))
|
||||
: transition.params.end;
|
||||
|
||||
if (time <= 0) return startValue;
|
||||
else if (time >= 1 - EPSILON && !transition.params.alternate_direction) return endValue;
|
||||
|
||||
let t = clamp(time, 0, 1);
|
||||
t = applyFrequency(t, transition.params.frequency ?? 1, !!transition.params.alternate_direction);
|
||||
|
||||
const easing = EasingFnMap[transition.params.easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
t = easing(t);
|
||||
|
||||
if (transition.params.kind === 'scalar') {
|
||||
return interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.discrete);
|
||||
} else if (transition.params.kind === 'vec3') {
|
||||
return interpolateVectors(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.spherical);
|
||||
} else if (transition.params.kind === 'rotation_matrix') {
|
||||
return interpolateRotation(startValue, endValue, t, transition.params.noise_magnitude ?? 0, cacheEntry);
|
||||
} else if (transition.params.kind === 'color') {
|
||||
const color = paletteFn(t);
|
||||
return Color.toHexStyle(color);
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviousMatrixEnd(previous: MVSAnimationNode<'interpolate'> | undefined, prop: 'rotation_start' | 'translation_start' | 'scale_start') {
|
||||
if (!previous || previous.params.kind !== 'transform_matrix') return undefined;
|
||||
return previous.params[prop];
|
||||
}
|
||||
|
||||
const TransformState = {
|
||||
pivotTranslation: Mat4(),
|
||||
pivotTranslationInv: Mat4(),
|
||||
@@ -172,31 +252,41 @@ const TransformState = {
|
||||
pivotNeg: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cache: InterpolationCacheEntry) {
|
||||
function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cache: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind !== 'transform_matrix') return;
|
||||
if (previous && previous.params.kind !== 'transform_matrix') return;
|
||||
|
||||
const offset = transition.params.property[0] === 'custom' ? 1 : 0;
|
||||
const transform = select(target, transition.params.property, offset) ?? Mat4.identity();
|
||||
|
||||
const startRotation = transition.params.rotation_start ?? Mat3.fromMat4(Mat3(), transform);
|
||||
const startTranslation = transition.params.translation_start ?? Mat4.getTranslation(Vec3(), transform);
|
||||
const startScale = transition.params.scale_start ?? Mat4.getScaling(Vec3(), transform);
|
||||
const startRotation = transition.params.rotation_start ?? getPreviousMatrixEnd(previous, 'rotation_start') ?? Mat3.fromMat4(Mat3(), transform);
|
||||
const startTranslation = transition.params.translation_start ?? getPreviousMatrixEnd(previous, 'translation_start') ?? Mat4.getTranslation(Vec3(), transform);
|
||||
const startScale = transition.params.scale_start ?? getPreviousMatrixEnd(previous, 'scale_start') ?? Mat4.getScaling(Vec3(), transform);
|
||||
|
||||
const endRotation = transition.params.rotation_end;
|
||||
const endTranslation = transition.params.translation_end;
|
||||
const endScale = transition.params.scale_end;
|
||||
|
||||
let t = applyFrequency(time, transition.params.rotation_frequency ?? 1, !!transition.params.rotation_alternate_direction);
|
||||
let easing = EasingFnMap[transition.params.rotation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
const rotation = interpolateRotation(startRotation as Mat3, endRotation as Mat3, easing(t), transition.params.rotation_noise_magnitude ?? 0, cache);
|
||||
let rotation, translation, scale;
|
||||
|
||||
t = applyFrequency(time, transition.params.translation_frequency ?? 1, !!transition.params.translation_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.translation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
const translation = interpolateVec3(startTranslation as Vec3, endTranslation as Vec3 | undefined, easing(t), transition.params.translation_noise_magnitude ?? 0, false);
|
||||
if (time <= 0) {
|
||||
rotation = startRotation as Mat3;
|
||||
translation = startTranslation as Vec3;
|
||||
scale = startScale as Vec3;
|
||||
} else {
|
||||
const clampedTime = clamp(time, 0, 1);
|
||||
|
||||
t = applyFrequency(time, transition.params.scale_frequency ?? 1, !!transition.params.scale_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.scale_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
const scale = interpolateVec3(startScale as Vec3, endScale as Vec3 | undefined, easing(t), transition.params.scale_noise_magnitude ?? 0, false);
|
||||
let t = applyFrequency(clampedTime, transition.params.rotation_frequency ?? 1, !!transition.params.rotation_alternate_direction);
|
||||
let easing = EasingFnMap[transition.params.rotation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
rotation = interpolateRotation(startRotation as Mat3, endRotation as Mat3, easing(t), transition.params.rotation_noise_magnitude ?? 0, cache);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.translation_frequency ?? 1, !!transition.params.translation_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.translation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
translation = interpolateVec3(startTranslation as Vec3, endTranslation as Vec3 | undefined, easing(t), transition.params.translation_noise_magnitude ?? 0, false);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.scale_frequency ?? 1, !!transition.params.scale_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.scale_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
scale = interpolateVec3(startScale as Vec3, endScale as Vec3 | undefined, easing(t), transition.params.scale_noise_magnitude ?? 0, false);
|
||||
}
|
||||
|
||||
const pivot = transition.params.pivot ?? Vec3.zero();
|
||||
|
||||
@@ -213,21 +303,21 @@ function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, tar
|
||||
Mat4.mul(result, TransformState.rotation, result);
|
||||
Mat4.mul(result, TransformState.translation, result);
|
||||
|
||||
assign(target, transition.params.property, result, offset);
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -235,19 +325,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -412,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;
|
||||
}
|
||||
|
||||
@@ -66,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);
|
||||
|
||||
@@ -133,7 +136,7 @@ async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext,
|
||||
|
||||
for (let i = 0; i < transitions.frames.length; i++) {
|
||||
const frame = transitions.frames[i];
|
||||
const molstarTree = convertMvsToMolstar(frame, options.sourceUrl);
|
||||
const molstarTree = convertMvsToMolstar(frame[0], options.sourceUrl);
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
@@ -145,7 +148,7 @@ async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext,
|
||||
StateTree.reuseTransformParams(entry.snapshot.data!.tree, parentEntry.snapshot.data!.tree);
|
||||
|
||||
animation.frames.push({
|
||||
durationInMs: transitions.frametimeMs,
|
||||
durationInMs: frame[1],
|
||||
data: entry.snapshot.data!,
|
||||
camera: transitions.tree.params?.include_camera ? entry.snapshot.camera : undefined,
|
||||
canvas3d: transitions.tree.params?.include_canvas ? entry.snapshot.canvas3d : undefined,
|
||||
@@ -176,6 +179,10 @@ function molstarTreeToEntry(
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
|
||||
trajectory_with_coordinates: {
|
||||
description: "Auxiliary node corresponding to assigning a separate coordinates to a trajectory.",
|
||||
description: 'Auxiliary node corresponding to assigning a separate coordinates to a trajectory.',
|
||||
parent: ['model'],
|
||||
params: SimpleParamsSchema({
|
||||
coordinates_ref: RequiredField(str, 'Coordinates reference'),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 { create as produce } from 'mutative';
|
||||
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 }),
|
||||
@@ -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);
|
||||
|
||||
@@ -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 { create as produce } from 'mutative';
|
||||
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 { create as produce } from 'mutative';
|
||||
import { produce } from '../../mol-util/produce';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { Camera } from '../camera';
|
||||
|
||||
@@ -175,7 +175,7 @@ export const AnimateStateSnapshotTransition = PluginStateAnimation.create({
|
||||
if (t.current >= animState.totalDuration) {
|
||||
if (snapshot?.transition && animState.isInitial) {
|
||||
const frameIndex = snapshot.transition.frames.length - 1;
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(frameIndex, false);
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(animState.totalDuration, false);
|
||||
await setPartialSnapshot(ctx.plugin, snapshot.transition.frames[frameIndex]);
|
||||
}
|
||||
return { kind: 'finished' };
|
||||
@@ -193,7 +193,7 @@ export const AnimateStateSnapshotTransition = PluginStateAnimation.create({
|
||||
return { kind: 'skip' };
|
||||
}
|
||||
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(frameIndex, false);
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(t.current, false);
|
||||
if (frameIndex === 0) {
|
||||
await setPartialSnapshot(ctx.plugin, {
|
||||
...transition.frames[frameIndex],
|
||||
|
||||
@@ -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,12 @@
|
||||
|
||||
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';
|
||||
import { AnimateStateSnapshotTransition } from '../animation/built-in/state-snapshots';
|
||||
|
||||
export type MarkdownExtensionEvent = 'click' | 'mouse-enter' | 'mouse-leave';
|
||||
|
||||
@@ -45,6 +47,17 @@ export const BuiltInMarkdownExtension: MarkdownExtension[] = [
|
||||
manager.plugin.managers.snapshot.applyKey(key);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'next-snapshot',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('next-snapshot' in args)) return;
|
||||
let dir: -1 | 1 = (+args['next-snapshot'] || 1) as -1 | 1;
|
||||
if (!dir) return;
|
||||
if (dir < 0) dir = -1;
|
||||
else dir = 1;
|
||||
manager.plugin.managers.snapshot.applyNext(dir);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'focus-refs',
|
||||
execute: ({ event, args, manager }) => {
|
||||
@@ -135,7 +148,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 +164,74 @@ 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();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'dispose-audio',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('dispose-audio' in args)) return;
|
||||
manager.audio.dispose();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'play-transition',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('play-transition' in args)) return;
|
||||
manager.plugin.managers.animation.play(AnimateStateSnapshotTransition, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'play-snapshots',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('play-snapshots' in args)) return;
|
||||
manager.plugin.managers.snapshot.play({ restart: true });
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'stop-animation',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('stop-animation' in args)) return;
|
||||
manager.plugin.managers.snapshot.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 +365,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);
|
||||
|
||||
@@ -25,7 +25,7 @@ export { PluginStateSnapshotManager };
|
||||
|
||||
interface StateManagerState {
|
||||
current?: UUID,
|
||||
currentAnimationFrame?: number,
|
||||
currentAnimationTimeMs?: number,
|
||||
entries: List<PluginStateSnapshotManager.Entry>,
|
||||
isPlaying: boolean,
|
||||
nextSnapshotDelayInMs: number
|
||||
@@ -38,8 +38,8 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
private defaultSnapshotId: UUID | undefined = undefined;
|
||||
|
||||
protected updateState(state: Partial<StateManagerState>) {
|
||||
if ('current' in state && !('curentAnimationFrame' in state)) {
|
||||
return super.updateState({ ...state, currentAnimationFrame: 0 });
|
||||
if ('current' in state && !('currentAnimationTimeMs' in state)) {
|
||||
return super.updateState({ ...state, currentAnimationTimeMs: 0 });
|
||||
} else {
|
||||
return super.updateState(state);
|
||||
}
|
||||
@@ -168,18 +168,23 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
}
|
||||
|
||||
private animationFrameQueue = new SingleTaskQueue();
|
||||
setSnapshotAnimationFrame(frame: number, load = false) {
|
||||
if (this.updateState({ currentAnimationFrame: frame })) {
|
||||
setSnapshotAnimationFrame(currentAnimationTimeMs: number, load = false) {
|
||||
const entry = this.getEntry(this.state.current);
|
||||
if (!entry) return;
|
||||
|
||||
const frameIndex = PluginState.getStateTransitionFrameIndex(entry.snapshot, currentAnimationTimeMs) ?? 0;
|
||||
|
||||
if (this.updateState({ currentAnimationTimeMs })) {
|
||||
this.events.changed.next(void 0);
|
||||
}
|
||||
|
||||
if (load) {
|
||||
this.animationFrameQueue.run(() => {
|
||||
const entry = this.getEntry(this.state.current);
|
||||
if (!entry) return Promise.resolve();
|
||||
return this.plugin.state.setAnimationSnapshot(entry.snapshot, frame);
|
||||
return this.plugin.state.setAnimationSnapshot(entry.snapshot, frameIndex ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
return frameIndex;
|
||||
}
|
||||
|
||||
getNextId(id: string | undefined, dir: -1 | 1) {
|
||||
@@ -233,7 +238,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
const next = entry && entry.snapshot;
|
||||
if (!next) return;
|
||||
await this.plugin.state.setSnapshot(next);
|
||||
if (snapshot.playback && snapshot.playback.isPlaying) this.play(true);
|
||||
if (snapshot.playback?.isPlaying) this.play({ delayFirst: true });
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -380,10 +385,18 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
}
|
||||
}
|
||||
|
||||
play(delayFirst: boolean = false) {
|
||||
async play(options?: { delayFirst?: boolean, restart?: boolean }) {
|
||||
if (this.state.isPlaying && !options?.delayFirst) {
|
||||
if (options?.restart) {
|
||||
await this.stop();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateState({ isPlaying: true });
|
||||
|
||||
if (delayFirst) {
|
||||
if (options?.delayFirst) {
|
||||
const e = this.getEntry(this.state.current);
|
||||
if (!e) {
|
||||
this.next();
|
||||
@@ -398,20 +411,20 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.plugin.managers.animation.stop();
|
||||
async stop() {
|
||||
await this.plugin.managers.animation.stop();
|
||||
this.updateState({ isPlaying: false });
|
||||
if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
|
||||
this.timeoutHandle = void 0;
|
||||
this.events.changed.next(void 0);
|
||||
}
|
||||
|
||||
togglePlay() {
|
||||
async togglePlay() {
|
||||
if (this.state.isPlaying) {
|
||||
this.stop();
|
||||
this.plugin.managers.animation.stop();
|
||||
await this.stop();
|
||||
this.plugin.managers.markdownExtensions.audio.pause();
|
||||
} else {
|
||||
this.play();
|
||||
await this.play();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { StateTransformParameters } from './state/common';
|
||||
|
||||
export class PluginUIContext extends PluginContext {
|
||||
readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
|
||||
readonly customUIState: Record<string, any> = {};
|
||||
|
||||
private initCustomParamEditors() {
|
||||
if (!this.spec.customParamEditors) return;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { StructureQuickStylesControls } from './structure/quick-styles';
|
||||
import { Markdown } from './controls/markdown';
|
||||
import { Slider } from './controls/slider';
|
||||
import { AnimateStateSnapshotTransition } from '../mol-plugin-state/animation/built-in/state-snapshots';
|
||||
import { PluginState } from '../mol-plugin/state';
|
||||
|
||||
export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
|
||||
state = { show: false, label: '' };
|
||||
@@ -61,7 +62,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 +112,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 +152,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,19 +187,17 @@ 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'>
|
||||
<Slider
|
||||
value={snapshots.state.currentAnimationFrame ?? 0}
|
||||
min={1}
|
||||
step={1}
|
||||
max={(entry?.snapshot.transition?.frames.length ?? 1)}
|
||||
value={Math.round(100 * (snapshots.state.currentAnimationTimeMs ?? 0)) /100}
|
||||
min={0}
|
||||
step={PluginState.getMinFrameDuration(entry?.snapshot)}
|
||||
max={PluginState.getStateTransitionDuration(entry?.snapshot) ?? 1000}
|
||||
onChange={() => { }}
|
||||
onChangeImmediate={v => {
|
||||
snapshots.setSnapshotAnimationFrame(v - 1, true);
|
||||
}}
|
||||
onChangeImmediate={v => snapshots.setSnapshotAnimationFrame(v, true)}
|
||||
hideInput
|
||||
disabled={this.state.isBusy}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import { elementLabel } from '../mol-theme/label';
|
||||
import { Icon, HelpOutlineSvg } from './controls/icons';
|
||||
import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
|
||||
import { arrayEqual } from '../mol-util/array';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ThemeProvider } from '../mol-theme/theme';
|
||||
|
||||
const MaxDisplaySequenceLength = 5000;
|
||||
// TODO: add virtualized Select controls (at best with a search box)?
|
||||
@@ -244,6 +246,13 @@ export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceView
|
||||
}
|
||||
});
|
||||
|
||||
const experimentalSequenceTheme = this.plugin.customUIState.experimentalSequenceTheme as Observable<ThemeProvider<any, any> | undefined> | undefined;
|
||||
if (experimentalSequenceTheme) {
|
||||
this.subscribe(experimentalSequenceTheme as Observable<ThemeProvider<any, any>>, theme => {
|
||||
// do stuff
|
||||
});
|
||||
}
|
||||
|
||||
const modeOptions = this.plugin.spec.components?.sequenceViewer?.modeOptions;
|
||||
if (modeOptions) {
|
||||
const modeSet = new Set(modeOptions);
|
||||
|
||||
@@ -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 { create as produce } from 'mutative';
|
||||
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' }
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { create as produce } from 'mutative';
|
||||
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();
|
||||
|
||||
@@ -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 { create as produce } from 'mutative';
|
||||
import { produce } from '../mol-util/produce';
|
||||
import { merge } from 'rxjs';
|
||||
import { Camera } from '../mol-canvas3d/camera';
|
||||
import { Canvas3DContext, Canvas3DParams, Canvas3DProps } from '../mol-canvas3d/canvas3d';
|
||||
@@ -27,6 +27,7 @@ import { PluginConfig } from './config';
|
||||
import { PluginContext } from './context';
|
||||
import { AnimateStateSnapshotTransition } from '../mol-plugin-state/animation/built-in/state-snapshots';
|
||||
import { Scheduler } from '../mol-task';
|
||||
import { memoizeLatest } from '../mol-util/memoize';
|
||||
|
||||
export { PluginState };
|
||||
|
||||
@@ -118,6 +119,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 +153,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 +251,7 @@ namespace PluginState {
|
||||
},
|
||||
durationInMs?: number,
|
||||
transition?: StateTransition,
|
||||
onLoadMarkdownCommands?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface StateTransition {
|
||||
@@ -254,7 +265,23 @@ namespace PluginState {
|
||||
}[],
|
||||
}
|
||||
|
||||
export function getStateTransitionDuration(snapshot: Snapshot): number | undefined {
|
||||
export const getMinFrameDuration = memoizeLatest((snapshot: Snapshot | undefined): number => {
|
||||
if (!snapshot) return 1000 / 60;
|
||||
const { transition } = snapshot;
|
||||
if (!transition) return 1000 / 60;
|
||||
|
||||
let minDuration = Infinity;
|
||||
for (const frame of transition.frames) {
|
||||
if (frame.durationInMs > 0 && frame.durationInMs < minDuration) {
|
||||
minDuration = frame.durationInMs;
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(minDuration)) return 1000 / 60;
|
||||
return minDuration;
|
||||
});
|
||||
|
||||
export const getStateTransitionDuration = memoizeLatest((snapshot: Snapshot | undefined): number | undefined => {
|
||||
if (!snapshot) return undefined;
|
||||
const { transition } = snapshot;
|
||||
if (!transition) return undefined;
|
||||
let totalDuration = 0;
|
||||
@@ -263,7 +290,20 @@ namespace PluginState {
|
||||
totalDuration += frame.durationInMs;
|
||||
}
|
||||
return totalDuration;
|
||||
}
|
||||
});
|
||||
|
||||
export const getStateTransitionFrameTime = memoizeLatest((snapshot: Snapshot | undefined, frameIndex: number | undefined): number => {
|
||||
if (!snapshot || frameIndex === undefined) return 0;
|
||||
const { transition } = snapshot;
|
||||
if (!transition) return 0;
|
||||
let currentDuration = 0;
|
||||
for (let i = 0; i < frameIndex; i++) {
|
||||
if (transition.frames.length <= i) break;
|
||||
const frame = transition.frames[i];
|
||||
currentDuration += frame.durationInMs;
|
||||
}
|
||||
return currentDuration;
|
||||
});
|
||||
|
||||
export function getStateTransitionFrameIndex(snapshot: Snapshot, timestamp: number): number | undefined {
|
||||
const { transition } = snapshot;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { StateObject, StateObjectCell, StateObjectSelector, StateObjectRef } fro
|
||||
import { StateTransform } from '../transform';
|
||||
import { StateTransformer } from '../transformer';
|
||||
import { State } from '../state';
|
||||
import { create as produce } from 'mutative';
|
||||
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 { create as produce } from 'mutative';
|
||||
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