From cfcf9f681814b0dd8be82d340de722ae531cec08 Mon Sep 17 00:00:00 2001
From: Alexander Rose <alex.rose@rcsb.org>
Date: Tue, 18 Jun 2019 17:15:36 -0700
Subject: [PATCH] wip, plugin interactivity

---
 src/mol-model/structure/structure/element.ts  |  41 ++++++-
 .../behavior/dynamic/representation.ts        |  20 ++--
 src/mol-plugin/behavior/static/misc.ts        |  11 +-
 src/mol-plugin/command.ts                     |   2 +
 src/mol-plugin/context.ts                     |  15 +--
 src/mol-plugin/ui/sequence.tsx                |  18 +--
 src/mol-plugin/ui/viewport.tsx                |  18 +--
 .../util/{interaction.ts => interactivity.ts} | 104 +++++++++++++-----
 8 files changed, 165 insertions(+), 64 deletions(-)
 rename src/mol-plugin/util/{interaction.ts => interactivity.ts} (55%)

diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts
index 506e725af..9a0eaee3b 100644
--- a/src/mol-model/structure/structure/element.ts
+++ b/src/mol-model/structure/structure/element.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-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 { UniqueArray } from '../../../mol-data/generic';
@@ -224,6 +225,44 @@ namespace StructureElement {
             return Loci(loci.structure, elements);
         }
 
