Browse Source

plugin interaction helpers refactoring

Alexander Rose 5 years ago
parent
commit
f03ce68513

+ 2 - 2
src/mol-plugin/behavior/dynamic/camera.ts

@@ -14,7 +14,7 @@ export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extr
     category: 'interaction',
     ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> {
         register(): void {
-            this.subscribeObservable(this.ctx.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
+            this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
                 if (!this.ctx.canvas3d || buttons !== ButtonsType.Flag.Primary || !ModifiersKeys.areEqual(modifiers, ModifiersKeys.None)) return;
 
                 const sphere = Loci.getBoundingSphere(current.loci);
@@ -25,7 +25,7 @@ export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extr
     },
     params: () => ({
         minRadius: ParamDefinition.Numeric(8, { min: 1, max: 50, step: 1 }),
-        extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the boundning sphere radius of the Loci.' })
+        extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci.' })
     }),
     display: { name: 'Focus Loci on Select' }
 });

+ 20 - 76
src/mol-plugin/behavior/dynamic/representation.ts

@@ -6,44 +6,24 @@
  */
 
 import { MarkerAction } from '../../../mol-geo/geometry/marker-data';
-import { EmptyLoci } from '../../../mol-model/loci';
-import { StructureElement } from '../../../mol-model/structure';
 import { PluginContext } from '../../../mol-plugin/context';
-import { Representation } from '../../../mol-repr/representation';
 import { labelFirst } from '../../../mol-theme/label';
-import { ButtonsType } from '../../../mol-util/input/input-observer';
 import { PluginBehavior } from '../behavior';
