Bladeren bron

wip selection model

David Sehnal 6 jaren geleden
bovenliggende
commit
0e0529d35e

+ 6 - 5
src/mol-canvas3d/canvas3d.ts

@@ -59,8 +59,8 @@ interface Canvas3D {
     animate: () => void
     pick: () => void
     identify: (x: number, y: number) => Promise<PickingId | undefined>
-    mark: (loci: Loci, action: MarkerAction, repr?: Representation.Any) => void
-    getLoci: (pickingId: PickingId) => { loci: Loci, repr?: Representation.Any }
+    mark: (loci: Representation.Loci, action: MarkerAction) => void
+    getLoci: (pickingId: PickingId) => Representation.Loci
 
     readonly didDraw: BehaviorSubject<now.Timestamp>
 
@@ -141,12 +141,13 @@ namespace Canvas3D {
             return { loci, repr }
         }
 
-        function mark(loci: Loci, action: MarkerAction, repr?: Representation.Any) {
+        function mark(loci: Representation.Loci, action: MarkerAction) {
+            const repr = loci.repr
             let changed = false
             if (repr) {
-                changed = repr.mark(loci, action)
+                changed = repr.mark(loci.loci, action)
             } else {
-                reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
+                reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci.loci, action) || changed })
             }
             if (changed) {
                 scene.update(true)

+ 1 - 1
src/mol-model/structure/model/properties/utils/secondary-structure.ts

@@ -66,7 +66,7 @@ interface DSSPContext {
     hbonds: DsspHbonds
 }
 
-interface DSSPType extends BitFlags<DSSPType.Flag> { }
+type DSSPType = BitFlags<DSSPType.Flag>
 namespace DSSPType {
     export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has
     export const create: (f: Flag) => DSSPType = BitFlags.create

+ 2 - 2
src/mol-model/structure/model/types.ts

@@ -263,7 +263,7 @@ export const IonNames = new Set([
   'OHX'
 ])
 
-export interface SecondaryStructureType extends BitFlags<SecondaryStructureType.Flag> { }
+export type SecondaryStructureType = BitFlags<SecondaryStructureType.Flag>
 export namespace SecondaryStructureType {
     export const is: (ss: SecondaryStructureType, f: Flag) => boolean = BitFlags.has
     export const create: (fs: Flag) => SecondaryStructureType = BitFlags.create
@@ -511,7 +511,7 @@ export const VdwRadii = {
 }
 export const DefaultVdwRadius = 2.0
 
-export interface LinkType extends BitFlags<LinkType.Flag> { }
+export type LinkType = BitFlags<LinkType.Flag>
 export namespace LinkType {
     export const is: (b: LinkType, f: Flag) => boolean = BitFlags.has
     export const enum Flag {

+ 50 - 0
src/mol-model/structure/structure/element.ts

@@ -108,6 +108,56 @@ namespace StructureElement {
                 return l.unit.model.coarseHierarchy.gaussians.entityKey[l.element]
         }
     }
+
+    export namespace Loci {
+        export function all(structure: Structure): Loci {
+            return Loci(structure, structure.units.map(unit => ({
+                unit,
+                indices: OrderedSet.ofRange<UnitIndex>(0 as UnitIndex, unit.elements.length as UnitIndex)
+            })));
+        }
+
+        export function union(xs: Loci, ys: Loci): Loci {
+            if (xs.structure !== ys.structure) throw new Error(`Can't union Loci of different structures.`);
+            if (xs.elements.length > ys.elements.length) return union(ys, xs);
+            if (xs.elements.length === 0) return ys;
+
+            const map = new Map<number, OrderedSet<UnitIndex>>();
+
+            for (const e of xs.elements) map.set(e.unit.id, e.indices);
+
+            const elements: Loci['elements'][0][] = [];
+            for (const e of ys.elements) {
+                if (map.has(e.unit.id)) {
+                    elements[elements.length] = { unit: e.unit, indices: OrderedSet.union(map.get(e.unit.id)!, e.indices) };
+                } else {
+                    elements[elements.length] = e;
+                }
+            }
+
+            return Loci(xs.structure, elements);
+        }
+
+        export function subtract(xs: Loci, ys: Loci): Loci {
+            if (xs.structure !== ys.structure) throw new Error(`Can't subtract Loci of different structures.`);
+
+            const map = new Map<number, OrderedSet<UnitIndex>>();
+            for (const e of ys.elements) map.set(e.unit.id, e.indices);
+
+            const elements: Loci['elements'][0][] = [];
+            for (const e of xs.elements) {
+                if (map.has(e.unit.id)) {
+                    const indices = OrderedSet.subtract(e.indices, map.get(e.unit.id)!);
+                    if (OrderedSet.size(indices) === 0) continue;
+                    elements[elements.length] = { unit: e.unit, indices };
+                } else {
+                    elements[elements.length] = e;
+                }
+            }
+
+            return xs;
+        }
+    }
 }
 
 export default StructureElement

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

@@ -13,9 +13,9 @@ 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.canvas.selectLoci, current => {
+            this.subscribeObservable(this.ctx.events.canvas3d.click, current => {
                 if (!this.ctx.canvas3d) return;
-                const sphere = Loci.getBoundingSphere(current.loci);
+                const sphere = Loci.getBoundingSphere(current.loci.loci);
                 if (!sphere) return;
                 this.ctx.canvas3d.camera.focus(sphere.center, Math.max(sphere.radius + this.params.extraRadius, this.params.minRadius));
             });

+ 16 - 16
src/mol-plugin/behavior/dynamic/representation.ts

@@ -6,7 +6,7 @@
 
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
-import { EmptyLoci, Loci } from 'mol-model/loci';
+import { EmptyLoci } from 'mol-model/loci';
 import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginStateObject } from 'mol-plugin/state/objects';
@@ -14,21 +14,22 @@ import { StateObjectTracker, StateSelection } from 'mol-state';
 import { labelFirst } from 'mol-theme/label';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PluginBehavior } from '../behavior';
+import { Representation } from 'mol-repr/representation';
 
 export const HighlightLoci = PluginBehavior.create({
     name: 'representation-highlight-loci',
     category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
         register(): void {
-            let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
-            this.subscribeObservable(this.ctx.behaviors.canvas.highlightLoci, current => {
+            let prev: Representation.Loci = { loci: EmptyLoci, repr: void 0 };
+
+            this.subscribeObservable(this.ctx.events.canvas3d.highlight, ({ loci }) => {
                 if (!this.ctx.canvas3d) return;
 
-                if (current.repr !== prevRepr || !Loci.areEqual(current.loci, prevLoci)) {
-                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight, prevRepr);
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight, current.repr);
-                    prevLoci = current.loci;
-                    prevRepr = current.repr;
+                if (!Representation.Loci.areEqual(prev, loci)) {
+                    this.ctx.canvas3d.mark(prev, MarkerAction.RemoveHighlight);
+                    this.ctx.canvas3d.mark(loci, MarkerAction.Highlight);
+                    prev = loci;
                 }
             });
         }
@@ -41,16 +42,15 @@ export const SelectLoci = PluginBehavior.create({
     category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
         register(): void {
-            let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
-            this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => {
+            let prev = Representation.Loci.Empty;
+            this.subscribeObservable(this.ctx.events.canvas3d.click, ({ loci: current }) => {
                 if (!this.ctx.canvas3d) return;
-                if (current.repr !== prevRepr || !Loci.areEqual(current.loci, prevLoci)) {
-                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.Deselect, prevRepr);
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Select, current.repr);
-                    prevLoci = current.loci;
-                    prevRepr = current.repr;
+                if (!Representation.Loci.areEqual(prev, current)) {
+                    this.ctx.canvas3d.mark(prev, MarkerAction.Deselect);
+                    this.ctx.canvas3d.mark(current, MarkerAction.Select);
+                    prev = current;
                 } else {
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Toggle);
+                    this.ctx.canvas3d.mark(current, MarkerAction.Toggle);
                 }
                 // this.ctx.canvas3d.mark(loci, MarkerAction.Toggle);
             });

+ 1 - 1
src/mol-plugin/behavior/static/misc.ts

@@ -14,6 +14,6 @@ export function registerDefault(ctx: PluginContext) {
 export function Canvas3DSetSettings(ctx: PluginContext) {
     PluginCommands.Canvas3D.SetSettings.subscribe(ctx, e => {
         ctx.canvas3d.setProps(e.settings);
-        ctx.events.canvad3d.settingsUpdated.next();
+        ctx.events.canvas3d.settingsUpdated.next();
     })
 }

+ 3 - 3
src/mol-plugin/behavior/static/state.ts

@@ -107,16 +107,16 @@ export function Highlight(ctx: PluginContext) {
         const cell = state.select(ref)[0]
         const repr = cell && SO.isRepresentation3D(cell.obj) ? cell.obj.data : undefined
         if (cell && cell.obj && cell.obj.type === PluginStateObject.Molecule.Structure.type) {
-            ctx.behaviors.canvas.highlightLoci.next({ loci: Structure.Loci(cell.obj.data) })
+            ctx.events.canvas3d.highlight.next({ loci: { loci: Structure.Loci(cell.obj.data) } });
         } else if (repr) {
-            ctx.behaviors.canvas.highlightLoci.next({ loci: EveryLoci, repr })
+            ctx.events.canvas3d.highlight.next({ loci: { loci: EveryLoci, repr } });
         }
     });
 }
 
 export function ClearHighlight(ctx: PluginContext) {
     PluginCommands.State.ClearHighlight.subscribe(ctx, ({ state, ref }) => {
-        ctx.behaviors.canvas.highlightLoci.next({ loci: EmptyLoci })
+        ctx.events.canvas3d.highlight.next({ loci: { loci: EmptyLoci } });
     });
 }
 

+ 8 - 0
src/mol-plugin/command.ts

@@ -9,6 +9,7 @@ import { PluginCommand } from './command/base';
 import { StateTransform, State, StateAction } from 'mol-state';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { PluginLayoutStateProps } from './layout';
+import { StructureElement } from 'mol-model/structure';
 
 export * from './command/base';
 
@@ -38,6 +39,13 @@ export const PluginCommands = {
             OpenFile: PluginCommand<{ file: File }>({ isImmediate: true }),
         }
     },
+    Interactivity: {
+        Structure: {
+            AddHighlight: PluginCommand<{ loci: StructureElement.Loci, tryRange?: boolean }>({ isImmediate: true }),
+            ClearHighlight: PluginCommand<{ }>({ isImmediate: true }),
+            SelectHighlighted: PluginCommand<{ type: 'toggle' | 'add' }>({ isImmediate: true })
+        }
+    },
     Layout: {
         Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>({ isImmediate: true })
     },

+ 13 - 9
src/mol-plugin/context.ts

@@ -5,7 +5,6 @@
  */
 
 import { Canvas3D } from 'mol-canvas3d/canvas3d';
-import { EmptyLoci, Loci } from 'mol-model/loci';
 import { Representation } from 'mol-repr/representation';
 import { StructureRepresentationRegistry } from 'mol-repr/structure/registry';
 import { State, StateTransform, StateTransformer } from 'mol-state';
@@ -32,6 +31,7 @@ import { StateTransformParameters } from './ui/state/common';
 import { DataFormatRegistry } from './state/actions/data-format';
 import { PluginBehavior } from './behavior/behavior';
 import { CustomPropertyRegistry } from 'mol-model-props/common/custom-property-registry';
+import { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer';
 
 export class PluginContext {
     private disposed = false;
@@ -58,19 +58,23 @@ export class PluginContext {
         },
         log: this.ev<LogEntry>(),
         task: this.tasks.events,
-        labels: {
-            highlight: this.ev<{ entries: ReadonlyArray<LociLabelEntry> }>()
-        },
-        canvad3d: {
-            settingsUpdated: this.ev()
+        canvas3d: {
+            settingsUpdated: this.ev(),
+
+            highlight: this.ev<{ loci: Representation.Loci, modifiers?: ModifiersKeys }>(),
+            click: this.ev<{ loci: Representation.Loci, buttons?: ButtonsType, modifiers?: ModifiersKeys }>(),
         }
     };
 
     readonly behaviors = {
-        canvas: {
-            highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
-            selectLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
+        // canvas: {
+        //     highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any, modifiers?: ModifiersKeys }>({ loci: EmptyLoci }),
+        //     selectLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any, modifiers?: ModifiersKeys }>({ loci: EmptyLoci }),
+        // },
+        labels: {
+            highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] })
         },
+
         command: this.commands.behaviour
     };
 

+ 1 - 1
src/mol-plugin/ui/controls.tsx

@@ -82,7 +82,7 @@ export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyA
     state = { entries: [] }
 
     componentDidMount() {
-        this.subscribe(this.plugin.events.labels.highlight, e => this.setState({ entries: e.entries }));
+        this.subscribe(this.plugin.behaviors.labels.highlight, e => this.setState({ entries: e.entries }));
     }
 
     render() {

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

@@ -51,7 +51,7 @@ export class ViewportControls extends PluginUIComponent {
     }
 
     componentDidMount() {
-        this.subscribe(this.plugin.events.canvad3d.settingsUpdated, e => {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, e => {
             this.forceUpdate();
         });
 
@@ -114,18 +114,18 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
 
         const idHelper = new Canvas3dIdentifyHelper(this.plugin, 15);
 
-        this.subscribe(canvas3d.input.move, ({x, y, inside, buttons}) => {
+        this.subscribe(canvas3d.input.move, ({x, y, inside, buttons, modifiers }) => {
             if (!inside || buttons) { return; }
-            idHelper.move(x, y);
+            idHelper.move(x, y, modifiers);
         });
 
         this.subscribe(canvas3d.input.leave, () => {
             idHelper.leave();
         });
 
-        this.subscribe(canvas3d.input.click, ({x, y, buttons}) => {
+        this.subscribe(canvas3d.input.click, ({x, y, buttons, modifiers }) => {
             if (buttons !== ButtonsType.Flag.Primary) return;
-            idHelper.select(x, y);
+            idHelper.select(x, y, buttons, modifiers);
         });
 
         this.subscribe(this.plugin.layout.events.updated, () => {

+ 16 - 9
src/mol-plugin/util/canvas3d-identify.ts

@@ -8,6 +8,7 @@ import { PluginContext } from '../context';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from 'mol-model/loci';
 import { Representation } from 'mol-repr/representation';
+import { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer';
 
 export class Canvas3dIdentifyHelper {
     private cX = -1;
@@ -20,12 +21,15 @@ export class Canvas3dIdentifyHelper {
 
     private currentIdentifyT = 0;
 
-    private prevLoci: { loci: Loci, repr?: Representation.Any } = { loci: EmptyLoci };
+    private prevLoci: Representation.Loci = Representation.Loci.Empty;
     private prevT = 0;
 
     private inside = false;
 
-    private async identify(select: boolean, t: number) {
+    private buttons: ButtonsType = ButtonsType.create(0);
+    private modifiers: ModifiersKeys = ModifiersKeys.None;
+
+    private async identify(isClick: boolean, t: number) {
         if (this.lastX !== this.cX && this.lastY !== this.cY) {
             this.id = await this.ctx.canvas3d.identify(this.cX, this.cY);
             this.lastX = this.cX;
@@ -34,8 +38,8 @@ export class Canvas3dIdentifyHelper {
 
         if (!this.id) return;
 
-        if (select) {
-            this.ctx.behaviors.canvas.selectLoci.next(this.ctx.canvas3d.getLoci(this.id));
+        if (isClick) {
+            this.ctx.events.canvas3d.click.next({ loci: this.ctx.canvas3d.getLoci(this.id), buttons: this.buttons, modifiers: this.modifiers });
             return;
         }
 
@@ -46,7 +50,7 @@ export class Canvas3dIdentifyHelper {
 
         const loci = this.ctx.canvas3d.getLoci(this.id);
         if (loci.repr !== this.prevLoci.repr || !Loci.areEqual(loci.loci, this.prevLoci.loci)) {
-            this.ctx.behaviors.canvas.highlightLoci.next(loci);
+            this.ctx.events.canvas3d.highlight.next({ loci, modifiers: this.modifiers });
             this.prevLoci = loci;
         }
     }
@@ -63,21 +67,24 @@ export class Canvas3dIdentifyHelper {
     leave() {
         this.inside = false;
         if (this.prevLoci.loci !== EmptyLoci) {
-            this.prevLoci = { loci: EmptyLoci };
-            this.ctx.behaviors.canvas.highlightLoci.next(this.prevLoci);
+            this.prevLoci = Representation.Loci.Empty;
+            this.ctx.events.canvas3d.highlight.next({ loci: this.prevLoci });
             this.ctx.canvas3d.requestDraw(true);
         }
     }
 
-    move(x: number, y: number) {
+    move(x: number, y: number, modifiers: ModifiersKeys) {
         this.inside = true;
+        this.modifiers = modifiers;
         this.cX = x;
         this.cY = y;
     }
 
-    select(x: number, y: number) {
+    select(x: number, y: number, buttons: ButtonsType, modifiers: ModifiersKeys) {
         this.cX = x;
         this.cY = y;
+        this.buttons = buttons;
+        this.modifiers = modifiers;
         this.identify(true, 0);
     }
 

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

@@ -24,7 +24,7 @@ export class LociLabelManager {
     }
 
     private empty: any[] = [];
-    private getInfo(loci: Loci, repr?: Representation<any>) {
+    private getInfo({ loci, repr }: Representation.Loci) {
         if (!loci || loci.kind === 'empty-loci') return this.empty;
         const info: LociLabelEntry[] = [];
         for (let p of this.providers) {
@@ -35,6 +35,6 @@ export class LociLabelManager {
     }
 
     constructor(public ctx: PluginContext) {
-        ctx.behaviors.canvas.highlightLoci.subscribe(ev => ctx.events.labels.highlight.next({ entries: this.getInfo(ev.loci, ev.repr) }));
+        ctx.events.canvas3d.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.loci) }));
     }
 }

+ 115 - 0
src/mol-plugin/util/selection/structure-element.ts

@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureElement, Structure } from 'mol-model/structure';
+import { PluginContext } from '../../context';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { State } from 'mol-state';
+
+export { StructureElementSelectionManager }
+
+class StructureElementSelectionManager {
+    private entries = new Map<string, Entry>();
+
+    // maps structure to a parent StateObjectCell
+    private structures = {
+        root: new Map<Structure, string>(),
+        tracked: new Map<string, Structure>()
+    }
+
+    private getEntry(s: Structure) {
+        if (!this.structures.root.has(s)) return;
+        const key = this.structures.root.get(s)!;
+        if (!this.entries.has(key)) {
+            const entry: Entry = {
+                selection: StructureElement.Loci(s, []),
+                highlight: StructureElement.Loci(s, []),
+            };
+            this.entries.set(key, entry);
+            return entry;
+        }
+
+        return this.entries.get(key)!;
+    }
+
+    add(loci: StructureElement.Loci | Structure.Loci, type: 'selection' | 'highlight'): Loci {
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return EmptyLoci;
+        entry[type] = Structure.isLoci(loci) ? StructureElement.Loci.all(loci.structure) : StructureElement.Loci.union(entry[type], loci);
+        return entry[type];
+    }
+
+    remove(loci: StructureElement.Loci | Structure.Loci, type: 'selection' | 'highlight'): Loci {
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return EmptyLoci;
+        entry[type] = Structure.isLoci(loci) ? StructureElement.Loci(loci.structure, []) : StructureElement.Loci.subtract(entry[type], loci);
+        return entry[type].elements.length === 0 ? EmptyLoci : entry[type];
+    }
+
+    set(loci: StructureElement.Loci | Structure.Loci, type: 'selection' | 'highlight'): Loci {
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return EmptyLoci;
+        entry[type] = Structure.isLoci(loci) ? StructureElement.Loci.all(loci.structure) : loci;
+        return entry[type].elements.length === 0 ? EmptyLoci : entry[type];
+    }
+
+    private prevHighlight: StructureElement.Loci | undefined = void 0;
+
+    accumulateInteractiveHighlight(loci: StructureElement.Loci) {
+        if (this.prevHighlight) {
+            this.prevHighlight = StructureElement.Loci.union(this.prevHighlight, loci);
+        } else {
+            this.prevHighlight = loci;
+        }
+        return this.prevHighlight;
+    }
+
+    clearInteractiveHighlight() {
+        const ret = this.prevHighlight;
+        this.prevHighlight = void 0;
+        return ret || EmptyLoci;
+    }
+
+    private trackCell(state: State, ref: string) {
+        const cell = state.cells.get(ref);
+        if (!cell || !cell.obj || !PluginStateObject.Molecule.Structure.is(cell.obj)) return;
+        const parent = state.selectQ(q => q.byRef(cell.transform.ref).rootOfType([PluginStateObject.Molecule.Structure]))[0];
+        this.structures.tracked.set(cell.transform.ref, cell.obj.data);
+        if (!parent) {
+            this.structures.root.set(cell.obj.data, cell.transform.ref);
+        } else {
+            this.structures.root.set(cell.obj.data, parent.transform.ref);
+        }
+    }
+
+    private untrackCell(state: State, ref: string) {
+        if (!this.structures.tracked.has(ref)) return;
+        const s = this.structures.tracked.get(ref)!;
+        this.structures.tracked.delete(ref);
+        this.structures.root.delete(s);
+    }
+
+    constructor(plugin: PluginContext) {
+        plugin.state.dataState.events.object.created.subscribe(e => {
+            this.trackCell(e.state, e.ref);
+        });
+
+        plugin.state.dataState.events.object.removed.subscribe(e => {
+            this.untrackCell(e.state, e.ref);
+        });
+
+        plugin.state.dataState.events.object.updated.subscribe(e => {
+            this.untrackCell(e.state, e.ref);
+            this.trackCell(e.state, e.ref);
+        });
+    }
+}
+
+interface Entry {
+    selection: StructureElement.Loci,
+    highlight: StructureElement.Loci
+}

+ 15 - 5
src/mol-repr/representation.ts

@@ -7,7 +7,7 @@
 import { Task } from 'mol-task'
 import { GraphicsRenderObject } from 'mol-gl/render-object'
 import { PickingId } from '../mol-geo/geometry/picking';
-import { Loci, isEmptyLoci, EmptyLoci } from 'mol-model/loci';
+import { Loci as ModelLoci, isEmptyLoci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../mol-geo/geometry/marker-data';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { WebGLContext } from 'mol-gl/webgl/context';
@@ -102,11 +102,21 @@ interface Representation<D, P extends PD.Params = {}, S extends Representation.S
     createOrUpdate: (props?: Partial<PD.Values<P>>, data?: D) => Task<void>
     setState: (state: Partial<S>) => void
     setTheme: (theme: Theme) => void
-    getLoci: (pickingId: PickingId) => Loci
-    mark: (loci: Loci, action: MarkerAction) => boolean
+    getLoci: (pickingId: PickingId) => ModelLoci
+    mark: (loci: ModelLoci, action: MarkerAction) => boolean
     destroy: () => void
 }
 namespace Representation {
+    export interface Loci { loci: ModelLoci, 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 State {
         /** Controls if the representation's renderobjects are rendered or not */
         visible: boolean
@@ -218,7 +228,7 @@ namespace Representation {
                 }
                 return EmptyLoci
             },
-            mark: (loci: Loci, action: MarkerAction) => {
+            mark: (loci: ModelLoci, action: MarkerAction) => {
                 let marked = false
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     marked = reprList[i].mark(loci, action) || marked
@@ -275,7 +285,7 @@ namespace Representation {
                 // TODO
                 return EmptyLoci
             },
-            mark: (loci: Loci, action: MarkerAction) => {
+            mark: (loci: ModelLoci, action: MarkerAction) => {
                 // TODO
                 return false
             },

+ 1 - 1
src/mol-util/bit-flags.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-interface BitFlags<Flags> extends Number { '@type': Flags }
+type BitFlags<Flags> = number & Flags
 
 namespace BitFlags {
     export function create<F>(flags: F): BitFlags<F> { return flags as any; }

+ 15 - 8
src/mol-util/input/input-observer.ts

@@ -49,8 +49,11 @@ export type ModifiersKeys = {
     control: boolean,
     meta: boolean
 }
+export namespace ModifiersKeys {
+    export const None: ModifiersKeys = { shift: false, alt: false, control: false, meta: false }
+}
 
-export interface ButtonsType extends BitFlags<ButtonsType.Flag> { }
+export type ButtonsType = BitFlags<ButtonsType.Flag>
 
 export namespace ButtonsType {
     export const has: (ss: ButtonsType, f: Flag) => boolean = BitFlags.has
@@ -73,7 +76,7 @@ export namespace ButtonsType {
 }
 
 type BaseInput = {
-    buttons: number
+    buttons: ButtonsType
     modifiers: ModifiersKeys
 }
 
@@ -166,9 +169,13 @@ namespace InputObserver {
             meta: false
         }
 
+        function getModifiers(): ModifiersKeys {
+            return { ...modifiers };
+        }
+
         let dragging: DraggingState = DraggingState.Stopped
         let disposed = false
-        let buttons = 0
+        let buttons = 0 as ButtonsType
 
         const drag = new Subject<DragInput>()
         const interactionEnd = new Subject<undefined>();
@@ -260,7 +267,7 @@ namespace InputObserver {
 
         function handleBlur () {
             if (buttons || modifiers.shift || modifiers.alt || modifiers.meta || modifiers.control) {
-                buttons = 0
+                buttons = 0 as ButtonsType
                 modifiers.shift = modifiers.alt = modifiers.control = modifiers.meta = false
             }
         }
@@ -361,7 +368,7 @@ namespace InputObserver {
                 const { pageX, pageY } = ev
                 const [ x, y ] = pointerEnd
 
-                click.next({ x, y, pageX, pageY, buttons, modifiers })
+                click.next({ x, y, pageX, pageY, buttons, modifiers: getModifiers() })
             }
         }
 
@@ -370,7 +377,7 @@ namespace InputObserver {
             const { pageX, pageY } = ev
             const [ x, y ] = pointerEnd
             const inside = insideBounds(pointerEnd)
-            move.next({ x, y, pageX, pageY, buttons, modifiers, inside })
+            move.next({ x, y, pageX, pageY, buttons, modifiers: getModifiers(), inside })
 
             if (dragging === DraggingState.Stopped) return
 
@@ -378,7 +385,7 @@ namespace InputObserver {
 
             const isStart = dragging === DraggingState.Started
             const [ dx, dy ] = pointerDelta
-            drag.next({ x, y, dx, dy, pageX, pageY, buttons, modifiers, isStart })
+            drag.next({ x, y, dx, dy, pageX, pageY, buttons, modifiers: getModifiers(), isStart })
 
             Vec2.copy(pointerStart, pointerEnd)
             dragging = DraggingState.Moving
@@ -401,7 +408,7 @@ namespace InputObserver {
             const dz = (ev.deltaZ || 0) * scale
 
             if (dx || dy || dz) {
-                wheel.next({ dx, dy, dz, buttons, modifiers })
+                wheel.next({ dx, dy, dz, buttons, modifiers: getModifiers() })
             }
         }