UNPKG

molstar

Version:

A comprehensive macromolecular library.

301 lines (300 loc) 17.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DefaultStructureTools = exports.CustomStructureControls = exports.LociLabels = exports.SelectionViewportControls = exports.AnimationViewportControls = exports.StateSnapshotViewportControls = exports.TrajectoryViewportControls = void 0; exports.ViewportSnapshotDescription = ViewportSnapshotDescription; exports.MarkdownAnchor = MarkdownAnchor; const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); /** * 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> */ const React = tslib_1.__importStar(require("react")); const react_markdown_1 = tslib_1.__importDefault(require("react-markdown")); const structure_1 = require("../mol-plugin-state/actions/structure"); const transforms_1 = require("../mol-plugin-state/transforms"); const commands_1 = require("../mol-plugin/commands"); const base_1 = require("./base"); const common_1 = require("./controls/common"); const icons_1 = require("./controls/icons"); const animation_1 = require("./state/animation"); const components_1 = require("./structure/components"); const measurements_1 = require("./structure/measurements"); const selection_1 = require("./structure/selection"); const source_1 = require("./structure/source"); const volume_1 = require("./structure/volume"); const config_1 = require("../mol-plugin/config"); const superposition_1 = require("./structure/superposition"); const quick_styles_1 = require("./structure/quick-styles"); class TrajectoryViewportControls extends base_1.PluginUIComponent { constructor() { super(...arguments); this.state = { show: false, label: '' }; this.update = () => { const state = this.plugin.state.data; const models = state.selectQ(q => q.ofTransformer(transforms_1.StateTransforms.Model.ModelFromTrajectory)); if (models.length === 0) { this.setState({ show: false }); return; } let label = '', count = 0; const parents = new Set(); for (const m of models) { if (!m.sourceRef) continue; const parent = state.cells.get(m.sourceRef).obj; if (!parent) continue; if (parent.data.frameCount > 1) { if (parents.has(m.sourceRef)) { // do not show the controls if there are 2 models of the same trajectory present this.setState({ show: false }); return; } parents.add(m.sourceRef); count++; if (!label) { const idx = m.transform.params.modelIndex; label = `Model ${idx + 1} / ${parent.data.frameCount}`; } } } if (count > 1) label = ''; this.setState({ show: count > 0, label }); }; this.reset = () => commands_1.PluginCommands.State.ApplyAction(this.plugin, { state: this.plugin.state.data, action: structure_1.UpdateTrajectory.create({ action: 'reset' }) }); this.prev = () => commands_1.PluginCommands.State.ApplyAction(this.plugin, { state: this.plugin.state.data, action: structure_1.UpdateTrajectory.create({ action: 'advance', by: -1 }) }); this.next = () => commands_1.PluginCommands.State.ApplyAction(this.plugin, { state: this.plugin.state.data, action: structure_1.UpdateTrajectory.create({ action: 'advance', by: 1 }) }); } componentDidMount() { this.subscribe(this.plugin.state.data.events.changed, this.update); this.subscribe(this.plugin.behaviors.state.isAnimating, this.update); } render() { const isAnimating = this.plugin.behaviors.state.isAnimating.value; if (!this.state.show || (isAnimating && !this.state.label) || !this.plugin.config.get(config_1.PluginConfig.Viewport.ShowTrajectoryControls)) return null; return (0, jsx_runtime_1.jsxs)("div", { className: 'msp-traj-controls', children: [!isAnimating && (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: icons_1.SkipPreviousSvg, title: 'First Model', onClick: this.reset, disabled: isAnimating }), !isAnimating && (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: icons_1.NavigateBeforeSvg, title: 'Previous Model', onClick: this.prev, disabled: isAnimating }), !isAnimating && (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: icons_1.NavigateNextSvg, title: 'Next Model', onClick: this.next, disabled: isAnimating }), !!this.state.label && (0, jsx_runtime_1.jsx)("span", { children: this.state.label })] }); } } exports.TrajectoryViewportControls = TrajectoryViewportControls; class StateSnapshotViewportControls extends base_1.PluginUIComponent { constructor() { super(...arguments); this.state = { isBusy: false, show: true }; this.keyUp = (e) => { 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); } }; this.change = (e) => { if (e.target.value === 'none') return; this.update(e.target.value); }; this.prev = () => { const s = this.plugin.managers.snapshot; const id = s.getNextId(s.state.current, -1); if (id) this.update(id); }; this.next = () => { const s = this.plugin.managers.snapshot; const id = s.getNextId(s.state.current, 1); if (id) this.update(id); }; this.togglePlay = () => { this.plugin.managers.snapshot.togglePlay(); }; } componentDidMount() { // TODO: this needs to be diabled when the state is updating! 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); } async update(id) { this.setState({ isBusy: true }); await commands_1.PluginCommands.State.Snapshots.Apply(this.plugin, { id }); this.setState({ isBusy: false }); } render() { const snapshots = this.plugin.managers.snapshot; const count = snapshots.state.entries.size; if (count < 2 || !this.state.show) { return null; } const current = snapshots.state.current; const isPlaying = snapshots.state.isPlaying; return (0, jsx_runtime_1.jsxs)("div", { className: 'msp-state-snapshot-viewport-controls', children: [(0, jsx_runtime_1.jsxs)("select", { className: 'msp-form-control', value: current || 'none', onChange: this.change, disabled: this.state.isBusy || isPlaying, children: [!current && (0, jsx_runtime_1.jsx)("option", { value: 'none' }, 'none'), snapshots.state.entries.valueSeq().map((e, i) => (0, jsx_runtime_1.jsxs)("option", { value: e.snapshot.id, children: [`[${i + 1}/${count}]`, " ", e.name || new Date(e.timestamp).toLocaleString()] }, e.snapshot.id))] }), (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: isPlaying ? icons_1.StopSvg : icons_1.PlayArrowSvg, title: isPlaying ? 'Pause' : 'Cycle States', onClick: this.togglePlay, disabled: isPlaying ? false : this.state.isBusy }), !isPlaying && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: icons_1.NavigateBeforeSvg, title: 'Previous State', onClick: this.prev, disabled: this.state.isBusy || isPlaying }), (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: icons_1.NavigateNextSvg, title: 'Next State', onClick: this.next, disabled: this.state.isBusy || isPlaying })] })] }); } } exports.StateSnapshotViewportControls = StateSnapshotViewportControls; function ViewportSnapshotDescription() { var _a; const plugin = React.useContext(base_1.PluginReactContext); const [_, setV] = React.useState(0); React.useEffect(() => { const sub = plugin.managers.snapshot.events.changed.subscribe(() => setV(v => v + 1)); return () => sub.unsubscribe(); }, [plugin]); const current = plugin.managers.snapshot.state.current; if (!current) return null; const e = plugin.managers.snapshot.getEntry(current); if (!((_a = e === null || e === void 0 ? void 0 : e.description) === null || _a === void 0 ? void 0 : _a.trim())) return null; return (0, jsx_runtime_1.jsx)("div", { id: 'snapinfo', className: 'msp-snapshot-description-wrapper', children: e.descriptionFormat === 'plaintext' && e.description || (0, jsx_runtime_1.jsx)(react_markdown_1.default, { skipHtml: true, components: { a: MarkdownAnchor }, children: e.description }) }); } function MarkdownAnchor({ href, children, element }) { const plugin = React.useContext(base_1.PluginReactContext); if (!href) return element; if (href[0] === '#') { return (0, jsx_runtime_1.jsx)("a", { href: '#', onClick: (e) => { e.preventDefault(); plugin.managers.snapshot.applyKey(href.substring(1)); }, children: children }); } else if (href) { return (0, jsx_runtime_1.jsx)("a", { href: href, target: '_blank', rel: 'noopener noreferrer', children: children }); } // TODO: consider adding more "commands", for example !reset-camera return children; } class AnimationViewportControls extends base_1.PluginUIComponent { constructor() { super(...arguments); this.state = { isEmpty: true, isExpanded: false, isBusy: false, isAnimating: false, isPlaying: false }; this.toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); this.stop = () => { this.plugin.managers.animation.stop(); this.plugin.managers.snapshot.stop(); }; } componentDidMount() { this.subscribe(this.plugin.managers.snapshot.events.changed, () => { if (this.plugin.managers.snapshot.state.isPlaying) this.setState({ isPlaying: true, isExpanded: false }); else this.setState({ isPlaying: false }); }); this.subscribe(this.plugin.behaviors.state.isBusy, isBusy => { if (isBusy) this.setState({ isBusy: true, isExpanded: false, isEmpty: this.plugin.state.data.tree.transforms.size < 2 }); else this.setState({ isBusy: false, isEmpty: this.plugin.state.data.tree.transforms.size < 2 }); }); this.subscribe(this.plugin.behaviors.state.isAnimating, isAnimating => { if (isAnimating) this.setState({ isAnimating: true, isExpanded: false }); else this.setState({ isAnimating: false }); }); } render() { const isPlaying = this.plugin.managers.snapshot.state.isPlaying; if (isPlaying || this.state.isEmpty || this.plugin.managers.animation.isEmpty || !this.plugin.config.get(config_1.PluginConfig.Viewport.ShowAnimation)) return null; const isAnimating = this.state.isAnimating; return (0, jsx_runtime_1.jsxs)("div", { className: 'msp-animation-viewport-controls', children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: 'msp-semi-transparent-background' }), (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: isAnimating || isPlaying ? icons_1.StopSvg : icons_1.SubscriptionsOutlinedSvg, transparent: true, title: isAnimating ? 'Stop' : 'Select Animation', onClick: isAnimating || isPlaying ? this.stop : this.toggleExpanded, toggleState: this.state.isExpanded, disabled: isAnimating || isPlaying ? false : this.state.isBusy || this.state.isPlaying || this.state.isEmpty })] }), (this.state.isExpanded && !this.state.isBusy) && (0, jsx_runtime_1.jsx)("div", { className: 'msp-animation-viewport-controls-select', children: (0, jsx_runtime_1.jsx)(animation_1.AnimationControls, { onStart: this.toggleExpanded }) })] }); } } exports.AnimationViewportControls = AnimationViewportControls; class SelectionViewportControls extends base_1.PluginUIComponent { componentDidMount() { this.subscribe(this.plugin.behaviors.interaction.selectionMode, () => this.forceUpdate()); } render() { if (!this.plugin.selectionMode) return null; return (0, jsx_runtime_1.jsx)("div", { className: 'msp-selection-viewport-controls', children: (0, jsx_runtime_1.jsx)(selection_1.StructureSelectionActionsControls, {}) }); } } exports.SelectionViewportControls = SelectionViewportControls; class LociLabels extends base_1.PluginUIComponent { constructor() { super(...arguments); this.state = { labels: [] }; } componentDidMount() { this.subscribe(this.plugin.behaviors.labels.highlight, e => this.setState({ labels: e.labels })); } render() { if (this.state.labels.length === 0) { return null; } return (0, jsx_runtime_1.jsx)("div", { className: 'msp-highlight-info', children: this.state.labels.map((e, i) => { if (e.indexOf('\n') >= 0) { return (0, jsx_runtime_1.jsx)("div", { className: 'msp-highlight-markdown-row', children: (0, jsx_runtime_1.jsx)(react_markdown_1.default, { skipHtml: true, children: e }) }, '' + i); } return (0, jsx_runtime_1.jsx)("div", { className: 'msp-highlight-simple-row', dangerouslySetInnerHTML: { __html: e } }, '' + i); }) }); } } exports.LociLabels = LociLabels; class CustomStructureControls extends base_1.PluginUIComponent { componentDidMount() { this.subscribe(this.plugin.state.behaviors.events.changed, () => this.forceUpdate()); } render() { const controls = []; this.plugin.customStructureControls.forEach((Controls, key) => { controls.push((0, jsx_runtime_1.jsx)(Controls, { initiallyCollapsed: this.props.initiallyCollapsed }, key)); }); return controls.length > 0 ? (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: controls }) : null; } } exports.CustomStructureControls = CustomStructureControls; class DefaultStructureTools extends base_1.PluginUIComponent { render() { return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: 'msp-section-header', children: [(0, jsx_runtime_1.jsx)(icons_1.Icon, { svg: icons_1.BuildSvg }), "Structure Tools"] }), (0, jsx_runtime_1.jsx)(source_1.StructureSourceControls, {}), (0, jsx_runtime_1.jsx)(measurements_1.StructureMeasurementsControls, {}), (0, jsx_runtime_1.jsx)(superposition_1.StructureSuperpositionControls, {}), (0, jsx_runtime_1.jsx)(quick_styles_1.StructureQuickStylesControls, {}), (0, jsx_runtime_1.jsx)(components_1.StructureComponentControls, {}), this.plugin.config.get(config_1.PluginConfig.VolumeStreaming.Enabled) && (0, jsx_runtime_1.jsx)(volume_1.VolumeStreamingControls, {}), (0, jsx_runtime_1.jsx)(volume_1.VolumeSourceControls, {}), (0, jsx_runtime_1.jsx)(CustomStructureControls, {})] }); } } exports.DefaultStructureTools = DefaultStructureTools;