+import { Interaction } from '../../util/interaction';
 
 export const HighlightLoci = PluginBehavior.create({
     name: 'representation-highlight-loci',
     category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
-        register(): void {
-            let prev: Representation.Loci = { loci: EmptyLoci, repr: void 0 };
-            const sel = this.ctx.helpers.structureSelection;
-
-            this.subscribeObservable(this.ctx.behaviors.canvas3d.highlight, ({ current, modifiers }) => {
-                if (!this.ctx.canvas3d) return;
-
-                if (StructureElement.isLoci(current.loci)) {
-                    let loci: StructureElement.Loci = current.loci;
-                    if (modifiers && modifiers.shift) {
-                        loci = sel.tryGetRange(loci) || loci;
-                    }
-
-                    this.ctx.canvas3d.mark(prev, MarkerAction.RemoveHighlight);
-                    const toHighlight = { loci, repr: current.repr };
-                    this.ctx.canvas3d.mark(toHighlight, MarkerAction.Highlight);
-                    prev = toHighlight;
-                } else {
-                    if (!Representation.Loci.areEqual(prev, current)) {
-                        this.ctx.canvas3d.mark(prev, MarkerAction.RemoveHighlight);
-                        this.ctx.canvas3d.mark(current, MarkerAction.Highlight);
-                        prev = current;
-                    }
-                }
-
-            });
+        private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => {
+            if (!this.ctx.canvas3d) return;
+            this.ctx.canvas3d.mark(loci, action)
+        }
+        register() {
+            this.ctx.lociHighlights.addProvider(this.lociMarkProvider)
+        }
+        unregister() {
+            this.ctx.lociHighlights.removeProvider(this.lociMarkProvider)
         }
     },
     display: { name: 'Highlight Loci on Canvas' }
@@ -53,51 +33,15 @@ export const SelectLoci = PluginBehavior.create({
     name: 'representation-select-loci',
     category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
-        register(): void {
-            const sel = this.ctx.helpers.structureSelection;
-
-            const toggleSel = (current: Representation.Loci<StructureElement.Loci>) => {
-                if (sel.has(current.loci)) {
-                    sel.remove(current.loci);
-                    this.ctx.canvas3d.mark(current, MarkerAction.Deselect);
-                } else {
-                    sel.add(current.loci);
-                    this.ctx.canvas3d.mark(current, MarkerAction.Select);
-                }
-            }
-
-            this.subscribeObservable(this.ctx.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
-                if (!this.ctx.canvas3d) return;
-
-                if (current.loci.kind === 'empty-loci') {
-                    if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
-                        // clear the selection on Ctrl + Right-Click on empty
-                        const sels = sel.clear();
-                        for (const s of sels) this.ctx.canvas3d.mark({ loci: s }, MarkerAction.Deselect);
-                    }
-                } else if (StructureElement.isLoci(current.loci)) {
-                    if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
-                        // select only the current element on Ctrl + Right-Click
-                        const old = sel.get(current.loci.structure);
-                        this.ctx.canvas3d.mark({ loci: old }, MarkerAction.Deselect);
-                        sel.set(current.loci);
-                        this.ctx.canvas3d.mark(current, MarkerAction.Select);
-                    } else if (modifiers.control && buttons === ButtonsType.Flag.Primary) {
-                        // toggle current element on Ctrl + Left-Click
-                        toggleSel(current as Representation.Loci<StructureElement.Loci>);
-                    } else if (modifiers.shift && buttons === ButtonsType.Flag.Primary) {
-                        // try to extend sequence on Shift + Left-Click
-                        let loci: StructureElement.Loci = current.loci;
-                        if (modifiers && modifiers.shift) {
-                            loci = sel.tryGetRange(loci) || loci;
-                        }
-                        toggleSel({ loci, repr: current.repr });
-                    }
-                } else {
-                    if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return;
-                    this.ctx.canvas3d.mark(current, MarkerAction.Toggle);
-                }
-            });
+        private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => {
+            if (!this.ctx.canvas3d) return;
+            this.ctx.canvas3d.mark(loci, action)
+        }
+        register() {
+            this.ctx.lociSelections.addProvider(this.lociMarkProvider)
+        }
+        unregister() {
+            this.ctx.lociSelections.removeProvider(this.lociMarkProvider)
         }
     },
     display: { name: 'Select Loci on Canvas' }
@@ -108,7 +52,7 @@ export const DefaultLociLabelProvider = PluginBehavior.create({
     category: 'interaction',
     ctor: class implements PluginBehavior<undefined> {
         private f = labelFirst;
-        register(): void { this.ctx.lociLabels.addProvider(this.f); }
+        register() { this.ctx.lociLabels.addProvider(this.f); }
         unregister() { this.ctx.lociLabels.removeProvider(this.f); }
         constructor(protected ctx: PluginContext) { }
     },

+ 1 - 1
src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts

@@ -121,7 +121,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
             }
         });
 
-        this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
+        this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
             if (buttons !== ButtonsType.Flag.Secondary) return;
 
             if (current.loci.kind === 'empty-loci') {

+ 1 - 1
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -195,7 +195,7 @@ export namespace VolumeStreaming {
                 }
             });
 
-            this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
+            this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
                 if (buttons !== ButtonsType.Flag.Secondary || this.params.view.name !== 'selection-box') return;
 
                 if (current.loci.kind === 'empty-loci') {

+ 10 - 5
src/mol-plugin/context.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { List } from 'immutable';
@@ -32,10 +33,10 @@ import { TaskManager } from './util/task-manager';
 import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
 import { StructureElementSelectionManager } from './util/structure-element-selection';
 import { SubstructureParentHelper } from './util/substructure-parent-helper';
-import { Representation } from '../mol-repr/representation';
 import { ModifiersKeys } from '../mol-util/input/input-observer';
 import { isProductionMode, isDebugMode } from '../mol-util/debug';
 import { Model, Structure } from '../mol-model/structure';
+import { Interaction } from './util/interaction';
 
 export class PluginContext {
     private disposed = false;
@@ -72,9 +73,9 @@ export class PluginContext {
             isAnimating: this.ev.behavior<boolean>(false),
             isUpdating: this.ev.behavior<boolean>(false)
         },
-        canvas3d: {
-            highlight: this.ev.behavior<Canvas3D.HighlightEvent>({ current: Representation.Loci.Empty }),
-            click: this.ev.behavior<Canvas3D.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 })
+        interaction: {
+            highlight: this.ev.behavior<Interaction.HighlightEvent>({ current: Interaction.Loci.Empty }),
+            click: this.ev.behavior<Interaction.ClickEvent>({ current: Interaction.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 })
         },
         labels: {
             highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] })