+        function getChainSegments(unit: Unit) {
+            switch (unit.kind) {
+                case Unit.Kind.Atomic: return unit.model.atomicHierarchy.chainAtomSegments
+                case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.chainElementSegments
+                case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.chainElementSegments
+            }
+        }
+
+        export function extendToWholeChains(loci: Loci): Loci {
+            const elements: Loci['elements'][0][] = [];
+
+            for (const lociElement of loci.elements) {
+                const newIndices: UnitIndex[] = [];
+                const unitElements = lociElement.unit.elements;
+
+                const { index: chainIndex, offsets: chainOffsets } = getChainSegments(lociElement.unit)
+
+                const indices = lociElement.indices, len = OrderedSet.size(indices);
+                let i = 0;
+                while (i < len) {
+                    const cI = chainIndex[unitElements[OrderedSet.getAt(indices, i)]];
+                    i++;
+                    while (i < len && chainIndex[unitElements[OrderedSet.getAt(indices, i)]] === cI) {
+                        i++;
+                    }
+
+                    for (let j = chainOffsets[cI], _j = chainOffsets[cI + 1]; j < _j; j++) {
+                        const idx = OrderedSet.indexOf(unitElements, j);
+                        if (idx >= 0) newIndices[newIndices.length] = idx as UnitIndex;
+                    }
+                }
+
+                elements[elements.length] = { unit: lociElement.unit, indices: SortedArray.ofSortedArray(newIndices) };
+            }
+
+            return Loci(loci.structure, elements);
+        }
+
         const boundaryHelper = new BoundaryHelper(), tempPos = Vec3.zero();
         export function getBoundary(loci: Loci): Boundary {
             boundaryHelper.reset(0);
diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts
index 20058f9b9..edf502302 100644
--- a/src/mol-plugin/behavior/dynamic/representation.ts
+++ b/src/mol-plugin/behavior/dynamic/representation.ts
@@ -10,22 +10,22 @@ import { PluginContext } from '../../../mol-plugin/context';
 import { PluginStateObject as SO } from '../../state/objects';
 import { labelFirst } from '../../../mol-theme/label';
 import { PluginBehavior } from '../behavior';
-import { Interaction } from '../../util/interaction';
+import { Interactivity } from '../../util/interactivity';
 import { StateTreeSpine } from '../../../mol-state/tree/spine';
 
 export const HighlightLoci = PluginBehavior.create({
     name: 'representation-highlight-loci',
     category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
-        private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => {
+        private lociMarkProvider = (interactionLoci: Interactivity.Loci, action: MarkerAction) => {
             if (!this.ctx.canvas3d) return;
-            this.ctx.canvas3d.mark({ ...loci, repr: undefined }, action)
+            this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action)
         }
         register() {
-            this.ctx.lociHighlights.addProvider(this.lociMarkProvider)
+            this.ctx.interactivity.lociHighlights.addProvider(this.lociMarkProvider)
         }
         unregister() {
-            this.ctx.lociHighlights.removeProvider(this.lociMarkProvider)
+            this.ctx.interactivity.lociHighlights.removeProvider(this.lociMarkProvider)
         }
     },
     display: { name: 'Highlight Loci on Canvas' }
@@ -36,14 +36,14 @@ export const SelectLoci = PluginBehavior.create({
     category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
         private spine: StateTreeSpine.Impl
-        private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => {
+        private lociMarkProvider = (interactionLoci: Interactivity.Loci, action: MarkerAction) => {
             if (!this.ctx.canvas3d) return;
-            this.ctx.canvas3d.mark({ ...loci, repr: undefined }, action)
+            this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action)
         }
         register() {
-            this.ctx.lociSelections.addProvider(this.lociMarkProvider)
+            this.ctx.interactivity.lociSelections.addProvider(this.lociMarkProvider)
 
-            this.subscribeObservable(this.ctx.events.state.object.created, ({ ref, state }) => {
+            this.subscribeObservable(this.ctx.events.state.object.created, ({ ref }) => {
                 const cell = this.ctx.state.dataState.cells.get(ref)
                 if (cell && SO.isRepresentation3D(cell.obj)) {
                     this.spine.current = cell
@@ -56,7 +56,7 @@ export const SelectLoci = PluginBehavior.create({
             });
         }
         unregister() {
-            this.ctx.lociSelections.removeProvider(this.lociMarkProvider)
+            this.ctx.interactivity.lociSelections.removeProvider(this.lociMarkProvider)
         }
         constructor(ctx: PluginContext, params: {}) {
             super(ctx, params)
diff --git a/src/mol-plugin/behavior/static/misc.ts b/src/mol-plugin/behavior/static/misc.ts
index 5807476dd..92e61436b 100644
--- a/src/mol-plugin/behavior/static/misc.ts
+++ b/src/mol-plugin/behavior/static/misc.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 { PluginContext } from '../../../mol-plugin/context';
@@ -9,6 +10,7 @@ import { PluginCommands } from '../../../mol-plugin/command';
 
 export function registerDefault(ctx: PluginContext) {
     Canvas3DSetSettings(ctx);
+    InteractivitySetProps(ctx);
 }
 
 export function Canvas3DSetSettings(ctx: PluginContext) {
@@ -17,3 +19,10 @@ export function Canvas3DSetSettings(ctx: PluginContext) {
         ctx.events.canvas3d.settingsUpdated.next();
     })
 }
+
+export function InteractivitySetProps(ctx: PluginContext) {
+    PluginCommands.Interactivity.SetProps.subscribe(ctx, e => {
+        ctx.interactivity.setProps(e.props);
+        ctx.events.interactivity.propsUpdated.next();
+    })
+}
diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts
index ce0c6a319..f29c1a5d7 100644
--- a/src/mol-plugin/command.ts
+++ b/src/mol-plugin/command.ts
@@ -11,6 +11,7 @@ import { Canvas3DProps } from '../mol-canvas3d/canvas3d';
 import { PluginLayoutStateProps } from './layout';
 import { StructureElement } from '../mol-model/structure';
 import { PluginState } from './state';
+import { Interactivity } from './util/interactivity';
 
 export * from './command/base';
 
@@ -43,6 +44,7 @@ export const PluginCommands = {
         }
     },
     Interactivity: {
+        SetProps: PluginCommand<{ props: Partial<Interactivity.Props> }>(),
         Structure: {
             Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>(),
             Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>()
diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index 20c0e810c..a11fe2ba1 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -36,7 +36,7 @@ import { SubstructureParentHelper } from './util/substructure-parent-helper';
 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';
+import { Interactivity } from './util/interactivity';
 
 interface Log {
     entries: List<LogEntry>
@@ -74,6 +74,9 @@ export class PluginContext {
         task: this.tasks.events,
         canvas3d: {
             settingsUpdated: this.ev()
+        },
+        interactivity: {
+            propsUpdated: this.ev()
         }
     } as const
 
@@ -83,8 +86,8 @@ export class PluginContext {
             isUpdating: this.ev.behavior<boolean>(false)
         },
         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 })
+            highlight: this.ev.behavior<Interactivity.HighlightEvent>({ current: Interactivity.Loci.Empty }),
+            click: this.ev.behavior<Interactivity.ClickEvent>({ current: Interactivity.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 })
         },
         labels: {
             highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] })
@@ -93,10 +96,9 @@ export class PluginContext {
 
     readonly canvas3d: Canvas3D;
     readonly layout: PluginLayout = new PluginLayout(this);
+    readonly interactivity: Interactivity;
 
     readonly lociLabels: LociLabelManager;
-    readonly lociSelections: Interaction.LociSelectionManager;
-    readonly lociHighlights: Interaction.LociHighlightManager;
 
     readonly structureRepresentation = {
         registry: new StructureRepresentationRegistry(),
@@ -238,9 +240,8 @@ export class PluginContext {
         this.initAnimations();
         this.initCustomParamEditors();
 
+        this.interactivity = new Interactivity(this);
         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`);
diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx
index 239d5e316..073bbccfe 100644
--- a/src/mol-plugin/ui/sequence.tsx
+++ b/src/mol-plugin/ui/sequence.tsx
@@ -10,7 +10,7 @@ import { Structure, StructureSequence, Queries, StructureSelection, StructurePro
 import { PluginUIComponent } from './base';
 import { StateTreeSpine } from '../../mol-state/tree/spine';
 import { PluginStateObject as SO } from '../state/objects';
-import { Interaction } from '../util/interaction';
+import { Interactivity } from '../util/interactivity';
 import { OrderedSet, Interval } from '../../mol-data/int';
 import { Loci } from '../../mol-model/loci';
 import { applyMarkerAction, MarkerAction } from '../../mol-util/marker-action';
@@ -156,13 +156,13 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen
         markerData: ValueBox.create(new Uint8Array(this.props.markerArray))
     }
 
-    private lociHighlightProvider = (loci: Interaction.Loci, action: MarkerAction) => {
+    private lociHighlightProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
         const { markerData } = this.state;
         const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action)
         if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) })
     }
 
-    private lociSelectionProvider = (loci: Interaction.Loci, action: MarkerAction) => {
+    private lociSelectionProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
         const { markerData } = this.state;
         const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action)
         if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) })
@@ -176,13 +176,13 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen
     }
 
     componentDidMount() {
-        this.plugin.lociHighlights.addProvider(this.lociHighlightProvider)
-        this.plugin.lociSelections.addProvider(this.lociSelectionProvider)
+        this.plugin.interactivity.lociHighlights.addProvider(this.lociHighlightProvider)
+        this.plugin.interactivity.lociSelections.addProvider(this.lociSelectionProvider)
     }
 
     componentWillUnmount() {
-        this.plugin.lociHighlights.removeProvider(this.lociHighlightProvider)
-        this.plugin.lociSelections.removeProvider(this.lociSelectionProvider)
+        this.plugin.interactivity.lociHighlights.removeProvider(this.lociHighlightProvider)
+        this.plugin.interactivity.lociSelections.removeProvider(this.lociSelectionProvider)
     }
 
     getLoci(seqId: number) {
@@ -192,7 +192,7 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen
     }
 
     highlight(seqId?: number, modifiers?: ModifiersKeys) {
-        const ev = { current: Interaction.Loci.Empty, modifiers }
+        const ev = { current: Interactivity.Loci.Empty, modifiers }
         if (seqId !== undefined) {
             const loci = this.getLoci(seqId);
             if (loci.elements.length > 0) ev.current = { loci };
@@ -201,7 +201,7 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen
     }
 
     click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
-        const ev = { current: Interaction.Loci.Empty, buttons, modifiers }
+        const ev = { current: Interactivity.Loci.Empty, buttons, modifiers }
         if (seqId !== undefined) {
             const loci = this.getLoci(seqId);
             if (loci.elements.length > 0) ev.current = { loci };
diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx
index 12c97fdfb..539496b0d 100644
--- a/src/mol-plugin/ui/viewport.tsx
+++ b/src/mol-plugin/ui/viewport.tsx
@@ -14,6 +14,7 @@ import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
 import { PluginLayoutStateParams } from '../../mol-plugin/layout';
 import { ControlGroup, IconButton } from './controls/common';
 import { resizeCanvas } from '../../mol-canvas3d/util';
+import { Interactivity } from '../util/interactivity';
 
 interface ViewportState {
     noWebGl: boolean
@@ -49,14 +50,14 @@ export class ViewportControls extends PluginUIComponent<{}, { isSettingsExpanded
         PluginCommands.Layout.Update.dispatch(this.plugin, { state: { [p.name]: p.value } });
     }
 
-    componentDidMount() {
-        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, e => {
-            this.forceUpdate();
-        });
+    setInteractivityProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { [p.name]: p.value } });
+    }
 
-        this.subscribe(this.plugin.layout.events.updated, () => {
-            this.forceUpdate();
-        });
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+        this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
+        this.subscribe(this.plugin.events.interactivity.propsUpdated, () => this.forceUpdate());
     }
 
     icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
@@ -75,6 +76,9 @@ export class ViewportControls extends PluginUIComponent<{}, { isSettingsExpanded
                 <ControlGroup header='Layout' initialExpanded={true}>
                     <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} />
                 </ControlGroup>
+                <ControlGroup header='Interactivity' initialExpanded={true}>
+                    <ParameterControls params={Interactivity.Params} values={this.plugin.interactivity.props} onChange={this.setInteractivityProps} />
+                </ControlGroup>
                 <ControlGroup header='Viewport' initialExpanded={true}>
                     <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
                 </ControlGroup>
diff --git a/src/mol-plugin/util/interaction.ts b/src/mol-plugin/util/interactivity.ts
similarity index 55%
rename from src/mol-plugin/util/interaction.ts
rename to src/mol-plugin/util/interactivity.ts
index e97670cbd..a33435481 100644
--- a/src/mol-plugin/util/interaction.ts
+++ b/src/mol-plugin/util/interactivity.ts
@@ -12,8 +12,37 @@ import { StructureElement } from '../../mol-model/structure';
 import { MarkerAction } from '../../mol-util/marker-action';
 import { StructureElementSelectionManager } from './structure-element-selection';
 import { PluginContext } from '../context';
+import { StructureElement as SE, Structure } from '../../mol-model/structure';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { PluginCommands } from '../command';
+import { capitalize } from '../../mol-util/string';
 
-export namespace Interaction {
+export { Interactivity }
+
+class Interactivity {
+    readonly lociSelections: Interactivity.LociSelectionManager;
+    readonly lociHighlights: Interactivity.LociHighlightManager;
+
+    private _props = PD.getDefaultValues(Interactivity.Params)
+
+    get props() { return { ...this._props } }
+    setProps(props: Partial<Interactivity.Props>) {
+        Object.assign(this._props, props)
+        this.lociSelections.setProps(this._props)
+        this.lociHighlights.setProps(this._props)
+    }
+
+    constructor(readonly ctx: PluginContext, props: Partial<Interactivity.Props> = {}) {
+        Object.assign(this._props, props)
+
+        this.lociSelections = new Interactivity.LociSelectionManager(ctx, this._props);
+        this.lociHighlights = new Interactivity.LociHighlightManager(ctx, this._props);
+
+        PluginCommands.Interactivity.SetProps.subscribe(ctx, e => this.setProps(e.props));
+    }
+}
+
+namespace Interactivity {
     export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any }
 
     export namespace Loci {
@@ -23,6 +52,20 @@ export namespace Interaction {
         export const Empty: Loci = { loci: EmptyLoci };
     }
 
+    const LociExpansion = {
+        'none': (loci: ModelLoci) => loci,
+        'residue': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeResidues(loci) : loci,
+        'chain': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeChains(loci) : loci,
+        'structure': (loci: ModelLoci) => SE.isLoci(loci) ? Structure.Loci(loci.structure) : loci
+    }
+    type LociExpansion = keyof typeof LociExpansion
+    const LociExpansionOptions = Object.keys(LociExpansion).map(n => [n, capitalize(n)]) as [LociExpansion, string][]
+
+    export const Params = {
+        lociExpansion: PD.Select('residue', LociExpansionOptions),
+    }
+    export type Props = PD.Values<typeof Params>
+
     export interface HighlightEvent { current: Loci, modifiers?: ModifiersKeys }
     export interface ClickEvent { current: Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
 
@@ -32,6 +75,12 @@ export namespace Interaction {
         protected providers: LociMarkProvider[] = [];
         protected sel: StructureElementSelectionManager
 
+        readonly props: Readonly<Props> = PD.getDefaultValues(Params)
+
+        setProps(props: Partial<Props>) {
+            Object.assign(this.props, props)
+        }
+
         addProvider(provider: LociMarkProvider) {
             this.providers.push(provider);
         }
@@ -41,14 +90,8 @@ export namespace Interaction {
             // 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);
-            }
+        expandLoci(loci: ModelLoci) {
+            return LociExpansion[this.props.lociExpansion](loci)
         }
 
         protected mark(current: Loci<ModelLoci>, action: MarkerAction) {
@@ -57,8 +100,9 @@ export namespace Interaction {
 
         abstract apply(e: MarkEvent): void
 
-        constructor(public ctx: PluginContext) {
+        constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) {
             this.sel = ctx.helpers.structureSelection
+            this.setProps(props)
         }
     }
 
@@ -67,27 +111,28 @@ export namespace Interaction {
 
         apply(e: HighlightEvent) {
             const { current, modifiers } = e
-            if (StructureElement.isLoci(current.loci)) {
-                let loci: StructureElement.Loci = current.loci;
+            const expanded: Loci<ModelLoci> = { loci: this.expandLoci(current.loci), repr: current.repr }
+            if (StructureElement.isLoci(expanded.loci)) {
+                let loci: StructureElement.Loci = expanded.loci;
                 if (modifiers && modifiers.shift) {
                     loci = this.sel.tryGetRange(loci) || loci;
                 }
 
                 this.mark(this.prev, MarkerAction.RemoveHighlight);
-                const toHighlight = { loci, repr: current.repr };
+                const toHighlight = { loci, repr: expanded.repr };
                 this.mark(toHighlight, MarkerAction.Highlight);
                 this.prev = toHighlight;
             } else {
-                if (!Loci.areEqual(this.prev, current)) {
+                if (!Loci.areEqual(this.prev, expanded)) {
                     this.mark(this.prev, MarkerAction.RemoveHighlight);
-                    this.mark(current, MarkerAction.Highlight);
-                    this.prev = current;
+                    this.mark(expanded, MarkerAction.Highlight);
+                    this.prev = expanded;
                 }
             }
         }
 
-        constructor(public ctx: PluginContext) {
-            super(ctx)
+        constructor(ctx: PluginContext, props: Partial<Props> = {}) {
+            super(ctx, props)
             ctx.behaviors.interaction.highlight.subscribe(e => this.apply(e));
         }
     }
@@ -105,38 +150,39 @@ export namespace Interaction {
 
         apply(e: ClickEvent) {
             const { current, buttons, modifiers } = e
-            if (current.loci.kind === 'empty-loci') {
+            const expanded: Loci<ModelLoci> = { loci: this.expandLoci(current.loci), repr: current.repr }
+            if (expanded.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)) {
+            } else if (StructureElement.isLoci(expanded.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);
+                    const old = this.sel.get(expanded.loci.structure);
                     this.mark({ loci: old }, MarkerAction.Deselect);
-                    this.sel.set(current.loci);
-                    this.mark(current, MarkerAction.Select);
+                    this.sel.set(expanded.loci);
+                    this.mark(expanded, 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>);
+                    this.toggleSel(expanded 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;
+                    let loci: StructureElement.Loci = expanded.loci;
                     if (modifiers && modifiers.shift) {
                         loci = this.sel.tryGetRange(loci) || loci;
                     }
-                    this.toggleSel({ loci, repr: current.repr });
+                    this.toggleSel({ loci, repr: expanded.repr });
                 }
             } else {
                 if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return;
-                for (let p of this.providers) p(current, MarkerAction.Toggle);
+                for (let p of this.providers) p(expanded, MarkerAction.Toggle);
             }
         }
 
-        constructor(public ctx: PluginContext) {
-            super(ctx)
+        constructor(ctx: PluginContext, props: Partial<Props> = {}) {
+            super(ctx, props)
             ctx.behaviors.interaction.click.subscribe(e => this.apply(e));
         }
     }
-- 
GitLab