molstar
Version:
A comprehensive macromolecular library.
215 lines (214 loc) • 9.95 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { OrderedSet, SortedArray } from '../../mol-data/int';
import { StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
import { lociLabel } from '../../mol-theme/label';
import { Binding } from '../../mol-util/binding';
import { memoizeLatest } from '../../mol-util/memoize';
import { PluginUIComponent } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { Button, IconButton, ToggleButton } from '../controls/common';
import { CancelOutlinedSvg, CenterFocusStrongSvg } from '../controls/icons';
function addSymmetryGroupEntries(entries, location, unitSymmetryGroup, granularity) {
const idx = SortedArray.indexOf(location.unit.elements, location.element);
const base = StructureElement.Loci(location.structure, [
{ unit: location.unit, indices: OrderedSet.ofSingleton(idx) }
]);
const extended = granularity === 'residue'
? StructureElement.Loci.extendToWholeResidues(base)
: StructureElement.Loci.extendToWholeChains(base);
const name = StructureProperties.entity.pdbx_description(location).join(', ');
for (const u of unitSymmetryGroup.units) {
const loci = StructureElement.Loci(extended.structure, [
{ unit: u, indices: extended.elements[0].indices }
]);
let label = lociLabel(loci, { reverse: true, hidePrefix: true, htmlStyling: false, granularity });
if (!label)
label = lociLabel(loci, { hidePrefix: false, htmlStyling: false });
if (unitSymmetryGroup.units.length > 1) {
label += ` | ${loci.elements[0].unit.conformation.operator.name}`;
}
const item = { label, category: name, loci };
if (entries.has(name))
entries.get(name).push(item);
else
entries.set(name, [item]);
}
}
function getFocusEntries(structure) {
const entityEntries = new Map();
const l = StructureElement.Location.create(structure);
for (const ug of structure.unitSymmetryGroups) {
if (!Unit.isAtomic(ug.units[0]))
continue;
l.unit = ug.units[0];
l.element = ug.elements[0];
const isMultiChain = Unit.Traits.is(l.unit.traits, Unit.Trait.MultiChain);
const entityType = StructureProperties.entity.type(l);
const isNonPolymer = entityType === 'non-polymer';
const isBranched = entityType === 'branched';
const isBirdMolecule = !!StructureProperties.entity.prd_id(l);
if (isBirdMolecule) {
addSymmetryGroupEntries(entityEntries, l, ug, 'chain');
}
else if (isNonPolymer && !isMultiChain) {
addSymmetryGroupEntries(entityEntries, l, ug, 'residue');
}
else if (isBranched || (isNonPolymer && isMultiChain)) {
const u = l.unit;
const { index: residueIndex } = u.model.atomicHierarchy.residueAtomSegments;
let prev = -1;
for (let i = 0, il = u.elements.length; i < il; ++i) {
const eI = u.elements[i];
const rI = residueIndex[eI];
if (rI !== prev) {
l.element = eI;
addSymmetryGroupEntries(entityEntries, l, ug, 'residue');
prev = rI;
}
}
}
}
const entries = [];
entityEntries.forEach((e, name) => {
if (e.length === 1) {
entries.push({ label: `${name}: ${e[0].label}`, loci: e[0].loci });
}
else if (e.length < 2000) {
entries.push(...e);
}
});
return entries;
}
export class StructureFocusControls extends PluginUIComponent {
constructor() {
super(...arguments);
this.state = { isBusy: false, showAction: false };
this.getSelectionItems = memoizeLatest((structures) => {
var _a;
const presetItems = [];
for (const s of structures) {
const d = (_a = s.cell.obj) === null || _a === void 0 ? void 0 : _a.data;
if (d) {
const entries = getFocusEntries(d);
if (entries.length > 0) {
presetItems.push([
ActionMenu.Header(d.label, { description: d.label }),
...ActionMenu.createItems(entries, {
label: f => f.label,
category: f => f.category,
description: f => f.label
})
]);
}
}
}
return presetItems;
});
this.selectAction = (item, e) => {
if (!item || !this.state.showAction) {
this.setState({ showAction: false });
return;
}
const f = item.value;
if (e === null || e === void 0 ? void 0 : e.shiftKey) {
this.plugin.managers.structure.focus.addFromLoci(f.loci);
}
else {
this.plugin.managers.structure.focus.set(f);
}
this.focusCamera();
};
this.toggleAction = () => this.setState({ showAction: !this.state.showAction });
this.focusCamera = () => {
const { current } = this.plugin.managers.structure.focus;
if (current)
this.plugin.managers.camera.focusLoci(current.loci);
};
this.clear = () => {
this.plugin.managers.structure.focus.clear();
this.plugin.managers.camera.reset();
};
this.highlightCurrent = () => {
const { current } = this.plugin.managers.structure.focus;
if (current)
this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci: current.loci }, false);
};
this.clearHighlights = () => {
this.plugin.managers.interactivity.lociHighlights.clearHighlights();
};
}
componentDidMount() {
this.subscribe(this.plugin.managers.structure.focus.behaviors.current, c => {
// clear the memo cache
this.getSelectionItems([]);
this.forceUpdate();
});
this.subscribe(this.plugin.managers.structure.focus.events.historyUpdated, c => {
this.forceUpdate();
});
this.subscribe(this.plugin.behaviors.state.isBusy, v => {
this.setState({ isBusy: v, showAction: false });
});
}
get isDisabled() {
return this.state.isBusy || this.plugin.managers.structure.hierarchy.selection.structures.length === 0;
}
get actionItems() {
const historyItems = [];
const { history } = this.plugin.managers.structure.focus;
if (history.length > 0) {
historyItems.push([
ActionMenu.Header('History', { description: 'Previously focused on items.' }),
...ActionMenu.createItems(history, {
label: f => f.label,
description: f => {
return f.category && f.label !== f.category
? `${f.category} | ${f.label}`
: f.label;
}
})
]);
}
const presetItems = this.getSelectionItems(this.plugin.managers.structure.hierarchy.selection.structures);
if (presetItems.length === 1) {
const item = presetItems[0];
const header = item[0];
header.initiallyExpanded = true;
}
const items = [];
if (presetItems.length > 0)
items.push(...presetItems);
if (historyItems.length > 0)
items.push(...historyItems);
return items;
}
getToggleBindingLabel() {
var _a;
const t = this.plugin.state.behaviors.transforms.get(FocusLoci.id);
if (!t)
return '';
const binding = (_a = t.params) === null || _a === void 0 ? void 0 : _a.bindings.clickFocus;
if (!binding || Binding.isEmpty(binding))
return '';
return Binding.formatTriggers(binding);
}
render() {
const { current } = this.plugin.managers.structure.focus;
const label = (current === null || current === void 0 ? void 0 : current.label) || 'Nothing Focused';
let title = 'Click to Center Camera';
if (!current) {
title = 'Select focus using the menu';
const binding = this.getToggleBindingLabel();
if (binding) {
title += `\nor use '${binding}' on element`;
}
}
return _jsxs(_Fragment, { children: [_jsxs("div", { className: 'msp-flex-row', children: [_jsx(Button, { noOverflow: true, onClick: this.focusCamera, title: title, onMouseEnter: this.highlightCurrent, onMouseLeave: this.clearHighlights, disabled: this.isDisabled || !current, style: { textAlignLast: current ? 'left' : void 0 }, children: label }), current && _jsx(IconButton, { svg: CancelOutlinedSvg, onClick: this.clear, title: 'Clear', className: 'msp-form-control', flex: true, disabled: this.isDisabled }), _jsx(ToggleButton, { icon: CenterFocusStrongSvg, title: 'Select a focus target to center on an show its surroundings. Hold shift to focus on multiple targets.', toggle: this.toggleAction, isSelected: this.state.showAction, disabled: this.isDisabled, style: { flex: '0 0 40px', padding: 0 } })] }), this.state.showAction && _jsx(ActionMenu, { items: this.actionItems, onSelect: this.selectAction })] });
}
}