@@ -85,6 +86,8 @@ export class PluginContext {
     readonly layout: PluginLayout = new PluginLayout(this);
 
     readonly lociLabels: LociLabelManager;
+    readonly lociSelections: Interaction.LociSelectionManager;
+    readonly lociHighlights: Interaction.LociHighlightManager;
 
     readonly structureRepresentation = {
         registry: new StructureRepresentationRegistry(),
@@ -227,6 +230,8 @@ export class PluginContext {
         this.initCustomParamEditors();
 
         this.lociLabels = new LociLabelManager(this);
+        this.lociSelections = new Interaction.LociSelectionManager(this);
+        this.lociHighlights = new Interaction.LociHighlightManager(this);
 
         this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`);
         if (!isProductionMode) this.log.message(`Development mode enabled`);

+ 2 - 2
src/mol-plugin/ui/viewport.tsx

@@ -109,8 +109,8 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
         const canvas3d = this.plugin.canvas3d;
         this.subscribe(canvas3d.input.resize, this.handleResize);
 
-        this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.canvas3d.click.next(e));
-        this.subscribe(canvas3d.interaction.highlight, e => this.plugin.behaviors.canvas3d.highlight.next(e));
+        this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.interaction.click.next(e));
+        this.subscribe(canvas3d.interaction.highlight, e => this.plugin.behaviors.interaction.highlight.next(e));
         this.subscribe(this.plugin.layout.events.updated, () => {
             setTimeout(this.handleResize, 50);
         });

+ 143 - 0
src/mol-plugin/util/interaction.ts

@@ -0,0 +1,143 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Loci as ModelLoci, EmptyLoci } from '../../mol-model/loci';
+import { ModifiersKeys, ButtonsType } from '../../mol-util/input/input-observer';
+import { Representation } from '../../mol-repr/representation';
+import { StructureElement } from '../../mol-model/structure';
+import { MarkerAction } from '../../mol-geo/geometry/marker-data';
+import { StructureElementSelectionManager } from './structure-element-selection';
+import { PluginContext } from '../context';
+
+export namespace Interaction {
+    export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any }
+
+    export namespace Loci {
+        export function areEqual(a: Loci, b: Loci) {
+            return a.repr === b.repr && ModelLoci.areEqual(a.loci, b.loci);
+        }
+        export const Empty: Loci = { loci: EmptyLoci };
+    }
+
+    export interface HighlightEvent { current: Loci, modifiers?: ModifiersKeys }
+    export interface ClickEvent { current: Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
+
+    export type LociMarkProvider = (loci: Loci, action: MarkerAction) => void
+
+    export abstract class LociMarkManager<MarkEvent extends any> {
+        protected providers: LociMarkProvider[] = [];
+        protected sel: StructureElementSelectionManager
+
+        addProvider(provider: LociMarkProvider) {
+            this.providers.push(provider);
+        }
+
+        removeProvider(provider: LociMarkProvider) {
+            this.providers = this.providers.filter(p => p !== provider);
+            // TODO clear, then re-apply remaining providers
+        }
+
+        toggleSel(current: Loci<ModelLoci>) {
+            if (this.sel.has(current.loci)) {
+                this.sel.remove(current.loci);
+                this.mark(current, MarkerAction.Deselect);
+            } else {
+                this.sel.add(current.loci);
+                this.mark(current, MarkerAction.Select);
+            }
+        }
+
+        protected mark(current: Loci<ModelLoci>, action: MarkerAction) {
+            for (let p of this.providers) p(current, action);
+        }
+
+        abstract apply(e: MarkEvent): void
+
+        constructor(public ctx: PluginContext) {
+            this.sel = ctx.helpers.structureSelection
+        }
+    }
+
+    export class LociHighlightManager extends LociMarkManager<HighlightEvent> {
+        private prev: Loci = { loci: EmptyLoci, repr: void 0 };
+
+        apply(e: HighlightEvent) {
+            const { current, modifiers } = e
+            if (StructureElement.isLoci(current.loci)) {
+                let loci: StructureElement.Loci = current.loci;
+                if (modifiers && modifiers.shift) {
+                    loci = this.sel.tryGetRange(loci) || loci;
+                }
+
+                this.mark(this.prev, MarkerAction.RemoveHighlight);
+                const toHighlight = { loci, repr: current.repr };
+                this.mark(toHighlight, MarkerAction.Highlight);
+                this.prev = toHighlight;
+            } else {
+                if (!Loci.areEqual(this.prev, current)) {
+                    this.mark(this.prev, MarkerAction.RemoveHighlight);
+                    this.mark(current, MarkerAction.Highlight);
+                    this.prev = current;
+                }
+            }
+        }
+
+        constructor(public ctx: PluginContext) {
+            super(ctx)
+            ctx.behaviors.interaction.highlight.subscribe(e => this.apply(e));
+        }
+    }
+
+    export class LociSelectionManager extends LociMarkManager<ClickEvent> {
+        toggleSel(current: Loci<ModelLoci>) {
+            if (this.sel.has(current.loci)) {
+                this.sel.remove(current.loci);
+                this.mark(current, MarkerAction.Deselect);
+            } else {
+                this.sel.add(current.loci);
+                this.mark(current, MarkerAction.Select);
+            }
+        }
+
+        apply(e: ClickEvent) {
+            const { current, buttons, modifiers } = e
+            if (current.loci.kind === 'empty-loci') {
+                if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
+                    // clear the selection on Ctrl + Right-Click on empty
+                    const sels = this.sel.clear();
+                    for (const s of sels) this.mark({ loci: s }, MarkerAction.Deselect);
+                }
+            } else if (StructureElement.isLoci(current.loci)) {
+                if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
+                    // select only the current element on Ctrl + Right-Click
+                    const old = this.sel.get(current.loci.structure);
+                    this.mark({ loci: old }, MarkerAction.Deselect);
+                    this.sel.set(current.loci);
+                    this.mark(current, MarkerAction.Select);
+                } else if (modifiers.control && buttons === ButtonsType.Flag.Primary) {
+                    // toggle current element on Ctrl + Left-Click
+                    this.toggleSel(current as Representation.Loci<StructureElement.Loci>);
+                } else if (modifiers.shift && buttons === ButtonsType.Flag.Primary) {
+                    // try to extend sequence on Shift + Left-Click
+                    let loci: StructureElement.Loci = current.loci;
+                    if (modifiers && modifiers.shift) {
+                        loci = this.sel.tryGetRange(loci) || loci;
+                    }
+                    this.toggleSel({ loci, repr: current.repr });
+                }
+            } else {
+                if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return;
+                for (let p of this.providers) p(current, MarkerAction.Toggle);
+            }
+        }
+
+        constructor(public ctx: PluginContext) {
+            super(ctx)
+            ctx.behaviors.interaction.click.subscribe(e => this.apply(e));
+        }
+    }
+}

+ 1 - 1
src/mol-plugin/util/loci-label-manager.ts

@@ -35,6 +35,6 @@ export class LociLabelManager {
     }
 
     constructor(public ctx: PluginContext) {
-        ctx.behaviors.canvas3d.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) }));
+        ctx.behaviors.interaction.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) }));
     }
 }