interactivity.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. /**
  2. * Copyright (c) 2019-2021 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. import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
  18. export { InteractivityManager };
  19. interface InteractivityManagerState {
  20. props: PD.ValuesFor<InteractivityManager.Params>
  21. }
  22. // TODO: make this customizable somewhere?
  23. const DefaultInteractivityFocusOptions = {
  24. minRadius: 6,
  25. extraRadius: 6,
  26. durationMs: 250,
  27. };
  28. export type InteractivityFocusLociOptions = typeof DefaultInteractivityFocusOptions
  29. class InteractivityManager extends StatefulPluginComponent<InteractivityManagerState> {
  30. readonly lociSelects: InteractivityManager.LociSelectManager;
  31. readonly lociHighlights: InteractivityManager.LociHighlightManager;
  32. private _props = PD.getDefaultValues(InteractivityManager.Params)
  33. readonly events = {
  34. propsUpdated: this.ev()
  35. };
  36. get props(): Readonly<InteractivityManagerState['props']> { return { ...this.state.props }; }
  37. setProps(props: Partial<InteractivityManager.Props>) {
  38. const old = this.props;
  39. const _new = { ...this.state.props, ...props };
  40. if (shallowEqual(old, _new)) return;
  41. this.updateState({ props: _new });
  42. this.lociSelects.setProps(_new);
  43. this.lociHighlights.setProps(_new);
  44. this.events.propsUpdated.next(void 0);
  45. }
  46. constructor(readonly plugin: PluginContext, props: Partial<InteractivityManager.Props> = {}) {
  47. super({ props: { ...PD.getDefaultValues(InteractivityManager.Params), ...props } });
  48. this.lociSelects = new InteractivityManager.LociSelectManager(plugin, this._props);
  49. this.lociHighlights = new InteractivityManager.LociHighlightManager(plugin, this._props);
  50. }
  51. }
  52. namespace InteractivityManager {
  53. export const Params = {
  54. 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' }),
  55. };
  56. export type Params = typeof Params
  57. export type Props = PD.Values<Params>
  58. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  59. export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
  60. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  61. /**
  62. * The `noRender` argument indicates that the action should only update the internal
  63. * data structure but not render anything user visible. For example 1) no drawing of
  64. * the canvas3d scene or 2) no ui update of loci labels. This is useful because some
  65. * actions require clearing any markings before they can be applied.
  66. */
  67. export type LociMarkProvider = (loci: Representation.Loci, action: MarkerAction, /* test */ noRender?: boolean) => void
  68. export abstract class LociMarkManager {
  69. protected providers: LociMarkProvider[] = [];
  70. protected sel: StructureSelectionManager
  71. readonly props: Readonly<Props> = PD.getDefaultValues(Params)
  72. setProps(props: Partial<Props>) {
  73. Object.assign(this.props, props);
  74. }
  75. addProvider(provider: LociMarkProvider) {
  76. this.providers.push(provider);
  77. }
  78. removeProvider(provider: LociMarkProvider) {
  79. this.providers = this.providers.filter(p => p !== provider);
  80. // TODO clear, then re-apply remaining providers
  81. }
  82. protected normalizedLoci(reprLoci: Representation.Loci, applyGranularity = true) {
  83. const { loci, repr } = reprLoci;
  84. const granularity = applyGranularity ? this.props.granularity : undefined;
  85. return { loci: Loci.normalize(loci, granularity), repr };
  86. }
  87. protected mark(current: Representation.Loci, action: MarkerAction, noRender = false) {
  88. if (!Loci.isEmpty(current.loci)) {
  89. for (const p of this.providers) p(current, action, noRender);
  90. }
  91. }
  92. constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) {
  93. this.sel = ctx.managers.structure.selection;
  94. this.setProps(props);
  95. }
  96. }
  97. //
  98. export class LociHighlightManager extends LociMarkManager {
  99. private prev: Representation.Loci[] = [];
  100. private isHighlighted(loci: Representation.Loci) {
  101. for (const p of this.prev) {
  102. if (Representation.Loci.areEqual(p, loci)) return true;
  103. }
  104. return false;
  105. }
  106. private addHighlight(loci: Representation.Loci) {
  107. this.mark(loci, MarkerAction.Highlight);
  108. this.prev.push(loci);
  109. }
  110. clearHighlights = (noRender = false) => {
  111. for (const p of this.prev) {
  112. this.mark(p, MarkerAction.RemoveHighlight, noRender);
  113. }
  114. this.prev.length = 0;
  115. }
  116. highlight(current: Representation.Loci, applyGranularity = true) {
  117. const normalized = this.normalizedLoci(current, applyGranularity);
  118. if (!this.isHighlighted(normalized)) {
  119. this.addHighlight(normalized);
  120. }
  121. }
  122. highlightOnly(current: Representation.Loci, applyGranularity = true) {
  123. const normalized = this.normalizedLoci(current, applyGranularity);
  124. if (!this.isHighlighted(normalized)) {
  125. if (Loci.isEmpty(normalized.loci)) {
  126. this.clearHighlights();
  127. } else {
  128. this.clearHighlights(true);
  129. this.addHighlight(normalized);
  130. }
  131. }
  132. }
  133. highlightOnlyExtend(current: Representation.Loci, applyGranularity = true) {
  134. const normalized = this.normalizedLoci(current, applyGranularity);
  135. if (StructureElement.Loci.is(normalized.loci)) {
  136. const extended = {
  137. loci: this.sel.tryGetRange(normalized.loci) || normalized.loci,
  138. repr: normalized.repr
  139. };
  140. if (!this.isHighlighted(extended)) {
  141. if (Loci.isEmpty(extended.loci)) {
  142. this.clearHighlights();
  143. } else {
  144. this.clearHighlights(true);
  145. this.addHighlight(extended);
  146. }
  147. }
  148. }
  149. }
  150. }
  151. //
  152. export class LociSelectManager extends LociMarkManager {
  153. toggle(current: Representation.Loci, applyGranularity = true) {
  154. if (Loci.isEmpty(current.loci)) return;
  155. const normalized = this.normalizedLoci(current, applyGranularity);
  156. if (StructureElement.Loci.is(normalized.loci)) {
  157. this.toggleSel(normalized);
  158. } else {
  159. super.mark(normalized, MarkerAction.Toggle);
  160. }
  161. }
  162. toggleExtend(current: Representation.Loci, applyGranularity = true) {
  163. if (Loci.isEmpty(current.loci)) return;
  164. const normalized = this.normalizedLoci(current, applyGranularity);
  165. if (StructureElement.Loci.is(normalized.loci)) {
  166. const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci;
  167. this.toggleSel({ loci, repr: normalized.repr });
  168. }
  169. }
  170. select(current: Representation.Loci, applyGranularity = true) {
  171. const normalized = this.normalizedLoci(current, applyGranularity);
  172. if (StructureElement.Loci.is(normalized.loci)) {
  173. this.sel.modify('add', normalized.loci);
  174. }
  175. this.mark(normalized, MarkerAction.Select);
  176. }
  177. selectJoin(current: Representation.Loci, applyGranularity = true) {
  178. const normalized = this.normalizedLoci(current, applyGranularity);
  179. if (StructureElement.Loci.is(normalized.loci)) {
  180. this.sel.modify('intersect', normalized.loci);
  181. }
  182. this.mark(normalized, MarkerAction.Select);
  183. }
  184. selectOnly(current: Representation.Loci, applyGranularity = true) {
  185. const normalized = this.normalizedLoci(current, applyGranularity);
  186. if (StructureElement.Loci.is(normalized.loci)) {
  187. // only deselect for the structure of the given loci
  188. this.deselect({ loci: Structure.toStructureElementLoci(normalized.loci.structure), repr: normalized.repr }, false);
  189. this.sel.modify('set', normalized.loci);
  190. }
  191. this.mark(normalized, MarkerAction.Select);
  192. }
  193. deselect(current: Representation.Loci, applyGranularity = true) {
  194. const normalized = this.normalizedLoci(current, applyGranularity);
  195. if (StructureElement.Loci.is(normalized.loci)) {
  196. this.sel.modify('remove', normalized.loci);
  197. }
  198. this.mark(normalized, MarkerAction.Deselect);
  199. }
  200. deselectAll() {
  201. this.sel.clear();
  202. this.mark({ loci: EveryLoci }, MarkerAction.Deselect);
  203. }
  204. deselectAllOnEmpty(current: Representation.Loci) {
  205. if (isEmptyLoci(current.loci)) this.deselectAll();
  206. }
  207. protected mark(current: Representation.Loci, action: MarkerAction.Select | MarkerAction.Deselect) {
  208. const { loci } = current;
  209. if (!Loci.isEmpty(loci)) {
  210. if (StructureElement.Loci.is(loci)) {
  211. // do a full deselect/select for the current structure so visuals that are
  212. // marked with granularity unequal to 'element' and join/intersect operations
  213. // are handled properly
  214. super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect, true);
  215. super.mark({ loci: this.sel.getLoci(loci.structure) }, MarkerAction.Select);
  216. } else {
  217. super.mark(current, action);
  218. }
  219. }
  220. }
  221. private toggleSel(current: Representation.Loci) {
  222. if (this.sel.has(current.loci)) {
  223. this.sel.modify('remove', current.loci);
  224. this.mark(current, MarkerAction.Deselect);
  225. } else {
  226. this.sel.modify('add', current.loci);
  227. this.mark(current, MarkerAction.Select);
  228. }
  229. }
  230. }
  231. }