interactivity.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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, EveryLoci, isEmptyLoci } 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 lociSelects: Interactivity.LociSelectManager;
  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.lociSelects.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.lociSelects = new Interactivity.LociSelectManager(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 Granularity = {
  45. 'element': (loci: ModelLoci) => loci,
  46. 'residue': (loci: ModelLoci) => SE.Loci.is(loci) ? SE.Loci.extendToWholeResidues(loci) : loci,
  47. 'chain': (loci: ModelLoci) => SE.Loci.is(loci) ? SE.Loci.extendToWholeChains(loci) : loci,
  48. 'structure': (loci: ModelLoci) => SE.Loci.is(loci) ? Structure.Loci(loci.structure) : loci
  49. }
  50. type Granularity = keyof typeof Granularity
  51. const GranularityOptions = Object.keys(Granularity).map(n => [n, capitalize(n)]) as [Granularity, string][]
  52. export const Params = {
  53. granularity: PD.Select('residue', GranularityOptions, { description: 'Controls if selections are expanded to whole residues, chains, structures, 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: Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
  58. export interface ClickEvent { current: Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
  59. export type LociMarkProvider = (loci: Loci, action: MarkerAction) => void
  60. export abstract class LociMarkManager {
  61. protected providers: LociMarkProvider[] = [];
  62. protected sel: StructureElementSelectionManager
  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. normalizedLoci(interactivityLoci: Loci, applyGranularity = true) {
  75. let { loci, repr } = interactivityLoci
  76. if (this.props.granularity !== 'element' && Link.isLoci(loci)) {
  77. // convert Link.Loci to a StructureElement.Loci so granularity can be applied
  78. loci = Link.toStructureElementLoci(loci)
  79. }
  80. if (applyGranularity) {
  81. loci = Granularity[this.props.granularity](loci)
  82. }
  83. if (Structure.isLoci(loci)) {
  84. // convert to StructureElement.Loci
  85. loci = Structure.toStructureElementLoci(loci)
  86. }
  87. if (StructureElement.Loci.is(loci)) {
  88. // ensure the root structure is used
  89. loci = StructureElement.Loci.remap(loci, loci.structure.root)
  90. }
  91. return { loci, repr }
  92. }
  93. protected mark(current: Loci<ModelLoci>, action: MarkerAction) {
  94. for (let p of this.providers) p(current, action);
  95. }
  96. constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) {
  97. this.sel = ctx.helpers.structureSelectionManager
  98. this.setProps(props)
  99. }
  100. }
  101. //
  102. export class LociHighlightManager extends LociMarkManager {
  103. private prev: Loci = { loci: EmptyLoci, repr: void 0 };
  104. highlightOnly(current: Loci, applyGranularity = true) {
  105. const normalized = this.normalizedLoci(current, applyGranularity)
  106. if (StructureElement.Loci.is(normalized.loci)) {
  107. const loci = normalized.loci;
  108. this.mark(this.prev, MarkerAction.RemoveHighlight);
  109. const toHighlight = { loci, repr: normalized.repr };
  110. this.mark(toHighlight, MarkerAction.Highlight);
  111. this.prev = toHighlight;
  112. } else {
  113. if (!Loci.areEqual(this.prev, normalized)) {
  114. this.mark(this.prev, MarkerAction.RemoveHighlight);
  115. this.mark(normalized, MarkerAction.Highlight);
  116. this.prev = normalized;
  117. }
  118. }
  119. }
  120. highlightOnlyExtend(current: Loci, applyGranularity = true) {
  121. const normalized = this.normalizedLoci(current, applyGranularity)
  122. if (StructureElement.Loci.is(normalized.loci)) {
  123. const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci;
  124. this.mark(this.prev, MarkerAction.RemoveHighlight);
  125. const toHighlight = { loci, repr: normalized.repr };
  126. this.mark(toHighlight, MarkerAction.Highlight);
  127. this.prev = toHighlight;
  128. }
  129. }
  130. }
  131. //
  132. export class LociSelectManager extends LociMarkManager {
  133. selectToggle(current: Loci<ModelLoci>, applyGranularity = true) {
  134. const normalized = this.normalizedLoci(current, applyGranularity)
  135. if (StructureElement.Loci.is(normalized.loci)) {
  136. this.toggleSel(normalized);
  137. } else {
  138. this.mark(normalized, MarkerAction.Toggle);
  139. }
  140. }
  141. selectExtend(current: Loci<ModelLoci>, applyGranularity = true) {
  142. const normalized = this.normalizedLoci(current, applyGranularity)
  143. if (StructureElement.Loci.is(normalized.loci)) {
  144. const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci;
  145. this.toggleSel({ loci, repr: normalized.repr });
  146. }
  147. }
  148. select(current: Loci<ModelLoci>, applyGranularity = true) {
  149. const normalized = this.normalizedLoci(current, applyGranularity)
  150. if (StructureElement.Loci.is(normalized.loci)) {
  151. this.sel.add(normalized.loci);
  152. }
  153. this.mark(normalized, MarkerAction.Select);
  154. }
  155. selectOnly(current: Loci<ModelLoci>, applyGranularity = true) {
  156. this.deselectAll()
  157. const normalized = this.normalizedLoci(current, applyGranularity)
  158. if (StructureElement.Loci.is(normalized.loci)) {
  159. this.sel.set(normalized.loci);
  160. }
  161. this.mark(normalized, MarkerAction.Select);
  162. }
  163. deselect(current: Loci<ModelLoci>, applyGranularity = true) {
  164. const normalized = this.normalizedLoci(current, applyGranularity)
  165. if (StructureElement.Loci.is(normalized.loci)) {
  166. this.sel.remove(normalized.loci);
  167. }
  168. this.mark(normalized, MarkerAction.Deselect);
  169. }
  170. deselectAll() {
  171. this.sel.clear();
  172. this.mark({ loci: EveryLoci }, MarkerAction.Deselect);
  173. }
  174. deselectAllOnEmpty(current: Loci<ModelLoci>) {
  175. if (isEmptyLoci(current.loci)) this.deselectAll()
  176. }
  177. private toggleSel(current: Loci<ModelLoci>) {
  178. if (this.sel.has(current.loci)) {
  179. this.sel.remove(current.loci);
  180. this.mark(current, MarkerAction.Deselect);
  181. } else {
  182. this.sel.add(current.loci);
  183. this.mark(current, MarkerAction.Select);
  184. }
  185. }
  186. }
  187. }