From c5fff9d3c8624560682bb89882b27d27232da2a5 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Thu, 8 Nov 2018 19:04:35 +0100
Subject: [PATCH] mol-plugin: highlight and selection on canvas helper

---
 src/mol-geo/geometry/picking.ts               |   6 +
 src/mol-model/loci.ts                         |   2 +-
 .../behavior/built-in/representation.ts       | 103 ++++++++++++------
 src/mol-plugin/context.ts                     |   8 ++
 src/mol-plugin/ui/viewport.tsx                |  43 +++-----
 src/mol-plugin/util/canvas3d-identify.ts      |  87 +++++++++++++++
 src/mol-util/input/input-observer.ts          |  20 ++++
 7 files changed, 208 insertions(+), 61 deletions(-)
 create mode 100644 src/mol-plugin/util/canvas3d-identify.ts

diff --git a/src/mol-geo/geometry/picking.ts b/src/mol-geo/geometry/picking.ts
index ac145f4b1..a42ae76ee 100644
--- a/src/mol-geo/geometry/picking.ts
+++ b/src/mol-geo/geometry/picking.ts
@@ -21,6 +21,12 @@ export interface PickingId {
     groupId: number
 }
 
+export namespace PickingId {
+    export function areSame(a: PickingId, b: PickingId) {
+        return a.objectId === b.objectId && a.instanceId === b.instanceId && a.groupId === b.groupId;
+    }
+}
+
 export interface PickingInfo {
     label: string
     data?: any
diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts
index 8e0b1c715..054e7ea69 100644
--- a/src/mol-model/loci.ts
+++ b/src/mol-model/loci.ts
@@ -37,4 +37,4 @@ export function areLociEqual(lociA: Loci, lociB: Loci) {
     return false
 }
 
-export type Loci =  StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci
\ No newline at end of file
+export type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/built-in/representation.ts b/src/mol-plugin/behavior/built-in/representation.ts
index 5246082ca..27c0d1434 100644
--- a/src/mol-plugin/behavior/built-in/representation.ts
+++ b/src/mol-plugin/behavior/built-in/representation.ts
@@ -6,43 +6,76 @@
 
 import { PluginBehavior } from '../behavior';
 import { PluginStateObject as SO } from '../../state/base';
-
-class _AddRepresentationToCanvas extends PluginBehavior.Handler {
-    register(): void {
-        this.subscribeObservable(this.ctx.events.state.data.object.created, o => {
-            if (!SO.isRepresentation3D(o.obj)) return;
-            this.ctx.canvas3d.add(o.obj.data);
-            this.ctx.canvas3d.requestDraw(true);
-        });
-        this.subscribeObservable(this.ctx.events.state.data.object.updated, o => {
-            const oo = o.obj;
-            if (!SO.isRepresentation3D(oo)) return;
-            this.ctx.canvas3d.add(oo.data);
-            this.ctx.canvas3d.requestDraw(true);
-        });
-        this.subscribeObservable(this.ctx.events.state.data.object.removed, o => {
-            const oo = o.obj;
-            if (!SO.isRepresentation3D(oo)) return;
-            this.ctx.canvas3d.remove(oo.data);
-            this.ctx.canvas3d.requestDraw(true);
-            oo.data.destroy();
-        });
-        this.subscribeObservable(this.ctx.events.state.data.object.replaced, o => {
-            if (o.oldObj && SO.isRepresentation3D(o.oldObj)) {
-                this.ctx.canvas3d.remove(o.oldObj.data);
-                this.ctx.canvas3d.requestDraw(true);
-                o.oldObj.data.destroy();
-            }
-            if (o.newObj && SO.isRepresentation3D(o.newObj)) {
-                this.ctx.canvas3d.add(o.newObj.data);
-                this.ctx.canvas3d.requestDraw(true);
-            }
-        });
-    }
-}
+import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci';
+import { MarkerAction } from 'mol-geo/geometry/marker-data';
 
 export const AddRepresentationToCanvas = PluginBehavior.create({
     name: 'add-representation-to-canvas',
-    ctor: _AddRepresentationToCanvas,
+    ctor: class extends PluginBehavior.Handler {
+        register(): void {
+            this.subscribeObservable(this.ctx.events.state.data.object.created, o => {
+                if (!SO.isRepresentation3D(o.obj)) return;
+                this.ctx.canvas3d.add(o.obj.data);
+                this.ctx.canvas3d.requestDraw(true);
+            });
+            this.subscribeObservable(this.ctx.events.state.data.object.updated, o => {
+                const oo = o.obj;
+                if (!SO.isRepresentation3D(oo)) return;
+                this.ctx.canvas3d.add(oo.data);
+                this.ctx.canvas3d.requestDraw(true);
+            });
+            this.subscribeObservable(this.ctx.events.state.data.object.removed, o => {
+                const oo = o.obj;
+                if (!SO.isRepresentation3D(oo)) return;
+                this.ctx.canvas3d.remove(oo.data);
+                this.ctx.canvas3d.requestDraw(true);
+                oo.data.destroy();
+            });
+            this.subscribeObservable(this.ctx.events.state.data.object.replaced, o => {
+                if (o.oldObj && SO.isRepresentation3D(o.oldObj)) {
+                    this.ctx.canvas3d.remove(o.oldObj.data);
+                    this.ctx.canvas3d.requestDraw(true);
+                    o.oldObj.data.destroy();
+                }
+                if (o.newObj && SO.isRepresentation3D(o.newObj)) {
+                    this.ctx.canvas3d.add(o.newObj.data);
+                    this.ctx.canvas3d.requestDraw(true);
+                }
+            });
+        }
+    },
     display: { name: 'Add Representation To Canvas' }
+});
+
+export const HighlightLoci = PluginBehavior.create({
+    name: 'representation-highlight-loci',
+    ctor: class extends PluginBehavior.Handler {
+        register(): void {
+            let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
+            this.subscribeObservable(this.ctx.behaviors.canvas.highlightLoci, current => {
+                if (!this.ctx.canvas3d) return;
+
+                if (current.repr !== prevRepr || !areLociEqual(current.loci, prevLoci)) {
+                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight);
+                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight);
+                    prevLoci = current.loci;
+                    prevRepr = current.repr;
+                }
+            });
+        }
+    },
+    display: { name: 'Highlight Loci on Canvas' }
+});
+
+export const SelectLoci = PluginBehavior.create({
+    name: 'representation-select-loci',
+    ctor: class extends PluginBehavior.Handler {
+        register(): void {
+            this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, ({ loci }) => {
+                if (!this.ctx.canvas3d) return;
+                this.ctx.canvas3d.mark(loci, MarkerAction.Toggle);
+            });
+        }
+    },
+    display: { name: 'Select Loci on Canvas' }
 });
