focus.tsx 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import CenterFocusStrong from '@material-ui/icons/CenterFocusStrong';
  7. import CancelOutlined from '@material-ui/icons/CancelOutlined';
  8. import * as React from 'react';
  9. import { OrderedSet, SortedArray } from '../../mol-data/int';
  10. import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
  11. import { Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
  12. import { UnitIndex } from '../../mol-model/structure/structure/element/element';
  13. import { FocusEntry } from '../../mol-plugin-state/manager/structure/focus';
  14. import { StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
  15. import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
  16. import { StateTransform } from '../../mol-state';
  17. import { lociLabel } from '../../mol-theme/label';
  18. import { Binding } from '../../mol-util/binding';
  19. import { memoizeLatest } from '../../mol-util/memoize';
  20. import { PluginUIComponent } from '../base';
  21. import { ActionMenu } from '../controls/action-menu';
  22. import { Button, IconButton, ToggleButton } from '../controls/common';
  23. interface StructureFocusControlsState {
  24. isBusy: boolean
  25. showAction: boolean
  26. }
  27. function addSymmetryGroupEntries(entries: Map<string, FocusEntry[]>, location: StructureElement.Location, unitSymmetryGroup: Unit.SymmetryGroup, granularity: 'residue' | 'chain') {
  28. const idx = SortedArray.indexOf(location.unit.elements, location.element) as UnitIndex;
  29. const base = StructureElement.Loci(location.structure, [
  30. { unit: location.unit, indices: OrderedSet.ofSingleton(idx) }
  31. ]);
  32. const extended = granularity === 'residue'
  33. ? StructureElement.Loci.extendToWholeResidues(base)
  34. : StructureElement.Loci.extendToWholeChains(base);
  35. const name = StructureProperties.entity.pdbx_description(location).join(', ');
  36. for (const u of unitSymmetryGroup.units) {
  37. const loci = StructureElement.Loci(extended.structure, [
  38. { unit: u, indices: extended.elements[0].indices }
  39. ]);
  40. let label = lociLabel(loci, { reverse: true, hidePrefix: true, htmlStyling: false, granularity });
  41. if (!label) label = lociLabel(loci, { hidePrefix: false, htmlStyling: false });
  42. if (unitSymmetryGroup.units.length > 1) {
  43. label += ` | ${loci.elements[0].unit.conformation.operator.name}`;
  44. }
  45. const item: FocusEntry = { label, category: name, loci };
  46. if (entries.has(name)) entries.get(name)!.push(item);
  47. else entries.set(name, [item]);
  48. }
  49. }
  50. function getFocusEntries(structure: Structure) {
  51. const entityEntries = new Map<string, FocusEntry[]>();
  52. const l = StructureElement.Location.create(structure);
  53. for (const ug of structure.unitSymmetryGroups) {
  54. if (!Unit.isAtomic(ug.units[0])) continue;
  55. l.unit = ug.units[0];
  56. l.element = ug.elements[0];
  57. const isMultiChain = Unit.Traits.is(l.unit.traits, Unit.Trait.MultiChain);
  58. const entityType = StructureProperties.entity.type(l);
  59. const isPolymer = entityType === 'non-polymer';
  60. const isBranched = entityType === 'branched';
  61. const asymId = StructureProperties.chain.label_asym_id(l);
  62. const isBirdMolecule = MmcifFormat.isBirdMolecule(l.unit.model, asymId);
  63. if (isBirdMolecule) {
  64. addSymmetryGroupEntries(entityEntries, l, ug, 'chain');
  65. } else if (isPolymer && !isMultiChain) {
  66. addSymmetryGroupEntries(entityEntries, l, ug, 'residue');
  67. } else if (isBranched || (isPolymer && isMultiChain)) {
  68. const u = l.unit;
  69. const { index: residueIndex } = u.model.atomicHierarchy.residueAtomSegments;
  70. let prev = -1;
  71. for (let i = 0, il = u.elements.length; i < il; ++i) {
  72. const eI = u.elements[i];
  73. const rI = residueIndex[eI];
  74. if(rI !== prev) {
  75. l.element = eI;
  76. addSymmetryGroupEntries(entityEntries, l, ug, 'residue');
  77. prev = rI;
  78. }
  79. }
  80. }
  81. }
  82. const entries: FocusEntry[] = [];
  83. entityEntries.forEach((e, name) => {
  84. if (e.length === 1) {
  85. entries.push({ label: `${name}: ${e[0].label}`, loci: e[0].loci });
  86. } else {
  87. entries.push(...e);
  88. }
  89. });
  90. return entries;
  91. }
  92. export class StructureFocusControls extends PluginUIComponent<{}, StructureFocusControlsState> {
  93. state = { isBusy: false, showAction: false }
  94. componentDidMount() {
  95. this.subscribe(this.plugin.managers.structure.focus.behaviors.current, c => {
  96. // clear the memo cache
  97. this.getSelectionItems([]);
  98. this.forceUpdate();
  99. });
  100. this.subscribe(this.plugin.managers.structure.focus.events.historyUpdated, c => {
  101. this.forceUpdate();
  102. });
  103. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  104. this.setState({ isBusy: v, showAction: false });
  105. });
  106. }
  107. get isDisabled() {
  108. return this.state.isBusy || this.actionItems.length === 0;
  109. }
  110. getSelectionItems = memoizeLatest((structures: ReadonlyArray<StructureRef>) => {
  111. const presetItems: ActionMenu.Items[] = [];
  112. for (const s of structures) {
  113. const d = s.cell.obj?.data;
  114. if (d) {
  115. const entries = getFocusEntries(d);
  116. if (entries.length > 0) {
  117. presetItems.push([
  118. ActionMenu.Header(d.label, { description: d.label }),
  119. ...ActionMenu.createItems(entries, {
  120. label: f => f.label,
  121. category: f => f.category,
  122. description: f => f.label
  123. })
  124. ]);
  125. }
  126. }
  127. }
  128. return presetItems;
  129. });
  130. get actionItems() {
  131. const historyItems: ActionMenu.Items[] = [];
  132. const { history } = this.plugin.managers.structure.focus;
  133. if (history.length > 0) {
  134. historyItems.push([
  135. ActionMenu.Header('History', { description: 'Previously focused on items.' }),
  136. ...ActionMenu.createItems(history, {
  137. label: f => f.label,
  138. description: f => {
  139. return f.category && f.label !== f.category
  140. ? `${f.category} | ${f.label}`
  141. : f.label;
  142. }
  143. })
  144. ]);
  145. }
  146. const presetItems: ActionMenu.Items[] = this.getSelectionItems(this.plugin.managers.structure.hierarchy.selection.structures);
  147. if (presetItems.length === 1) {
  148. const item = presetItems[0] as ActionMenu.Items[];
  149. const header = item[0] as ActionMenu.Header;
  150. header.initiallyExpanded = true;
  151. }
  152. const items: ActionMenu.Items[] = [];
  153. if (presetItems.length > 0) items.push(...presetItems);
  154. if (historyItems.length > 0) items.push(...historyItems);
  155. return items;
  156. }
  157. selectAction: ActionMenu.OnSelect = (item, e) => {
  158. if (!item || !this.state.showAction) {
  159. this.setState({ showAction: false });
  160. return;
  161. }
  162. const f = item.value as FocusEntry;
  163. if (e?.shiftKey) {
  164. this.plugin.managers.structure.focus.addFromLoci(f.loci);
  165. } else {
  166. this.plugin.managers.structure.focus.set(f);
  167. }
  168. }
  169. toggleAction = () => this.setState({ showAction: !this.state.showAction })
  170. focus = () => {
  171. const { current } = this.plugin.managers.structure.focus;
  172. if (current) this.plugin.managers.camera.focusLoci(current.loci);
  173. }
  174. clear = () => {
  175. this.plugin.managers.structure.focus.clear();
  176. this.plugin.managers.camera.reset();
  177. }
  178. highlightCurrent = () => {
  179. const { current } = this.plugin.managers.structure.focus;
  180. if (current) this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci: current.loci }, false);
  181. }
  182. clearHighlights = () => {
  183. this.plugin.managers.interactivity.lociHighlights.clearHighlights();
  184. }
  185. getToggleBindingLabel() {
  186. const t = this.plugin.state.behaviors.transforms.get(FocusLoci.id) as StateTransform<typeof FocusLoci>;
  187. if (!t) return '';
  188. const binding = t.params?.bindings.clickFocus;
  189. if (!binding || Binding.isEmpty(binding)) return '';
  190. return Binding.formatTriggers(binding);
  191. }
  192. render() {
  193. const { current } = this.plugin.managers.structure.focus;
  194. const label = current?.label || 'Nothing Focused';
  195. let title = 'Click to Center Camera';
  196. if (!current) {
  197. title = 'Select focus using the menu';
  198. const binding = this.getToggleBindingLabel();
  199. if (binding) {
  200. title += `\nor use '${binding}' on element`;
  201. }
  202. }
  203. return <>
  204. <div className='msp-flex-row'>
  205. <Button noOverflow onClick={this.focus} title={title} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}
  206. style={{ textAlignLast: current ? 'left' : void 0 }}>
  207. {label}
  208. </Button>
  209. {current && <IconButton svg={CancelOutlined} onClick={this.clear} title='Clear' className='msp-form-control' flex disabled={this.isDisabled} />}
  210. <ToggleButton icon={CenterFocusStrong} 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 }} />
  211. </div>
  212. {this.state.showAction && <ActionMenu items={this.actionItems} onSelect={this.selectAction} />}
  213. </>;
  214. }
  215. }