interactivity.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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 } 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 { Structure } from '../../mol-model/structure';
  15. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  16. import { PluginCommands } from '../command';
  17. export { Interactivity }
  18. class Interactivity {
  19. readonly lociSelects: Interactivity.LociSelectManager;
  20. readonly lociHighlights: Interactivity.LociHighlightManager;
  21. private _props = PD.getDefaultValues(Interactivity.Params)
  22. get props() { return { ...this._props } }
  23. setProps(props: Partial<Interactivity.Props>) {
  24. Object.assign(this._props, props)
  25. this.lociSelects.setProps(this._props)
  26. this.lociHighlights.setProps(this._props)
  27. }
  28. constructor(readonly ctx: PluginContext, props: Partial<Interactivity.Props> = {}) {
  29. Object.assign(this._props, props)
  30. this.lociSelects = new Interactivity.LociSelectManager(ctx, this._props);
  31. this.lociHighlights = new Interactivity.LociHighlightManager(ctx, this._props);
  32. PluginCommands.Interactivity.SetProps.subscribe(ctx, e => this.setProps(e.props));
  33. }
  34. }
  35. namespace Interactivity {
  36. export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any }
  37. export namespace Loci {
  38. export function areEqual(a: Loci, b: Loci) {
  39. return a.repr === b.repr && ModelLoci.areEqual(a.loci, b.loci);
  40. }
  41. export const Empty: Loci = { loci: EmptyLoci };
  42. }
  43. export const Params = {
  44. granularity: PD.Select('residue', ModelLoci.GranularityOptions, { description: 'Controls if selections are expanded to whole residues, chains, structures, or left as atoms and coarse elements' }),
  45. }
  46. export type Params = typeof Params
  47. export type Props = PD.Values<Params>
  48. export interface HoverEvent { current: Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  49. export interface ClickEvent { current: Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  50. export type LociMarkProvider = (loci: Loci, action: MarkerAction) => void
  51. export abstract class LociMarkManager {
  52. protected providers: LociMarkProvider[] = [];
  53. protected sel: StructureElementSelectionManager
  54. readonly props: Readonly<Props> = PD.getDefaultValues(Params)
  55. setProps(props: Partial<Props>) {
  56. Object.assign(this.props, props)
  57. }
  58. addProvider(provider: LociMarkProvider) {
  59. this.providers.push(provider);
  60. }
  61. removeProvider(provider: LociMarkProvider) {
  62. this.providers = this.providers.filter(p => p !== provider);
  63. // TODO clear, then re-apply remaining providers
  64. }
  65. protected normalizedLoci(interactivityLoci: Loci, applyGranularity = true) {
  66. const { loci, repr } = interactivityLoci
  67. const granularity = applyGranularity ? this.props.granularity : undefined
  68. return { loci: ModelLoci.normalize(loci, granularity), repr }
  69. }
  70. protected mark(current: Loci<ModelLoci>, action: MarkerAction) {
  71. for (let p of this.providers) p(current, action);
  72. }
  73. constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) {
  74. this.sel = ctx.helpers.structureSelectionManager
  75. this.setProps(props)
  76. }
  77. }
  78. //
  79. export class LociHighlightManager extends LociMarkManager {
  80. private prev: Loci[] = [];
  81. private isHighlighted(loci: Loci) {
  82. for (const p of this.prev) {
  83. if (Loci.areEqual(p, loci)) return true
  84. }
  85. return false
  86. }
  87. private addHighlight(loci: Loci) {
  88. this.mark(loci, MarkerAction.Highlight);
  89. this.prev.push(loci)
  90. }
  91. clearHighlights() {
  92. for (const p of this.prev) {
  93. this.mark(p, MarkerAction.RemoveHighlight);
  94. }
  95. this.prev.length = 0
  96. }
  97. highlight(current: Loci, applyGranularity = true) {
  98. const normalized = this.normalizedLoci(current, applyGranularity)
  99. if (!this.isHighlighted(normalized)) {
  100. this.addHighlight(normalized)
  101. }
  102. }
  103. highlightOnly(current: Loci, applyGranularity = true) {
  104. const normalized = this.normalizedLoci(current, applyGranularity)
  105. if (!this.isHighlighted(normalized)) {
  106. this.clearHighlights()
  107. this.addHighlight(normalized)
  108. }
  109. }
  110. highlightOnlyExtend(current: Loci, applyGranularity = true) {
  111. const normalized = this.normalizedLoci(current, applyGranularity)
  112. if (StructureElement.Loci.is(normalized.loci)) {
  113. const loci = {
  114. loci: this.sel.tryGetRange(normalized.loci) || normalized.loci,
  115. repr: normalized.repr
  116. }
  117. if (!this.isHighlighted(loci)) {
  118. this.clearHighlights()
  119. this.addHighlight(loci)
  120. }
  121. }
  122. }
  123. }
  124. //
  125. export class LociSelectManager extends LociMarkManager {
  126. toggle(current: Loci<ModelLoci>, applyGranularity = true) {
  127. if (ModelLoci.isEmpty(current.loci)) return;
  128. const normalized = this.normalizedLoci(current, applyGranularity)
  129. if (StructureElement.Loci.is(normalized.loci)) {
  130. this.toggleSel(normalized);
  131. } else {
  132. super.mark(normalized, MarkerAction.Toggle);
  133. }
  134. }
  135. toggleExtend(current: Loci<ModelLoci>, applyGranularity = true) {
  136. if (ModelLoci.isEmpty(current.loci)) return;
  137. const normalized = this.normalizedLoci(current, applyGranularity)
  138. if (StructureElement.Loci.is(normalized.loci)) {
  139. const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci;
  140. this.toggleSel({ loci, repr: normalized.repr });
  141. }
  142. }
  143. select(current: Loci<ModelLoci>, applyGranularity = true) {
  144. const normalized = this.normalizedLoci(current, applyGranularity)
  145. if (StructureElement.Loci.is(normalized.loci)) {
  146. this.sel.add(normalized.loci);
  147. }
  148. this.mark(normalized, MarkerAction.Select);
  149. }
  150. selectOnly(current: Loci<ModelLoci>, applyGranularity = true) {
  151. this.deselectAll()
  152. const normalized = this.normalizedLoci(current, applyGranularity)
  153. if (StructureElement.Loci.is(normalized.loci)) {
  154. this.sel.set(normalized.loci);
  155. }
  156. this.mark(normalized, MarkerAction.Select);
  157. }
  158. deselect(current: Loci<ModelLoci>, applyGranularity = true) {
  159. const normalized = this.normalizedLoci(current, applyGranularity)
  160. if (StructureElement.Loci.is(normalized.loci)) {
  161. this.sel.remove(normalized.loci);
  162. }
  163. this.mark(normalized, MarkerAction.Deselect);
  164. }
  165. deselectAll() {
  166. this.sel.clear();
  167. this.mark({ loci: EveryLoci }, MarkerAction.Deselect);
  168. }
  169. deselectAllOnEmpty(current: Loci<ModelLoci>) {
  170. if (isEmptyLoci(current.loci)) this.deselectAll()
  171. }
  172. protected mark(current: Loci<ModelLoci>, action: MarkerAction.Select | MarkerAction.Deselect) {
  173. const { loci } = current
  174. if (StructureElement.Loci.is(loci)) {
  175. // do a full deselect/select for the current structure so visuals
  176. // that are marked with granularity unequal to 'element' are handled properly
  177. super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect)
  178. super.mark({ loci: this.sel.get(loci.structure) }, MarkerAction.Select)
  179. } else {
  180. super.mark(current, action)
  181. }
  182. }
  183. private toggleSel(current: Loci<ModelLoci>) {
  184. if (this.sel.has(current.loci)) {
  185. this.sel.remove(current.loci);
  186. this.mark(current, MarkerAction.Deselect);
  187. } else {
  188. this.sel.add(current.loci);
  189. this.mark(current, MarkerAction.Select);
  190. }
  191. }
  192. }
  193. }