interactivity.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /**
  2. * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. */
  7. import { EveryLoci, isEmptyLoci, Loci } from '../../mol-model/loci';
  8. import { Structure, StructureElement } from '../../mol-model/structure';
  9. import { PluginContext } from '../../mol-plugin/context';
  10. import { Representation } from '../../mol-repr/representation';
  11. import { ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
  12. import { MarkerAction } from '../../mol-util/marker-action';
  13. import { shallowEqual } from '../../mol-util/object';
  14. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  15. import { StatefulPluginComponent } from '../component';
  16. import { StructureSelectionManager } from './structure/selection';
  17. export { InteractivityManager };
  18. interface InteractivityManagerState {
  19. props: PD.ValuesFor<InteractivityManager.Params>
  20. }
  21. // TODO: make this customizable somewhere?
  22. const DefaultInteractivityFocusOptions = {
  23. minRadius: 6,
  24. extraRadius: 6,
  25. durationMs: 250,
  26. };
  27. export type InteractivityFocusLociOptions = typeof DefaultInteractivityFocusOptions
  28. class InteractivityManager extends StatefulPluginComponent<InteractivityManagerState> {
  29. readonly lociSelects: InteractivityManager.LociSelectManager;
  30. readonly lociHighlights: InteractivityManager.LociHighlightManager;
  31. private _props = PD.getDefaultValues(InteractivityManager.Params)
  32. readonly events = {
  33. propsUpdated: this.ev()
  34. };
  35. get props(): Readonly<InteractivityManagerState['props']> { return { ...this.state.props }; }
  36. setProps(props: Partial<InteractivityManager.Props>) {
  37. const old = this.props;
  38. const _new = { ...this.state.props, ...props };
  39. if (shallowEqual(old, _new)) return;
  40. this.updateState({ props: _new });
  41. this.lociSelects.setProps(_new);
  42. this.lociHighlights.setProps(_new);
  43. this.events.propsUpdated.next();
  44. }
  45. constructor(readonly plugin: PluginContext, props: Partial<InteractivityManager.Props> = {}) {
  46. super({ props: { ...PD.getDefaultValues(InteractivityManager.Params), ...props } });
  47. this.lociSelects = new InteractivityManager.LociSelectManager(plugin, this._props);
  48. this.lociHighlights = new InteractivityManager.LociHighlightManager(plugin, this._props);
  49. }
  50. }
  51. namespace InteractivityManager {
  52. export const Params = {
  53. granularity: PD.Select('residue', Loci.GranularityOptions, { label: 'Picking Level', description: 'Controls if selections are expanded upon picking to whole residues, chains, structures, instances, or left as atoms and coarse elements' }),
  54. };
  55. export type Params = typeof Params
  56. export type Props = PD.Values<Params>
  57. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  58. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  59. export type LociMarkProvider = (loci: Representation.Loci, action: MarkerAction) => void
  60. export abstract class LociMarkManager {
  61. protected providers: LociMarkProvider[] = [];
  62. protected sel: StructureSelectionManager
  63. readonly props: Readonly<Props> = PD.getDefaultValues(Params)
  64. setProps(props: Partial<Props>) {
  65. Object.assign(this.props, props);
  66. }
  67. addProvider(provider: LociMarkProvider) {
  68. this.providers.push(provider);
  69. }
  70. removeProvider(provider: LociMarkProvider) {
  71. this.providers = this.providers.filter(p => p !== provider);
  72. // TODO clear, then re-apply remaining providers
  73. }
  74. protected normalizedLoci(reprLoci: Representation.Loci, applyGranularity = true) {
  75. const { loci, repr } = reprLoci;
  76. const granularity = applyGranularity ? this.props.granularity : undefined;
  77. return { loci: Loci.normalize(loci, granularity), repr };
  78. }
  79. protected mark(current: Representation.Loci, action: MarkerAction) {
  80. for (let p of this.providers) p(current, action);
  81. }
  82. constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) {
  83. this.sel = ctx.managers.structure.selection;
  84. this.setProps(props);
  85. }
  86. }
  87. //
  88. export class LociHighlightManager extends LociMarkManager {
  89. private prev: Representation.Loci[] = [];
  90. private isHighlighted(loci: Representation.Loci) {
  91. for (const p of this.prev) {
  92. if (Representation.Loci.areEqual(p, loci)) return true;
  93. }
  94. return false;
  95. }
  96. private addHighlight(loci: Representation.Loci) {
  97. this.mark(loci, MarkerAction.Highlight);
  98. this.prev.push(loci);
  99. }
  100. clearHighlights = () => {
  101. for (const p of this.prev) {
  102. this.mark(p, MarkerAction.RemoveHighlight);
  103. }
  104. this.prev.length = 0;
  105. }
  106. highlight(current: Representation.Loci, applyGranularity = true) {
  107. const normalized = this.normalizedLoci(current, applyGranularity);
  108. if (!this.isHighlighted(normalized)) {
  109. this.addHighlight(normalized);
  110. }
  111. }
  112. highlightOnly(current: Representation.Loci, applyGranularity = true) {
  113. const normalized = this.normalizedLoci(current, applyGranularity);
  114. if (!this.isHighlighted(normalized)) {
  115. this.clearHighlights();
  116. this.addHighlight(normalized);
  117. }
  118. }
  119. highlightOnlyExtend(current: Representation.Loci, applyGranularity = true) {
  120. const normalized = this.normalizedLoci(current, applyGranularity);
  121. if (StructureElement.Loci.is(normalized.loci)) {
  122. const loci = {
  123. loci: this.sel.tryGetRange(normalized.loci) || normalized.loci,
  124. repr: normalized.repr
  125. };
  126. if (!this.isHighlighted(loci)) {
  127. this.clearHighlights();
  128. this.addHighlight(loci);
  129. }
  130. }
  131. }
  132. }
  133. //
  134. export class LociSelectManager extends LociMarkManager {
  135. toggle(current: Representation.Loci, applyGranularity = true) {
  136. if (Loci.isEmpty(current.loci)) return;
  137. const normalized = this.normalizedLoci(current, applyGranularity);
  138. if (StructureElement.Loci.is(normalized.loci)) {
  139. this.toggleSel(normalized);
  140. } else {
  141. super.mark(normalized, MarkerAction.Toggle);
  142. }
  143. }
  144. toggleExtend(current: Representation.Loci, applyGranularity = true) {
  145. if (Loci.isEmpty(current.loci)) return;
  146. const normalized = this.normalizedLoci(current, applyGranularity);
  147. if (StructureElement.Loci.is(normalized.loci)) {
  148. const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci;
  149. this.toggleSel({ loci, repr: normalized.repr });
  150. }
  151. }
  152. select(current: Representation.Loci, applyGranularity = true) {
  153. const normalized = this.normalizedLoci(current, applyGranularity);
  154. if (StructureElement.Loci.is(normalized.loci)) {
  155. this.sel.modify('add', normalized.loci);
  156. }
  157. this.mark(normalized, MarkerAction.Select);
  158. }
  159. selectJoin(current: Representation.Loci, applyGranularity = true) {
  160. const normalized = this.normalizedLoci(current, applyGranularity);
  161. if (StructureElement.Loci.is(normalized.loci)) {
  162. this.sel.modify('intersect', normalized.loci);
  163. }
  164. this.mark(normalized, MarkerAction.Select);
  165. }
  166. selectOnly(current: Representation.Loci, applyGranularity = true) {
  167. const normalized = this.normalizedLoci(current, applyGranularity);
  168. if (StructureElement.Loci.is(normalized.loci)) {
  169. // only deselect for the structure of the given loci
  170. this.deselect({ loci: Structure.toStructureElementLoci(normalized.loci.structure), repr: normalized.repr }, false);
  171. this.sel.modify('set', normalized.loci);
  172. }
  173. this.mark(normalized, MarkerAction.Select);
  174. }
  175. deselect(current: Representation.Loci, applyGranularity = true) {
  176. const normalized = this.normalizedLoci(current, applyGranularity);
  177. if (StructureElement.Loci.is(normalized.loci)) {
  178. this.sel.modify('remove', normalized.loci);
  179. }
  180. this.mark(normalized, MarkerAction.Deselect);
  181. }
  182. deselectAll() {
  183. this.sel.clear();
  184. this.mark({ loci: EveryLoci }, MarkerAction.Deselect);
  185. }
  186. deselectAllOnEmpty(current: Representation.Loci) {
  187. if (isEmptyLoci(current.loci)) this.deselectAll();
  188. }
  189. protected mark(current: Representation.Loci, action: MarkerAction.Select | MarkerAction.Deselect) {
  190. const { loci } = current;
  191. if (StructureElement.Loci.is(loci)) {
  192. // do a full deselect/select for the current structure so visuals that are
  193. // marked with granularity unequal to 'element' and join/intersect operations
  194. // are handled properly
  195. super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect);
  196. super.mark({ loci: this.sel.getLoci(loci.structure) }, MarkerAction.Select);
  197. } else {
  198. super.mark(current, action);
  199. }
  200. }
  201. private toggleSel(current: Representation.Loci) {
  202. if (this.sel.has(current.loci)) {
  203. this.sel.modify('remove', current.loci);
  204. this.mark(current, MarkerAction.Deselect);
  205. } else {
  206. this.sel.modify('add', current.loci);
  207. this.mark(current, MarkerAction.Select);
  208. }
  209. }
  210. }
  211. }