focus.tsx 9.8 KB

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