representation.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /**
  2. * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { MarkerAction } from '../../../mol-util/marker-action';
  8. import { PluginContext } from '../../../mol-plugin/context';
  9. import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
  10. import { lociLabel } from '../../../mol-theme/label';
  11. import { PluginBehavior } from '../behavior';
  12. import { StateTreeSpine } from '../../../mol-state/tree/spine';
  13. import { StateSelection } from '../../../mol-state';
  14. import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
  15. import { Binding } from '../../../mol-util/binding';
  16. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  17. import { EmptyLoci, Loci } from '../../../mol-model/loci';
  18. import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
  19. import { arrayMax } from '../../../mol-util/array';
  20. import { Representation } from '../../../mol-repr/representation';
  21. import { LociLabel } from '../../../mol-plugin-state/manager/loci-label';
  22. const B = ButtonsType;
  23. const M = ModifiersKeys;
  24. const Trigger = Binding.Trigger;
  25. //
  26. const DefaultHighlightLociBindings = {
  27. hoverHighlightOnly: Binding([Trigger(B.Flag.None)], 'Highlight', 'Hover element using ${triggers}'),
  28. hoverHighlightOnlyExtend: Binding([Trigger(B.Flag.None, M.create({ shift: true }))], 'Extend highlight', 'From selected to hovered element along polymer using ${triggers}'),
  29. };
  30. const HighlightLociParams = {
  31. bindings: PD.Value(DefaultHighlightLociBindings, { isHidden: true }),
  32. };
  33. type HighlightLociProps = PD.Values<typeof HighlightLociParams>
  34. export const HighlightLoci = PluginBehavior.create({
  35. name: 'representation-highlight-loci',
  36. category: 'interaction',
  37. ctor: class extends PluginBehavior.Handler<HighlightLociProps> {
  38. private lociMarkProvider = (interactionLoci: Representation.Loci, action: MarkerAction) => {
  39. if (!this.ctx.canvas3d) return;
  40. this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action);
  41. }
  42. register() {
  43. this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, buttons, modifiers }) => {
  44. if (!this.ctx.canvas3d || this.ctx.isBusy) return;
  45. let matched = false;
  46. if (Binding.match(this.params.bindings.hoverHighlightOnly, buttons, modifiers)) {
  47. this.ctx.managers.interactivity.lociHighlights.highlightOnly(current);
  48. matched = true;
  49. }
  50. if (Binding.match(this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) {
  51. this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend(current);
  52. matched = true;
  53. }
  54. if (!matched) {
  55. this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci });
  56. }
  57. });
  58. this.ctx.managers.interactivity.lociHighlights.addProvider(this.lociMarkProvider);
  59. }
  60. unregister() {
  61. this.ctx.managers.interactivity.lociHighlights.removeProvider(this.lociMarkProvider);
  62. }
  63. },
  64. params: () => HighlightLociParams,
  65. display: { name: 'Highlight Loci on Canvas' }
  66. });
  67. //
  68. const DefaultSelectLociBindings = {
  69. clickSelect: Binding.Empty,
  70. clickToggleExtend: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Toggle extended selection', '${triggers} to extend selection along polymer'),
  71. clickSelectOnly: Binding.Empty,
  72. clickToggle: Binding([Trigger(B.Flag.Primary, M.create())], 'Toggle selection', '${triggers} on element'),
  73. clickDeselect: Binding.Empty,
  74. clickDeselectAllOnEmpty: Binding([Trigger(B.Flag.Primary, M.create())], 'Deselect all', 'Click on nothing using ${triggers}'),
  75. };
  76. const SelectLociParams = {
  77. bindings: PD.Value(DefaultSelectLociBindings, { isHidden: true }),
  78. };
  79. type SelectLociProps = PD.Values<typeof SelectLociParams>
  80. export const SelectLoci = PluginBehavior.create({
  81. name: 'representation-select-loci',
  82. category: 'interaction',
  83. ctor: class extends PluginBehavior.Handler<SelectLociProps> {
  84. private spine: StateTreeSpine.Impl
  85. private lociMarkProvider = (reprLoci: Representation.Loci, action: MarkerAction) => {
  86. if (!this.ctx.canvas3d) return;
  87. this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action);
  88. }
  89. private applySelectMark(ref: string, clear?: boolean) {
  90. const cell = this.ctx.state.data.cells.get(ref);
  91. if (cell && SO.isRepresentation3D(cell.obj)) {
  92. this.spine.current = cell;
  93. const so = this.spine.getRootOfType(SO.Molecule.Structure);
  94. if (so) {
  95. if (clear) {
  96. this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect);
  97. }
  98. const loci = this.ctx.managers.structure.selection.getLoci(so.data);
  99. this.lociMarkProvider({ loci }, MarkerAction.Select);
  100. }
  101. }
  102. }
  103. register() {
  104. const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci);
  105. const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci);
  106. const actions: [keyof typeof DefaultSelectLociBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [
  107. ['clickSelect', current => this.ctx.managers.interactivity.lociSelects.select(current), lociIsNotEmpty],
  108. ['clickToggle', current => this.ctx.managers.interactivity.lociSelects.toggle(current), lociIsNotEmpty],
  109. ['clickToggleExtend', current => this.ctx.managers.interactivity.lociSelects.toggleExtend(current), lociIsNotEmpty],
  110. ['clickSelectOnly', current => this.ctx.managers.interactivity.lociSelects.selectOnly(current), lociIsNotEmpty],
  111. ['clickDeselect', current => this.ctx.managers.interactivity.lociSelects.deselect(current), lociIsNotEmpty],
  112. ['clickDeselectAllOnEmpty', () => this.ctx.managers.interactivity.lociSelects.deselectAll(), lociIsEmpty],
  113. ];
  114. // sort the action so that the ones with more modifiers trigger sooner.
  115. actions.sort((a, b) => {
  116. const x = this.params.bindings[a[0]], y = this.params.bindings[b[0]];
  117. const k = x.triggers.length === 0 ? 0 : arrayMax(x.triggers.map(t => M.size(t.modifiers)));
  118. const l = y.triggers.length === 0 ? 0 : arrayMax(y.triggers.map(t => M.size(t.modifiers)));
  119. return l - k;
  120. });
  121. this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
  122. if (!this.ctx.canvas3d || this.ctx.isBusy || !this.ctx.selectionMode) return;
  123. // only trigger the 1st action that matches
  124. for (const [binding, action, condition] of actions) {
  125. if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) {
  126. action(current);
  127. break;
  128. }
  129. }
  130. });
  131. this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
  132. this.subscribeObservable(this.ctx.state.events.object.created, ({ ref }) => this.applySelectMark(ref));
  133. // re-apply select-mark to all representation of an updated structure
  134. this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref }) => {
  135. const cell = this.ctx.state.data.cells.get(ref);
  136. if (cell && SO.Molecule.Structure.is(cell.obj)) {
  137. const reprs = this.ctx.state.data.select(StateSelection.Generators.ofType(SO.Molecule.Structure.Representation3D, ref));
  138. for (const repr of reprs) this.applySelectMark(repr.transform.ref, true);
  139. }
  140. });
  141. }
  142. unregister() {
  143. this.ctx.managers.interactivity.lociSelects.removeProvider(this.lociMarkProvider);
  144. }
  145. constructor(ctx: PluginContext, params: SelectLociProps) {
  146. super(ctx, params);
  147. this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
  148. }
  149. },
  150. params: () => SelectLociParams,
  151. display: { name: 'Select Loci on Canvas' }
  152. });
  153. //
  154. export const DefaultLociLabelProvider = PluginBehavior.create({
  155. name: 'default-loci-label-provider',
  156. category: 'interaction',
  157. ctor: class implements PluginBehavior<undefined> {
  158. private f = {
  159. label: (loci: Loci) => {
  160. const label: string[] = [];
  161. if (StructureElement.Loci.is(loci) && loci.elements.length === 1) {
  162. const { unit: u } = loci.elements[0];
  163. const l = StructureElement.Location.create(loci.structure, u, u.elements[0]);
  164. const name = StructureProperties.entity.pdbx_description(l).join(', ');
  165. label.push(name);
  166. }
  167. label.push(lociLabel(loci));
  168. return label.filter(l => !!l).join('</br>');
  169. },
  170. group: (label: LociLabel) => label.toString().replace(/Model [0-9]+/g, 'Models'),
  171. priority: 100
  172. };
  173. register() { this.ctx.managers.lociLabels.addProvider(this.f); }
  174. unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); }
  175. constructor(protected ctx: PluginContext) { }
  176. },
  177. display: { name: 'Provide Default Loci Label' }
  178. });
  179. //
  180. const DefaultFocusLociBindings = {
  181. clickFocus: Binding([
  182. Trigger(B.Flag.Primary, M.create()),
  183. ], 'Representation Focus', 'Click element using ${triggers}'),
  184. clickFocusAdd: Binding([
  185. Trigger(B.Flag.Primary, M.create({ shift: true })),
  186. ], 'Representation Focus Add', 'Click element using ${triggers}'),
  187. clickFocusSelectMode: Binding([
  188. // default is empty
  189. ], 'Representation Focus', 'Click element using ${triggers}'),
  190. clickFocusAddSelectMode: Binding([
  191. // default is empty
  192. ], 'Representation Focus Add', 'Click element using ${triggers}'),
  193. };
  194. const FocusLociParams = {
  195. bindings: PD.Value(DefaultFocusLociBindings, { isHidden: true }),
  196. };
  197. type FocusLociProps = PD.Values<typeof FocusLociParams>
  198. export const FocusLoci = PluginBehavior.create<FocusLociProps>({
  199. name: 'representation-focus-loci',
  200. category: 'interaction',
  201. ctor: class extends PluginBehavior.Handler<FocusLociProps> {
  202. register(): void {
  203. this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
  204. const { clickFocus, clickFocusAdd, clickFocusSelectMode, clickFocusAddSelectMode } = this.params.bindings;
  205. // only apply structure focus for appropriate granularity
  206. const { granularity } = this.ctx.managers.interactivity.props;
  207. if (granularity !== 'residue' && granularity !== 'element') return;
  208. const binding = this.ctx.selectionMode ? clickFocusSelectMode : clickFocus;
  209. const matched = Binding.match(binding, button, modifiers);
  210. const bindingAdd = this.ctx.selectionMode ? clickFocusAddSelectMode : clickFocusAdd;
  211. const matchedAdd = Binding.match(bindingAdd, button, modifiers);
  212. if (!matched && !matchedAdd) return;
  213. const loci = Loci.normalize(current.loci, 'residue');
  214. const entry = this.ctx.managers.structure.focus.current;
  215. if (entry && Loci.areEqual(entry.loci, loci)) {
  216. this.ctx.managers.structure.focus.clear();
  217. } else {
  218. if (matched) {
  219. this.ctx.managers.structure.focus.setFromLoci(loci);
  220. } else {
  221. this.ctx.managers.structure.focus.addFromLoci(loci);
  222. // focus-add is not handled in camera behavior, doing it here
  223. const current = this.ctx.managers.structure.focus.current?.loci;
  224. if (current) this.ctx.managers.camera.focusLoci(current);
  225. }
  226. }
  227. });
  228. }
  229. },
  230. params: () => FocusLociParams,
  231. display: { name: 'Representation Focus Loci on Canvas' }
  232. });