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