representation.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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, isEmptyLoci } from '../../../mol-model/loci';
  18. import { Structure } from '../../../mol-model/structure';
  19. import { arrayMax } from '../../../mol-util/array';
  20. import { Representation } from '../../../mol-repr/representation';
  21. const B = ButtonsType
  22. const M = ModifiersKeys
  23. const Trigger = Binding.Trigger
  24. //
  25. const DefaultHighlightLociBindings = {
  26. hoverHighlightOnly: Binding([Trigger(B.Flag.None)], 'Highlight', 'Hover element using ${triggers}'),
  27. hoverHighlightOnlyExtend: Binding([Trigger(B.Flag.None, M.create({ shift: true }))], 'Extend highlight', 'From selected to hovered element along polymer using ${triggers}'),
  28. }
  29. const HighlightLociParams = {
  30. bindings: PD.Value(DefaultHighlightLociBindings, { isHidden: true }),
  31. }
  32. type HighlightLociProps = PD.Values<typeof HighlightLociParams>
  33. export const HighlightLoci = PluginBehavior.create({
  34. name: 'representation-highlight-loci',
  35. category: 'interaction',
  36. ctor: class extends PluginBehavior.Handler<HighlightLociProps> {
  37. private lociMarkProvider = (interactionLoci: Representation.Loci, action: MarkerAction) => {
  38. if (!this.ctx.canvas3d) return;
  39. this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action)
  40. }
  41. register() {
  42. this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, buttons, modifiers }) => {
  43. if (!this.ctx.canvas3d || this.ctx.isBusy) return
  44. let matched = false
  45. if (Binding.match(this.params.bindings.hoverHighlightOnly, buttons, modifiers)) {
  46. this.ctx.managers.interactivity.lociHighlights.highlightOnly(current)
  47. matched = true
  48. }
  49. if (Binding.match(this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) {
  50. this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend(current)
  51. matched = true
  52. }
  53. if (!matched) {
  54. this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci })
  55. }
  56. });
  57. this.ctx.managers.interactivity.lociHighlights.addProvider(this.lociMarkProvider)
  58. }
  59. unregister() {
  60. this.ctx.managers.interactivity.lociHighlights.removeProvider(this.lociMarkProvider)
  61. }
  62. },
  63. params: () => HighlightLociParams,
  64. display: { name: 'Highlight Loci on Canvas' }
  65. });
  66. //
  67. const DefaultSelectLociBindings = {
  68. clickSelect: Binding.Empty,
  69. clickToggleExtend: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Toggle extended selection', '${triggers} to extend selection along polymer'),
  70. clickSelectOnly: Binding.Empty,
  71. clickToggle: Binding([Trigger(B.Flag.Primary, M.create())], 'Toggle selection', '${triggers} on element'),
  72. clickDeselect: Binding.Empty,
  73. clickDeselectAllOnEmpty: Binding([Trigger(B.Flag.Primary, M.create())], 'Deselect all', 'Click on nothing using ${triggers}'),
  74. }
  75. const SelectLociParams = {
  76. bindings: PD.Value(DefaultSelectLociBindings, { isHidden: true }),
  77. }
  78. type SelectLociProps = PD.Values<typeof SelectLociParams>
  79. export const SelectLoci = PluginBehavior.create({
  80. name: 'representation-select-loci',
  81. category: 'interaction',
  82. ctor: class extends PluginBehavior.Handler<SelectLociProps> {
  83. private spine: StateTreeSpine.Impl
  84. private lociMarkProvider = (reprLoci: Representation.Loci, action: MarkerAction) => {
  85. if (!this.ctx.canvas3d) return;
  86. this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action)
  87. }
  88. private applySelectMark(ref: string, clear?: boolean) {
  89. const cell = this.ctx.state.data.cells.get(ref)
  90. if (cell && SO.isRepresentation3D(cell.obj)) {
  91. this.spine.current = cell
  92. const so = this.spine.getRootOfType(SO.Molecule.Structure)
  93. if (so) {
  94. if (clear) {
  95. this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect)
  96. }
  97. const loci = this.ctx.managers.structure.selection.getLoci(so.data)
  98. this.lociMarkProvider({ loci }, MarkerAction.Select)
  99. }
  100. }
  101. }
  102. register() {
  103. const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci)
  104. const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci)
  105. const actions: [keyof typeof DefaultSelectLociBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [
  106. ['clickSelect', current => this.ctx.managers.interactivity.lociSelects.select(current), lociIsNotEmpty],
  107. ['clickToggle', current => this.ctx.managers.interactivity.lociSelects.toggle(current), lociIsNotEmpty],
  108. ['clickToggleExtend', current => this.ctx.managers.interactivity.lociSelects.toggleExtend(current), lociIsNotEmpty],
  109. ['clickSelectOnly', current => this.ctx.managers.interactivity.lociSelects.selectOnly(current), lociIsNotEmpty],
  110. ['clickDeselect', current => this.ctx.managers.interactivity.lociSelects.deselect(current), lociIsNotEmpty],
  111. ['clickDeselectAllOnEmpty', () => this.ctx.managers.interactivity.lociSelects.deselectAll(), lociIsEmpty],
  112. ];
  113. // sort the action so that the ones with more modifiers trigger sooner.
  114. actions.sort((a, b) => {
  115. const x = this.params.bindings[a[0]], y = this.params.bindings[b[0]];
  116. const k = x.triggers.length === 0 ? 0 : arrayMax(x.triggers.map(t => M.size(t.modifiers)));
  117. const l = y.triggers.length === 0 ? 0 : arrayMax(y.triggers.map(t => M.size(t.modifiers)));
  118. return l - k;
  119. })
  120. this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
  121. if (!this.ctx.canvas3d || this.ctx.isBusy) return;
  122. // only trigger the 1st action that matches
  123. for (const [binding, action, condition] of actions) {
  124. if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) {
  125. action(current);
  126. break;
  127. }
  128. }
  129. });
  130. this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider)
  131. this.subscribeObservable(this.ctx.events.state.object.created, ({ ref }) => this.applySelectMark(ref));
  132. // re-apply select-mark to all representation of an updated structure
  133. this.subscribeObservable(this.ctx.events.state.object.updated, ({ ref }) => {
  134. const cell = this.ctx.state.data.cells.get(ref)
  135. if (cell && SO.Molecule.Structure.is(cell.obj)) {
  136. const reprs = this.ctx.state.data.select(StateSelection.Generators.ofType(SO.Molecule.Structure.Representation3D, ref))
  137. for (const repr of reprs) this.applySelectMark(repr.transform.ref, true)
  138. }
  139. });
  140. }
  141. unregister() {
  142. this.ctx.managers.interactivity.lociSelects.removeProvider(this.lociMarkProvider)
  143. }
  144. constructor(ctx: PluginContext, params: SelectLociProps) {
  145. super(ctx, params)
  146. this.spine = new StateTreeSpine.Impl(ctx.state.data.cells)
  147. }
  148. },
  149. params: () => SelectLociParams,
  150. display: { name: 'Select Loci on Canvas' }
  151. });
  152. //
  153. export const DefaultLociLabelProvider = PluginBehavior.create({
  154. name: 'default-loci-label-provider',
  155. category: 'interaction',
  156. ctor: class implements PluginBehavior<undefined> {
  157. private f = (loci: Loci) => lociLabel(loci);
  158. register() { this.ctx.managers.lociLabels.addProvider(this.f); }
  159. unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); }
  160. constructor(protected ctx: PluginContext) { }
  161. },
  162. display: { name: 'Provide Default Loci Label' }
  163. });
  164. //
  165. const DefaultFocusLociBindings = {
  166. clickFocus: Binding([
  167. Trigger(B.Flag.Secondary, M.create()),
  168. Trigger(B.Flag.Primary, M.create({ control: true }))
  169. ], 'Representation Focus', 'Click element using ${triggers}'),
  170. }
  171. const FocusLociParams = {
  172. bindings: PD.Value(DefaultFocusLociBindings, { isHidden: true }),
  173. }
  174. type FocusLociProps = PD.Values<typeof FocusLociParams>
  175. export const FocusLoci = PluginBehavior.create<FocusLociProps>({
  176. name: 'representation-focus-loci',
  177. category: 'interaction',
  178. ctor: class extends PluginBehavior.Handler<FocusLociProps> {
  179. register(): void {
  180. this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
  181. const { clickFocus } = this.params.bindings
  182. if (Binding.match(clickFocus, button, modifiers)) {
  183. const loci = Loci.normalize(current.loci, 'residue')
  184. const entry = this.ctx.managers.structure.focus.current
  185. if (entry && Loci.areEqual(entry.loci, loci)) {
  186. this.ctx.managers.structure.focus.clear()
  187. } else {
  188. this.ctx.managers.structure.focus.setFromLoci(loci)
  189. if (isEmptyLoci(loci)) {
  190. this.ctx.managers.camera.reset()
  191. }
  192. }
  193. }
  194. });
  195. }
  196. },
  197. params: () => FocusLociParams,
  198. display: { name: 'Representation Focus Loci on Canvas' }
  199. });