interactivity.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * Copyright (c) 2019 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 { Loci as ModelLoci, EmptyLoci } from '../../mol-model/loci';
  8. import { ModifiersKeys, ButtonsType } from '../../mol-util/input/input-observer';
  9. import { Representation } from '../../mol-repr/representation';
  10. import { StructureElement, Link } from '../../mol-model/structure';
  11. import { MarkerAction } from '../../mol-util/marker-action';
  12. import { StructureElementSelectionManager } from './structure-element-selection';
  13. import { PluginContext } from '../context';
  14. import { StructureElement as SE, Structure } from '../../mol-model/structure';
  15. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  16. import { PluginCommands } from '../command';
  17. import { capitalize } from '../../mol-util/string';
  18. export { Interactivity }
  19. class Interactivity {
  20. readonly lociSelections: Interactivity.LociSelectionManager;
  21. readonly lociHighlights: Interactivity.LociHighlightManager;
  22. private _props = PD.getDefaultValues(Interactivity.Params)
  23. get props() { return { ...this._props } }
  24. setProps(props: Partial<Interactivity.Props>) {
  25. Object.assign(this._props, props)
  26. this.lociSelections.setProps(this._props)
  27. this.lociHighlights.setProps(this._props)
  28. }
  29. constructor(readonly ctx: PluginContext, props: Partial<Interactivity.Props> = {}) {
  30. Object.assign(this._props, props)
  31. this.lociSelections = new Interactivity.LociSelectionManager(ctx, this._props);
  32. this.lociHighlights = new Interactivity.LociHighlightManager(ctx, this._props);
  33. PluginCommands.Interactivity.SetProps.subscribe(ctx, e => this.setProps(e.props));
  34. }
  35. }
  36. namespace Interactivity {
  37. export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any }
  38. export namespace Loci {
  39. export function areEqual(a: Loci, b: Loci) {
  40. return a.repr === b.repr && ModelLoci.areEqual(a.loci, b.loci);
  41. }
  42. export const Empty: Loci = { loci: EmptyLoci };
  43. }
  44. const LociExpansion = {
  45. 'none': (loci: ModelLoci) => loci,
  46. 'residue': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeResidues(loci) : loci,
  47. 'chain': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeChains(loci) : loci,
  48. 'structure': (loci: ModelLoci) => SE.isLoci(loci) ? Structure.Loci(loci.structure) : loci
  49. }
  50. type LociExpansion = keyof typeof LociExpansion
  51. const LociExpansionOptions = Object.keys(LociExpansion).map(n => [n, capitalize(n)]) as [LociExpansion, string][]
  52. export const Params = {
  53. lociExpansion: PD.Select('residue', LociExpansionOptions),
  54. }
  55. export type Props = PD.Values<typeof Params>
  56. export interface HighlightEvent { current: Loci, modifiers?: ModifiersKeys }
  57. export interface ClickEvent { current: Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
  58. export type LociMarkProvider = (loci: Loci, action: MarkerAction) => void
  59. export abstract class LociMarkManager<MarkEvent extends any> {
  60. protected providers: LociMarkProvider[] = [];
  61. protected sel: StructureElementSelectionManager
  62. readonly props: Readonly<Props> = PD.getDefaultValues(Params)
  63. setProps(props: Partial<Props>) {
  64. Object.assign(this.props, props)
  65. }
  66. addProvider(provider: LociMarkProvider) {
  67. this.providers.push(provider);
  68. }
  69. removeProvider(provider: LociMarkProvider) {
  70. this.providers = this.providers.filter(p => p !== provider);
  71. // TODO clear, then re-apply remaining providers
  72. }
  73. normalizedLoci(interactivityLoci: Loci) {
  74. let { loci, repr } = interactivityLoci
  75. if (this.props.lociExpansion !== 'none' && Link.isLoci(loci)) {
  76. loci = Link.toStructureElementLoci(loci)
  77. }
  78. loci = LociExpansion[this.props.lociExpansion](loci)
  79. return { loci, repr }
  80. }
  81. protected mark(current: Loci<ModelLoci>, action: MarkerAction) {
  82. for (let p of this.providers) p(current, action);
  83. }
  84. abstract apply(e: MarkEvent): void
  85. constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) {
  86. this.sel = ctx.helpers.structureSelection
  87. this.setProps(props)
  88. }
  89. }
  90. export class LociHighlightManager extends LociMarkManager<HighlightEvent> {
  91. private prev: Loci = { loci: EmptyLoci, repr: void 0 };
  92. apply(e: HighlightEvent) {
  93. const { current, modifiers } = e
  94. const normalized: Loci<ModelLoci> = this.normalizedLoci(current)
  95. if (StructureElement.isLoci(normalized.loci)) {
  96. let loci: StructureElement.Loci = normalized.loci;
  97. if (modifiers && modifiers.shift) {
  98. loci = this.sel.tryGetRange(loci) || loci;
  99. }
  100. this.mark(this.prev, MarkerAction.RemoveHighlight);
  101. const toHighlight = { loci, repr: normalized.repr };
  102. this.mark(toHighlight, MarkerAction.Highlight);
  103. this.prev = toHighlight;
  104. } else {
  105. if (!Loci.areEqual(this.prev, normalized)) {
  106. this.mark(this.prev, MarkerAction.RemoveHighlight);
  107. this.mark(normalized, MarkerAction.Highlight);
  108. this.prev = normalized;
  109. }
  110. }
  111. }
  112. constructor(ctx: PluginContext, props: Partial<Props> = {}) {
  113. super(ctx, props)
  114. ctx.behaviors.interaction.highlight.subscribe(e => this.apply(e));
  115. }
  116. }
  117. export class LociSelectionManager extends LociMarkManager<ClickEvent> {
  118. toggleSel(current: Loci<ModelLoci>) {
  119. if (this.sel.has(current.loci)) {
  120. this.sel.remove(current.loci);
  121. this.mark(current, MarkerAction.Deselect);
  122. } else {
  123. this.sel.add(current.loci);
  124. this.mark(current, MarkerAction.Select);
  125. }
  126. }
  127. apply(e: ClickEvent) {
  128. const { current, buttons, modifiers } = e
  129. const normalized: Loci<ModelLoci> = this.normalizedLoci(current)
  130. if (normalized.loci.kind === 'empty-loci') {
  131. if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
  132. // clear the selection on Ctrl + Right-Click on empty
  133. const sels = this.sel.clear();
  134. for (const s of sels) this.mark({ loci: s }, MarkerAction.Deselect);
  135. }
  136. } else if (StructureElement.isLoci(normalized.loci)) {
  137. if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
  138. // select only the current element on Ctrl + Right-Click
  139. const old = this.sel.get(normalized.loci.structure);
  140. this.mark({ loci: old }, MarkerAction.Deselect);
  141. this.sel.set(normalized.loci);
  142. this.mark(normalized, MarkerAction.Select);
  143. } else if (modifiers.control && buttons === ButtonsType.Flag.Primary) {
  144. // toggle current element on Ctrl + Left-Click
  145. this.toggleSel(normalized as Representation.Loci<StructureElement.Loci>);
  146. } else if (modifiers.shift && buttons === ButtonsType.Flag.Primary) {
  147. // try to extend sequence on Shift + Left-Click
  148. let loci: StructureElement.Loci = normalized.loci;
  149. if (modifiers && modifiers.shift) {
  150. loci = this.sel.tryGetRange(loci) || loci;
  151. }
  152. this.toggleSel({ loci, repr: normalized.repr });
  153. }
  154. } else {
  155. if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return;
  156. for (let p of this.providers) p(normalized, MarkerAction.Toggle);
  157. }
  158. }
  159. constructor(ctx: PluginContext, props: Partial<Props> = {}) {
  160. super(ctx, props)
  161. ctx.behaviors.interaction.click.subscribe(e => this.apply(e));
  162. }
  163. }
  164. }