UNPKG

molstar

Version:

A comprehensive macromolecular library.

342 lines (341 loc) 18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const jsx_runtime_1 = require("react/jsx-runtime"); /** * Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ const react_1 = require("react"); const client_1 = require("react-dom/client"); const rxjs_1 = require("rxjs"); const ligand_graph_1 = require("../../extensions/json-cif/ligand-graph"); const transformers_1 = require("../../extensions/json-cif/transformers"); const behavior_1 = require("../../extensions/mvs/behavior"); const structure_1 = require("../../mol-model/structure"); const objects_1 = require("../../mol-plugin-state/objects"); const model_1 = require("../../mol-plugin-state/transforms/model"); const representation_1 = require("../../mol-plugin-state/transforms/representation"); const context_1 = require("../../mol-plugin-ui/context"); const use_behavior_1 = require("../../mol-plugin-ui/hooks/use-behavior"); const plugin_1 = require("../../mol-plugin-ui/plugin"); require("../../mol-plugin-ui/skin/light.scss"); const spec_1 = require("../../mol-plugin-ui/spec"); const commands_1 = require("../../mol-plugin/commands"); const config_1 = require("../../mol-plugin/config"); const spec_2 = require("../../mol-plugin/spec"); const download_1 = require("../../mol-util/download"); const edits_1 = require("./edits"); const example_data_1 = require("./example-data"); require("./index.html"); const molfile_1 = require("./molfile"); const utils_1 = require("./utils"); const utils_2 = require("../../extensions/json-cif/utils"); async function init(target, molfile = example_data_1.ExampleMol) { const root = typeof target === 'string' ? document.getElementById(target) : target; const plugin = await createViewer(root); const model = new EditorModel(plugin); (0, client_1.createRoot)(root).render((0, jsx_runtime_1.jsx)(AppUI, { model: model })); loadMolfile(model, molfile); return model; } window.initLigandEditorExample = init; async function createViewer(root) { const spec = (0, spec_1.DefaultPluginUISpec)(); const plugin = new context_1.PluginUIContext({ ...spec, layout: { initial: { isExpanded: false, showControls: false } }, components: { remoteState: 'none', }, behaviors: [ ...spec.behaviors, spec_2.PluginSpec.Behavior(behavior_1.MolViewSpec) ], config: [ [config_1.PluginConfig.Viewport.ShowAnimation, false], [config_1.PluginConfig.Viewport.ShowSelectionMode, false], [config_1.PluginConfig.Viewport.ShowExpand, false], [config_1.PluginConfig.Viewport.ShowControls, false], ] }); await plugin.init(); plugin.managers.interactivity.setProps({ granularity: 'element' }); plugin.selectionMode = true; return plugin; } async function loadMolfile(model, molfile) { const { plugin } = model; await plugin.clear(); const file = await (0, utils_2.molfileToJSONCif)(molfile); const update = plugin.build(); const data = update.toRoot() .apply(transformers_1.ParseJSONCifFileData, { data: file.jsoncif }); data .apply(model_1.TrajectoryFromMmCif) .apply(model_1.ModelFromTrajectory) .apply(model_1.StructureFromModel, { type: { name: 'model', params: {} } }) .apply(representation_1.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: {} }, colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } } }); await update.commit(); model.setDataSelector(data.selector); } class EditorModel { get data() { var _a, _b, _c, _d; return (_d = (_c = (_b = (_a = this.dataSelector) === null || _a === void 0 ? void 0 : _a.cell) === null || _b === void 0 ? void 0 : _b.transform) === null || _c === void 0 ? void 0 : _c.params) === null || _d === void 0 ? void 0 : _d.data; } get history() { return this.state.history.value; } createGraph() { var _a; return new ligand_graph_1.JSONCifLigandGraph((_a = this.data) === null || _a === void 0 ? void 0 : _a.dataBlocks[0]); } setDataSelector(selector) { this.dataSelector = selector; this.updateMolFile(); } updateMolFile() { var _a; if (!this.data) return this.state.molfile.next(''); try { const molfile = (0, molfile_1.jsonCifToMolfile)((_a = this.data) === null || _a === void 0 ? void 0 : _a.dataBlocks[0], { comment: 'Generated by Mol* Ligand Editor' }); this.state.molfile.next(molfile); } catch (e) { console.error('Failed to convert to molfile'); console.error(e); this.state.molfile.next(`Error: ${e}`); } } async update(data, pushHistory = true, historyData) { if (!this.data) return; const updated = { ...this.data, dataBlocks: [data], }; if (pushHistory) { this.state.history.next([...this.history, historyData !== null && historyData !== void 0 ? historyData : this.data]); } const update = this.plugin.build(); update.to(this.dataSelector).update({ data: updated }); await update.commit(); this.updateMolFile(); } getEditableStructures() { var _a; if (!((_a = this.dataSelector) === null || _a === void 0 ? void 0 : _a.isOk)) return new Set(); const structures = this.plugin.state.data.selectQ(q => { var _a; return q .byRef((_a = this.dataSelector) === null || _a === void 0 ? void 0 : _a.ref) .subtree() .filter(c => objects_1.PluginStateObject.Molecule.Structure.is(c.obj)); }); return new Set(structures.map(s => { var _a; return (_a = s.obj) === null || _a === void 0 ? void 0 : _a.data; })); } getSelectedAtomIds() { if (!this.data) return []; const structures = this.getEditableStructures(); if (structures.size === 0) return []; const { selection } = this.plugin.managers.structure; const ids = []; selection.entries.forEach(e => { if (!structures.has(e.selection.structure)) return; structure_1.StructureElement.Loci.forEachLocation(e.selection, (l) => { ids.push(structure_1.StructureProperties.atom.id(l)); }); }); return ids; } async editGraphTopology(fn, ...args) { try { const graph = this.createGraph(); const result = await fn(graph, ...args); const data = graph.getData().block; await this.update(data); this.plugin.managers.interactivity.lociSelects.deselectAll(); return result; } catch (e) { console.error('Failed to edit graph'); console.error(e); this.notify(`${e}`, 5000); } } notify(message, timeoutMs = 2500) { commands_1.PluginCommands.Toast.Show(this.plugin, { key: '<edit>', title: 'Edit', message, timeoutMs }); } beginGeometryEdit(fn, initial, ...args) { try { this.geometryEditValues.next([initial, false]); const graph = this.createGraph(); this.geometryEditInitialData = this.data; this.currentGeometryEdit = fn(graph, ...args); this.currentGeomeryEditSub = this.geometryEditValues .pipe((0, rxjs_1.throttleTime)(1000 / 60, undefined, { leading: true, trailing: true })) .subscribe(this.applyGeometryEdit); } catch (e) { console.error('Failed to edit graph'); console.error(e); this.notify(`${e}`, 5000); } } setGeometryEditValue(param, finish = false) { this.geometryEditValues.next([param, finish]); } constructor(plugin) { this.plugin = plugin; this.dataSelector = undefined; this.state = { element: new rxjs_1.BehaviorSubject('C'), history: new rxjs_1.BehaviorSubject([]), molfile: new rxjs_1.BehaviorSubject(''), }; this.undo = async () => { if (!this.dataSelector) return; if (this.history.length === 0) return; const data = this.history[this.history.length - 1]; this.state.history.next(this.history.slice(0, this.history.length - 1)); const update = this.plugin.build(); update.to(this.dataSelector).update({ data }); await update.commit(); this.updateMolFile(); }; this.setElement = async () => { const symbol = this.state.element.value.trim(); if (!symbol) return this.notify('No element symbol provided'); const ids = this.getSelectedAtomIds(); if (!ids.length) return this.notify('No atoms selected'); await this.editGraphTopology(edits_1.TopologyEdits.setElement, ids, symbol); }; this.addElement = async () => { const symbol = this.state.element.value.trim(); if (!symbol) return this.notify('No element symbol provided'); const ids = this.getSelectedAtomIds(); if (ids.length !== 1) return this.notify('Select a single atom to add a new atom to'); await this.editGraphTopology(edits_1.TopologyEdits.addElement, ids[0], symbol); }; this.removeAtoms = async () => { const ids = this.getSelectedAtomIds(); if (!ids.length) return this.notify('No atoms selected'); await this.editGraphTopology(edits_1.TopologyEdits.removeAtoms, ids); }; this.removeBonds = async () => { const ids = this.getSelectedAtomIds(); if (!ids.length) return this.notify('No atoms selected'); await this.editGraphTopology(edits_1.TopologyEdits.removeBonds, ids); }; this.updateBonds = async (props) => { const ids = this.getSelectedAtomIds(); if (!ids.length) return this.notify('No atoms selected'); await this.editGraphTopology(edits_1.TopologyEdits.updateBonds, ids, props); }; this.attachRgroup = async (name) => { const ids = this.getSelectedAtomIds(); if (ids.length !== 1) return this.notify('Select a single hydrogen atom to attach an R-group to'); await this.editGraphTopology(edits_1.TopologyEdits.attachRgroup, ids[0], name); }; this.geometryEditInitialData = undefined; this.geometryEditValues = new rxjs_1.BehaviorSubject([0, false]); this.currentGeometryEdit = undefined; this.currentGeomeryEditSub = undefined; this.geometryEditQueue = new utils_1.SingleTaskQueue(); this.applyGeometryEdit = ([param, finish]) => { var _a; if (!this.currentGeometryEdit) return; const graph = this.currentGeometryEdit(param); const data = graph.getData().block; const initialData = this.geometryEditInitialData; if (finish) { this.currentGeometryEdit = undefined; (_a = this.currentGeomeryEditSub) === null || _a === void 0 ? void 0 : _a.unsubscribe(); this.currentGeomeryEditSub = undefined; this.geometryEditInitialData = undefined; } this.geometryEditQueue.run(() => this.update(data, finish, initialData)); }; this.twist = () => { this.beginGeometryEdit(edits_1.GeometryEdits.twist, 0, this.getSelectedAtomIds()); }; this.stretch = () => { this.beginGeometryEdit(edits_1.GeometryEdits.stretch, 0, this.getSelectedAtomIds()); }; } } function AppUI({ model }) { return (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexDirection: 'row', height: '100%', width: '100%' }, children: [(0, jsx_runtime_1.jsx)("div", { style: { flexGrow: 1, display: 'block', position: 'relative' }, children: (0, jsx_runtime_1.jsx)(plugin_1.Plugin, { plugin: model.plugin }) }), (0, jsx_runtime_1.jsx)("div", { style: { flexShrink: 0, minWidth: 500, width: 400, display: 'flex', flexDirection: 'column', gap: '5px' }, children: (0, jsx_runtime_1.jsx)(ControlsUI, { model: model }) })] }); } function ControlsUI({ model }) { return (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', flexDirection: 'column', gap: '5px', padding: 8, overflow: 'hidden', overflowY: 'auto' }, className: 'editor-controls', children: [(0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(UndoButton, { model: model }) }), (0, jsx_runtime_1.jsx)("b", { children: "Atoms" }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: '5px' }, children: [(0, jsx_runtime_1.jsx)("button", { onClick: model.removeAtoms, children: "Remove" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ElementEditUI, { model: model }), (0, jsx_runtime_1.jsx)("button", { onClick: model.setElement, style: { borderLeft: 'none' }, children: "Set Element" }), (0, jsx_runtime_1.jsx)("button", { onClick: model.addElement, style: { borderLeft: 'none' }, children: "Add Element" })] })] }), (0, jsx_runtime_1.jsx)("b", { children: "Bonds" }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: '5px' }, children: [(0, jsx_runtime_1.jsx)("button", { onClick: model.removeBonds, children: "Remove" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => model.updateBonds({ value_order: 'sing', type_id: 'covale' }), children: "-" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => model.updateBonds({ value_order: 'doub', type_id: 'covale' }), children: "=" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => model.updateBonds({ value_order: 'trip', type_id: 'covale' }), children: "\u2261" })] }), (0, jsx_runtime_1.jsx)("b", { children: "R-groups" }), (0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', gap: '5px' }, children: (0, jsx_runtime_1.jsxs)("button", { onClick: () => model.attachRgroup('CH3'), children: ["-CH", (0, jsx_runtime_1.jsx)("sub", { children: "3" })] }) }), (0, jsx_runtime_1.jsx)("b", { children: "Geometry" }), (0, jsx_runtime_1.jsx)(TwistUI, { model: model }), (0, jsx_runtime_1.jsx)(StretchUI, { model: model }), (0, jsx_runtime_1.jsx)("b", { children: "Molfile" }), (0, jsx_runtime_1.jsx)(MolFileUI, { model: model })] }); } function MolFileUI({ model }) { const molfile = (0, use_behavior_1.useBehavior)(model.state.molfile); return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("textarea", { value: molfile, readOnly: true, style: { width: '100%', height: 200, fontFamily: 'monospace', fontSize: '10px' } }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: '5px' }, children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => navigator.clipboard.writeText(molfile), children: "Copy" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => (0, download_1.download)(new Blob([molfile], { type: 'text/plain' }), `edited-molecule-${Date.now()}.mol`), children: "Save" })] })] }); } function UndoButton({ model }) { const history = (0, use_behavior_1.useBehavior)(model.state.history); return (0, jsx_runtime_1.jsxs)("button", { onClick: model.undo, disabled: history.length === 0, children: ["Undo [", history.length, "]"] }); } function ElementEditUI({ model }) { const element = (0, use_behavior_1.useBehavior)(model.state.element); return (0, jsx_runtime_1.jsx)("input", { type: "text", value: element, style: { width: 50 }, onChange: e => model.state.element.next(e.target.value) }); } const GeometryLabelWidth = 60; function TwistUI({ model }) { const [value, setValue] = (0, react_1.useState)(0); return (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("i", { style: { width: GeometryLabelWidth }, children: "Twist" }), " ", (0, jsx_runtime_1.jsx)("input", { type: 'range', min: -60, max: 60, step: 1, value: value, onMouseDown: model.twist, onMouseUp: (e) => { requestAnimationFrame(() => { model.setGeometryEditValue(Math.PI * value / 60, true); setValue(0); }); }, onChange: e => { const value = +e.target.value; setValue(value); model.setGeometryEditValue(Math.PI * value / 60); } })] }); } function StretchUI({ model }) { const [value, setValue] = (0, react_1.useState)(0); return (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [(0, jsx_runtime_1.jsx)("i", { style: { width: GeometryLabelWidth }, children: "Stretch" }), " ", (0, jsx_runtime_1.jsx)("input", { type: 'range', min: -60, max: 60, step: 1, value: value, onMouseDown: model.stretch, onMouseUp: (e) => { requestAnimationFrame(() => { model.setGeometryEditValue(0.5 * value / 60, true); setValue(0); }); }, onChange: e => { const value = +e.target.value; setValue(value); model.setGeometryEditValue(0.5 * value / 60); } })] }); }