\ No newline at end of file
diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index d32cb9bf9..50718e3e7 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -16,6 +16,8 @@ import { PluginCommand, PluginCommands } from './command';
 import { Task } from 'mol-task';
 import { merge } from 'rxjs';
 import { PluginBehaviors } from './behavior';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { Representation } from 'mol-repr';
 
 export class PluginContext {
     private disposed = false;
@@ -36,6 +38,10 @@ export class PluginContext {
             data: this.state.data.context.behaviors,
             behavior: this.state.behavior.context.behaviors
         },
+        canvas: {
+            highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
+            selectLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
+        },
         command: this.commands.behaviour
     };
 
@@ -82,6 +88,8 @@ export class PluginContext {
             .and().toRoot().apply(PluginBehaviors.Data.Update, { ref: PluginBehaviors.Data.Update.id })
             .and().toRoot().apply(PluginBehaviors.Data.RemoveObject, { ref: PluginBehaviors.Data.RemoveObject.id })
             .and().toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas, { ref: PluginBehaviors.Representation.AddRepresentationToCanvas.id })
+            .and().toRoot().apply(PluginBehaviors.Representation.HighlightLoci, { ref: PluginBehaviors.Representation.HighlightLoci.id })
+            .and().toRoot().apply(PluginBehaviors.Representation.SelectLoci, { ref: PluginBehaviors.Representation.SelectLoci.id })
             .getTree();
 
         await this.state.updateBehaviour(tree);
diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx
index a7ace28f1..8bd0e59cc 100644
--- a/src/mol-plugin/ui/viewport.tsx
+++ b/src/mol-plugin/ui/viewport.tsx
@@ -7,9 +7,10 @@
 
 import * as React from 'react';
 import { PluginContext } from '../context';
-import { Loci, EmptyLoci, areLociEqual } from 'mol-model/loci';
-import { MarkerAction } from 'mol-geo/geometry/marker-data';
+// import { Loci, EmptyLoci, areLociEqual } from 'mol-model/loci';
+// import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { ButtonsType } from 'mol-util/input/input-observer';
+import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify';
 
 interface ViewportProps {
     plugin: PluginContext
@@ -40,29 +41,21 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> {
         const canvas3d = this.props.plugin.canvas3d;
         canvas3d.input.resize.subscribe(() => this.handleResize());
 
-        let prevLoci: Loci = EmptyLoci;
-        canvas3d.input.move.subscribe(async ({x, y, inside, buttons}) => {
-            if (!inside || buttons) return;
-            const p = await canvas3d.identify(x, y);
-            if (p) {
-                const { loci } = canvas3d.getLoci(p);
-
-                if (!areLociEqual(loci, prevLoci)) {
-                    canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight);
-                    canvas3d.mark(loci, MarkerAction.Highlight);
-                    prevLoci = loci;
-                }
-            }
-        })
-
-        canvas3d.input.click.subscribe(async ({x, y, buttons}) => {
-            if (buttons !== ButtonsType.Flag.Primary) return
-            const p = await canvas3d.identify(x, y)
-            if (p) {
-                const { loci } = canvas3d.getLoci(p)
-                canvas3d.mark(loci, MarkerAction.Toggle)
-            }
-        })
+        const idHelper = new Canvas3dIdentifyHelper(this.props.plugin, 15);
+
+        canvas3d.input.move.subscribe(({x, y, inside, buttons}) => {
+            if (!inside || buttons) { return; }
+            idHelper.move(x, y);
+        });
+
+        canvas3d.input.leave.subscribe(() => {
+            idHelper.leave();
+        });
+
+        canvas3d.input.click.subscribe(({x, y, buttons}) => {
+            if (buttons !== ButtonsType.Flag.Primary) return;
+            idHelper.select(x, y);
+        });
     }
 
     componentWillUnmount() {
diff --git a/src/mol-plugin/util/canvas3d-identify.ts b/src/mol-plugin/util/canvas3d-identify.ts
new file mode 100644
index 000000000..e5b561304
--- /dev/null
+++ b/src/mol-plugin/util/canvas3d-identify.ts
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginContext } from '../context';
+import { PickingId } from 'mol-geo/geometry/picking';
+import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci';
+import { Representation } from 'mol-repr';
+
+export class Canvas3dIdentifyHelper {
+    private cX = -1;
+    private cY = -1;
+
+    private lastX = -1;
+    private lastY = -1;
+
+    private id: PickingId | undefined = void 0;
+
+    private currentIdentifyT = 0;
+
+    private prevLoci: { loci: Loci, repr?: Representation.Any } = { loci: EmptyLoci };
+    private prevT = 0;
+
+    private inside = false;
+
+    private async identify(select: 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;
+            this.lastY = this.cY;
+        }
+
+        if (!this.id) return;
+
+        if (select) {
+            this.ctx.behaviors.canvas.selectLoci.next(this.ctx.canvas3d.getLoci(this.id));
+            return;
+        }
+
+        // only highlight the latest
+        if (!this.inside || this.currentIdentifyT !== t) {
+            return;
+        }
+
+        const loci = this.ctx.canvas3d.getLoci(this.id);
+        if (loci.repr !== this.prevLoci.repr || !areLociEqual(loci.loci, this.prevLoci.loci)) {
+            this.ctx.behaviors.canvas.highlightLoci.next(loci);
+            this.prevLoci = loci;
+        }
+    }
+
+    private animate: (t: number) => void = t => {
+        if (this.inside && t - this.prevT > 1000 / this.maxFps) {
+            this.prevT = t;
+            this.currentIdentifyT = t;
+            this.identify(false, t);
+        }
+        requestAnimationFrame(this.animate);
+    }
+
+    leave() {
+        this.inside = false;
+        if (this.prevLoci.loci !== EmptyLoci) {
+            this.prevLoci = { loci: EmptyLoci };
+            this.ctx.behaviors.canvas.highlightLoci.next(this.prevLoci);
+            this.ctx.canvas3d.requestDraw(true);
+        }
+    }
+
+    move(x: number, y: number) {
+        this.inside = true;
+        this.cX = x;
+        this.cY = y;
+    }
+
+    select(x: number, y: number) {
+        this.cX = x;
+        this.cY = y;
+        this.identify(true, 0);
+    }
+
+    constructor(private ctx: PluginContext, private maxFps: number = 15) {
+        this.animate(0);
+    }
+}
\ No newline at end of file
diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts
index 08e475e6d..67bd8a14c 100644
--- a/src/mol-util/input/input-observer.ts
+++ b/src/mol-util/input/input-observer.ts
@@ -141,6 +141,8 @@ interface InputObserver {
     pinch: Subject<PinchInput>,
     click: Subject<ClickInput>,
     move: Subject<MoveInput>,
+    leave: Subject<undefined>,
+    enter: Subject<undefined>,
     resize: Subject<ResizeInput>,
 
     dispose: () => void
@@ -175,6 +177,8 @@ namespace InputObserver {
         const wheel = new Subject<WheelInput>()
         const pinch = new Subject<PinchInput>()
         const resize = new Subject<ResizeInput>()
+        const leave = new Subject<undefined>()
+        const enter = new Subject<undefined>()
 
         attach()
 
@@ -189,6 +193,8 @@ namespace InputObserver {
             pinch,
             click,
             move,
+            leave,
+            enter,
             resize,
 
             dispose
@@ -204,6 +210,9 @@ namespace InputObserver {
             window.addEventListener('mousemove', onMouseMove as any, false)
             window.addEventListener('mouseup', onMouseUp as any, false)
 
+            element.addEventListener('mouseenter', onMouseEnter as any, false)
+            element.addEventListener('mouseleave', onMouseLeave as any, false)
+
             element.addEventListener('touchstart', onTouchStart as any, false)
             element.addEventListener('touchmove', onTouchMove as any, false)
             element.addEventListener('touchend', onTouchEnd as any, false)
@@ -227,6 +236,9 @@ namespace InputObserver {
             window.removeEventListener('mousemove', onMouseMove as any, false)
             window.removeEventListener('mouseup', onMouseUp as any, false)
 
+            element.removeEventListener('mouseenter', onMouseEnter as any, false)
+            element.removeEventListener('mouseleave', onMouseLeave as any, false)
+
             element.removeEventListener('touchstart', onTouchStart as any, false)
             element.removeEventListener('touchmove', onTouchMove as any, false)
             element.removeEventListener('touchend', onTouchEnd as any, false)
@@ -386,6 +398,14 @@ namespace InputObserver {
             }
         }
 
+        function onMouseEnter (ev: Event) {
+            enter.next();
+        }
+
+        function onMouseLeave (ev: Event) {
+            leave.next();
+        }
+
         function onResize (ev: Event) {
             resize.next()
         }
-- 
GitLab