From eafb6fe1501b55823cc20b0c1d72462ef045a89b Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Thu, 28 Feb 2019 14:54:46 +0100 Subject: [PATCH] basic structure selection model, fixed bug in OrderedSet --- src/mol-canvas3d/canvas3d.ts | 26 +++- src/mol-canvas3d/helper/interaction-events.ts | 125 ++++++++++++++++++ src/mol-data/int/_spec/ordered-set.spec.ts | 3 + src/mol-data/int/impl/ordered-set.ts | 2 +- src/mol-geo/geometry/marker-data.ts | 10 +- src/mol-model/structure/structure/element.ts | 20 ++- src/mol-plugin/behavior/dynamic/camera.ts | 8 +- .../behavior/dynamic/representation.ts | 64 +++++++-- src/mol-plugin/behavior/static/state.ts | 20 ++- src/mol-plugin/command.ts | 5 +- src/mol-plugin/context.ts | 31 +++-- src/mol-plugin/ui/viewport.tsx | 20 +-- src/mol-plugin/util/canvas3d-identify.ts | 94 ------------- src/mol-plugin/util/loci-label-manager.ts | 2 +- ...loci.ts => structure-element-selection.ts} | 83 ++++++++---- src/mol-repr/representation.ts | 2 +- src/mol-util/input/input-observer.ts | 68 ++++++---- 17 files changed, 369 insertions(+), 214 deletions(-) create mode 100644 src/mol-canvas3d/helper/interaction-events.ts delete mode 100644 src/mol-plugin/util/canvas3d-identify.ts rename src/mol-plugin/util/{selection/structure-loci.ts => structure-element-selection.ts} (61%) diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 91c82d47b..58bf1db6a 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -8,7 +8,7 @@ import { BehaviorSubject, Subscription } from 'rxjs'; import { now } from 'mol-util/now'; import { Vec3 } from 'mol-math/linear-algebra' -import InputObserver from 'mol-util/input/input-observer' +import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer' import Renderer, { RendererStats } from 'mol-gl/renderer' import { GraphicsRenderObject } from 'mol-gl/render-object' @@ -29,6 +29,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper'; import { decodeFloatRGB } from 'mol-util/float-packing'; import { SetUtils } from 'mol-util/set'; +import { Canvas3dInteractionHelper } from './helper/interaction-events'; export const Canvas3DParams = { // TODO: FPS cap? @@ -76,10 +77,18 @@ interface Canvas3D { readonly props: Canvas3DProps readonly input: InputObserver readonly stats: RendererStats + readonly interaction: Canvas3dInteractionHelper['events'] + + // TODO: is this a good solution? + setSceneAnimating(animating: boolean): void + dispose: () => void } namespace Canvas3D { + export interface HighlightEvent { current: Representation.Loci, prev: Representation.Loci, modifiers?: ModifiersKeys } + export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys } + export function create(canvas: HTMLCanvasElement, container: Element, props: Partial<Canvas3DProps> = {}): Canvas3D { const p = { ...PD.getDefaultValues(Canvas3DParams), ...props } @@ -125,7 +134,10 @@ namespace Canvas3D { let isUpdating = false let drawPending = false - const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug) + const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug); + const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input); + + let isSceneAnimating = false function getLoci(pickingId: PickingId) { let loci: Loci = EmptyLoci @@ -250,7 +262,8 @@ namespace Canvas3D { function animate() { currentTime = now(); camera.transition.tick(currentTime); - draw(false) + draw(false); + if (!camera.transition.inTransition && !isSceneAnimating) interactionHelper.tick(currentTime); window.requestAnimationFrame(animate) } @@ -419,6 +432,12 @@ namespace Canvas3D { get stats() { return renderer.stats }, + get interaction() { + return interactionHelper.events + }, + setSceneAnimating(animating) { + isSceneAnimating = animating; + }, dispose: () => { scene.clear() debugHelper.clear() @@ -426,6 +445,7 @@ namespace Canvas3D { controls.dispose() renderer.dispose() camera.dispose() + interactionHelper.dispose() } } diff --git a/src/mol-canvas3d/helper/interaction-events.ts b/src/mol-canvas3d/helper/interaction-events.ts new file mode 100644 index 000000000..4267ecf0a --- /dev/null +++ b/src/mol-canvas3d/helper/interaction-events.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PickingId } from 'mol-geo/geometry/picking'; +import { EmptyLoci } from 'mol-model/loci'; +import { Representation } from 'mol-repr/representation'; +import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; + +type Canvas3D = import('../canvas3d').Canvas3D + +export class Canvas3dInteractionHelper { + private ev = RxEventHelper.create(); + + readonly events = { + highlight: this.ev<import('../canvas3d').Canvas3D.HighlightEvent>(), + click: this.ev<import('../canvas3d').Canvas3D.ClickEvent>(), + }; + + private cX = -1; + private cY = -1; + + private lastX = -1; + private lastY = -1; + + private id: PickingId | undefined = void 0; + + private currentIdentifyT = 0; + + private prevLoci: Representation.Loci = Representation.Loci.Empty; + private prevT = 0; + + private inside = false; + + 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.canvasIdentify(this.cX, this.cY); + this.lastX = this.cX; + this.lastY = this.cY; + } + + if (!this.id) return; + + if (isClick) { + this.events.click.next({ current: this.getLoci(this.id), buttons: this.buttons, modifiers: this.modifiers }); + return; + } + + // only highlight the latest + if (!this.inside || this.currentIdentifyT !== t) { + return; + } + + const loci = this.getLoci(this.id); + if (!Representation.Loci.areEqual(this.prevLoci, loci)) { + this.events.highlight.next({ current: loci, prev: this.prevLoci, modifiers: this.modifiers }); + this.prevLoci = loci; + } + } + + tick(t: number) { + if (this.inside && t - this.prevT > 1000 / this.maxFps) { + this.prevT = t; + this.currentIdentifyT = t; + this.identify(false, t); + } + } + + leave() { + this.inside = false; + if (this.prevLoci.loci !== EmptyLoci) { + const prev = this.prevLoci; + this.prevLoci = Representation.Loci.Empty; + this.events.highlight.next({ current: this.prevLoci, prev }); + } + } + + move(x: number, y: number, modifiers: ModifiersKeys) { + this.inside = true; + this.modifiers = modifiers; + this.cX = x; + this.cY = y; + } + + 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); + } + + modify(modifiers: ModifiersKeys) { + if (this.prevLoci.loci === EmptyLoci || ModifiersKeys.areEqual(modifiers, this.modifiers)) return; + this.modifiers = modifiers; + this.events.highlight.next({ current: this.prevLoci, prev: this.prevLoci, modifiers: this.modifiers }); + } + + dispose() { + this.ev.dispose(); + } + + constructor(private canvasIdentify: Canvas3D['identify'], private getLoci: Canvas3D['getLoci'], input: InputObserver, private maxFps: number = 15) { + input.move.subscribe(({x, y, inside, buttons, modifiers }) => { + if (!inside || buttons) { return; } + this.move(x, y, modifiers); + }); + + input.leave.subscribe(() => { + this.leave(); + }); + + input.click.subscribe(({x, y, buttons, modifiers }) => { + this.select(x, y, buttons, modifiers); + }); + + input.modifiers.subscribe(modifiers => this.modify(modifiers)); + } +} \ No newline at end of file diff --git a/src/mol-data/int/_spec/ordered-set.spec.ts b/src/mol-data/int/_spec/ordered-set.spec.ts index 401bff07e..5f717b2dd 100644 --- a/src/mol-data/int/_spec/ordered-set.spec.ts +++ b/src/mol-data/int/_spec/ordered-set.spec.ts @@ -140,6 +140,8 @@ describe('ordered set', () => { testEq('union AA3', OrderedSet.union(OrderedSet.ofSortedArray([1, 3]), OrderedSet.ofSortedArray([2, 4])), [1, 2, 3, 4]); testEq('union AA4', OrderedSet.union(OrderedSet.ofSortedArray([1, 3]), OrderedSet.ofSortedArray([1, 3, 4])), [1, 3, 4]); testEq('union AA5', OrderedSet.union(OrderedSet.ofSortedArray([1, 3, 4]), OrderedSet.ofSortedArray([1, 3])), [1, 3, 4]); + testEq('union AR', OrderedSet.union(OrderedSet.ofSortedArray([1, 2, 5, 6]), OrderedSet.ofRange(3, 4)), [1, 2, 3, 4, 5, 6]); + testEq('union AR1', OrderedSet.union(OrderedSet.ofSortedArray([1, 2, 6, 7]), OrderedSet.ofRange(3, 4)), [1, 2, 3, 4, 6, 7]); it('union AA6', () => expect(OrderedSet.union(arr136, OrderedSet.ofSortedArray([1, 3, 6]))).toBe(arr136)); testEq('intersect ES', OrderedSet.intersect(empty, singleton10), []); @@ -164,6 +166,7 @@ describe('ordered set', () => { testEq('subtract SR2', OrderedSet.subtract(range1_4, OrderedSet.ofSingleton(3)), [1, 2, 4]); testEq('subtract RR', OrderedSet.subtract(range1_4, range1_4), []); testEq('subtract RR1', OrderedSet.subtract(range1_4, OrderedSet.ofRange(3, 5)), [1, 2]); + testEq('subtract RR2', OrderedSet.subtract(range1_4, OrderedSet.ofRange(2, 3)), [1, 4]); testEq('subtract RA', OrderedSet.subtract(range1_4, arr136), [2, 4]); testEq('subtract RA1', OrderedSet.subtract(range1_4, OrderedSet.ofSortedArray([0, 1, 2, 3, 4, 7])), []); diff --git a/src/mol-data/int/impl/ordered-set.ts b/src/mol-data/int/impl/ordered-set.ts index 33794046c..832a0c281 100644 --- a/src/mol-data/int/impl/ordered-set.ts +++ b/src/mol-data/int/impl/ordered-set.ts @@ -169,7 +169,7 @@ function unionSI(a: S, b: I) { let offset = 0; for (let i = 0; i < start; i++) indices[offset++] = a[i]; for (let i = min; i <= max; i++) indices[offset++] = i; - for (let i = end, _i = a.length; i < _i; i++) indices[offset] = a[i]; + for (let i = end, _i = a.length; i < _i; i++) indices[offset++] = a[i]; return ofSortedArray(indices); } diff --git a/src/mol-geo/geometry/marker-data.ts b/src/mol-geo/geometry/marker-data.ts index 3fd6dc64d..22733a6ff 100644 --- a/src/mol-geo/geometry/marker-data.ts +++ b/src/mol-geo/geometry/marker-data.ts @@ -38,12 +38,14 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number, } break case MarkerAction.Select: - v += 2 + if (v < 2) v += 2 + // v += 2 break case MarkerAction.Deselect: - if (v >= 2) { - v -= 2 - } + // if (v >= 2) { + // v -= 2 + // } + v = v % 2 break case MarkerAction.Toggle: if (v >= 2) { diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 1e4c7ae2e..8b39d2215 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/src/mol-model/structure/structure/element.ts @@ -125,7 +125,6 @@ namespace StructureElement { } 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; @@ -146,8 +145,6 @@ namespace StructureElement { } 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); @@ -162,7 +159,22 @@ namespace StructureElement { } } - return xs; + return Loci(xs.structure, elements); + } + + export function areIntersecting(xs: Loci, ys: Loci): boolean { + if (xs.elements.length > ys.elements.length) return areIntersecting(ys, xs); + if (xs.elements.length === 0) return ys.elements.length === 0; + + const map = new Map<number, OrderedSet<UnitIndex>>(); + + for (const e of xs.elements) map.set(e.unit.id, e.indices); + for (const e of ys.elements) { + if (!map.has(e.unit.id)) continue; + if (OrderedSet.areIntersecting(map.get(e.unit.id)!, e.indices)) return true; + } + + return false; } } } diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts index 556e39963..34199a4fd 100644 --- a/src/mol-plugin/behavior/dynamic/camera.ts +++ b/src/mol-plugin/behavior/dynamic/camera.ts @@ -7,15 +7,17 @@ import { Loci } from 'mol-model/loci'; import { ParamDefinition } from 'mol-util/param-definition'; import { PluginBehavior } from '../behavior'; +import { ButtonsType, ModifiersKeys } from 'mol-util/input/input-observer'; export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number }>({ name: 'focus-loci-on-select', category: 'interaction', ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> { register(): void { - this.subscribeObservable(this.ctx.events.canvas3d.click, current => { - if (!this.ctx.canvas3d) return; - const sphere = Loci.getBoundingSphere(current.loci.loci); + this.subscribeObservable(this.ctx.events.canvas3d.click, ({ current, buttons, modifiers }) => { + if (!this.ctx.canvas3d || buttons !== ButtonsType.Flag.Primary || !ModifiersKeys.areEqual(modifiers, ModifiersKeys.None)) return; + + const sphere = Loci.getBoundingSphere(current.loci); if (!sphere) return; this.ctx.canvas3d.camera.focus(sphere.center, Math.max(sphere.radius + this.params.extraRadius, this.params.minRadius)); }); diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index 33a0962a4..bb1987dbf 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -15,6 +15,8 @@ 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'; +import { ButtonsType } from 'mol-util/input/input-observer'; +import { StructureElement } from 'mol-model/structure'; export const HighlightLoci = PluginBehavior.create({ name: 'representation-highlight-loci', @@ -22,15 +24,29 @@ export const HighlightLoci = PluginBehavior.create({ ctor: class extends PluginBehavior.Handler { register(): void { let prev: Representation.Loci = { loci: EmptyLoci, repr: void 0 }; + const sel = this.ctx.selection.structure; - this.subscribeObservable(this.ctx.events.canvas3d.highlight, ({ loci }) => { + this.subscribeObservable(this.ctx.events.canvas3d.highlight, ({ current, modifiers }) => { if (!this.ctx.canvas3d) return; - if (!Representation.Loci.areEqual(prev, loci)) { + 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); - this.ctx.canvas3d.mark(loci, MarkerAction.Highlight); - prev = loci; + 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; + } } + }); } }, @@ -42,17 +58,43 @@ export const SelectLoci = PluginBehavior.create({ category: 'interaction', ctor: class extends PluginBehavior.Handler { register(): void { - let prev = Representation.Loci.Empty; - this.subscribeObservable(this.ctx.events.canvas3d.click, ({ loci: current }) => { - if (!this.ctx.canvas3d) return; - if (!Representation.Loci.areEqual(prev, current)) { - this.ctx.canvas3d.mark(prev, MarkerAction.Deselect); + const sel = this.ctx.selection.structure; + + 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); - prev = current; + } + } + + this.subscribeObservable(this.ctx.events.canvas3d.click, ({ current, buttons, modifiers }) => { + if (!this.ctx.canvas3d) return; + + 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); } - // this.ctx.canvas3d.mark(loci, MarkerAction.Toggle); }); } }, diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 7142dcd55..b934f76d6 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -8,9 +8,7 @@ import { PluginCommands } from '../../command'; import { PluginContext } from '../../context'; import { StateTree, StateTransform, State } from 'mol-state'; import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots'; -import { PluginStateObject as SO, PluginStateObject } from '../../state/objects'; -import { EmptyLoci, EveryLoci } from 'mol-model/loci'; -import { Structure } from 'mol-model/structure'; +import { PluginStateObject as SO } from '../../state/objects'; import { getFormattedTime } from 'mol-util/date'; import { readFromFile } from 'mol-util/data-source'; @@ -104,19 +102,19 @@ function setVisibilityVisitor(t: StateTransform, tree: StateTree, ctx: { state: // TODO should also work for volumes and shapes export function Highlight(ctx: PluginContext) { PluginCommands.State.Highlight.subscribe(ctx, ({ state, ref }) => { - 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.events.canvas3d.highlight.next({ loci: { loci: Structure.Loci(cell.obj.data) } }); - } else if (repr) { - ctx.events.canvas3d.highlight.next({ loci: { loci: EveryLoci, repr } }); - } + // 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.events.canvas3d.highlight.next({ current: { loci: Structure.Loci(cell.obj.data) } }); + // } else if (repr) { + // ctx.events.canvas3d.highlight.next({ current: { loci: EveryLoci, repr } }); + // } }); } export function ClearHighlight(ctx: PluginContext) { PluginCommands.State.ClearHighlight.subscribe(ctx, ({ state, ref }) => { - ctx.events.canvas3d.highlight.next({ loci: { loci: EmptyLoci } }); + // ctx.events.canvas3d.highlight.next({ current: { loci: EmptyLoci } }); }); } diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index ddbd7604f..f2867ffb0 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -41,9 +41,8 @@ export const PluginCommands = { }, Interactivity: { Structure: { - AddHighlight: PluginCommand<{ loci: StructureElement.Loci, tryRange?: boolean }>({ isImmediate: true }), - ClearHighlight: PluginCommand<{ }>({ isImmediate: true }), - SelectHighlighted: PluginCommand<{ type: 'toggle' | 'add' }>({ isImmediate: true }) + Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true }), + Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true }) } }, Layout: { diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 33eecbc06..63f8891d5 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -4,34 +4,33 @@ * @author David Sehnal <david.sehnal@gmail.com> */ +import { List } from 'immutable'; import { Canvas3D } from 'mol-canvas3d/canvas3d'; -import { Representation } from 'mol-repr/representation'; +import { CustomPropertyRegistry } from 'mol-model-props/common/custom-property-registry'; import { StructureRepresentationRegistry } from 'mol-repr/structure/registry'; +import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry'; import { State, StateTransform, StateTransformer } from 'mol-state'; import { Task } from 'mol-task'; import { ColorTheme } from 'mol-theme/color'; import { SizeTheme } from 'mol-theme/size'; import { ThemeRegistryContext } from 'mol-theme/theme'; +import { Color } from 'mol-util/color'; +import { ajaxGet } from 'mol-util/data-source'; import { LogEntry } from 'mol-util/log-entry'; import { RxEventHelper } from 'mol-util/rx-event-helper'; import { merge } from 'rxjs'; import { BuiltInPluginBehaviors } from './behavior'; +import { PluginBehavior } from './behavior/behavior'; import { PluginCommand, PluginCommands } from './command'; +import { PluginLayout } from './layout'; import { PluginSpec } from './spec'; import { PluginState } from './state'; -import { TaskManager } from './util/task-manager'; -import { Color } from 'mol-util/color'; +import { DataFormatRegistry } from './state/actions/data-format'; +import { StateTransformParameters } from './ui/state/common'; import { LociLabelEntry, LociLabelManager } from './util/loci-label-manager'; -import { ajaxGet } from 'mol-util/data-source'; -import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry'; +import { TaskManager } from './util/task-manager'; import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version'; -import { PluginLayout } from './layout'; -import { List } from 'immutable'; -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'; +import { StructureElementSelectionManager } from './util/structure-element-selection'; export class PluginContext { private disposed = false; @@ -61,8 +60,8 @@ export class PluginContext { canvas3d: { settingsUpdated: this.ev(), - highlight: this.ev<{ loci: Representation.Loci, modifiers?: ModifiersKeys }>(), - click: this.ev<{ loci: Representation.Loci, buttons?: ButtonsType, modifiers?: ModifiersKeys }>(), + highlight: this.ev<Canvas3D.HighlightEvent>(), + click: this.ev<Canvas3D.ClickEvent>() } }; @@ -100,6 +99,10 @@ export class PluginContext { readonly customModelProperties = new CustomPropertyRegistry(); readonly customParamEditors = new Map<string, StateTransformParameters.Class>(); + readonly selection = { + structure: new StructureElementSelectionManager(this) + }; + initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) { try { this.layout.setRoot(container); diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 734684305..408c174a3 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -6,8 +6,6 @@ */ import * as React from 'react'; -import { ButtonsType } from 'mol-util/input/input-observer'; -import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify'; import { PluginUIComponent } from './base'; import { PluginCommands } from 'mol-plugin/command'; import { ParamDefinition as PD } from 'mol-util/param-definition'; @@ -112,22 +110,8 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> { const canvas3d = this.plugin.canvas3d; this.subscribe(canvas3d.input.resize, this.handleResize); - const idHelper = new Canvas3dIdentifyHelper(this.plugin, 15); - - this.subscribe(canvas3d.input.move, ({x, y, inside, buttons, modifiers }) => { - if (!inside || buttons) { return; } - idHelper.move(x, y, modifiers); - }); - - this.subscribe(canvas3d.input.leave, () => { - idHelper.leave(); - }); - - this.subscribe(canvas3d.input.click, ({x, y, buttons, modifiers }) => { - if (buttons !== ButtonsType.Flag.Primary) return; - idHelper.select(x, y, buttons, modifiers); - }); - + this.subscribe(canvas3d.interaction.click, e => this.plugin.events.canvas3d.click.next(e)); + this.subscribe(canvas3d.interaction.highlight, e => this.plugin.events.canvas3d.highlight.next(e)); this.subscribe(this.plugin.layout.events.updated, () => { setTimeout(this.handleResize, 50); }); diff --git a/src/mol-plugin/util/canvas3d-identify.ts b/src/mol-plugin/util/canvas3d-identify.ts deleted file mode 100644 index c7e0e64ed..000000000 --- a/src/mol-plugin/util/canvas3d-identify.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * 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 } 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; - private cY = -1; - - private lastX = -1; - private lastY = -1; - - private id: PickingId | undefined = void 0; - - private currentIdentifyT = 0; - - private prevLoci: Representation.Loci = Representation.Loci.Empty; - private prevT = 0; - - private inside = false; - - 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; - this.lastY = this.cY; - } - - if (!this.id) return; - - if (isClick) { - this.ctx.events.canvas3d.click.next({ loci: this.ctx.canvas3d.getLoci(this.id), buttons: this.buttons, modifiers: this.modifiers }); - return; - } - - // only highlight the latest - if (!this.inside || this.currentIdentifyT !== t) { - return; - } - - const loci = this.ctx.canvas3d.getLoci(this.id); - if (!Representation.Loci.areEqual(this.prevLoci, loci)) { - this.ctx.events.canvas3d.highlight.next({ loci, modifiers: this.modifiers }); - this.prevLoci = loci; - } - } - - private animate: (t: number) => void = t => { - if (!this.ctx.state.animation.isAnimating && 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 = Representation.Loci.Empty; - this.ctx.events.canvas3d.highlight.next({ loci: this.prevLoci }); - this.ctx.canvas3d.requestDraw(true); - } - } - - move(x: number, y: number, modifiers: ModifiersKeys) { - this.inside = true; - this.modifiers = modifiers; - this.cX = x; - this.cY = y; - } - - 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); - } - - constructor(private ctx: PluginContext, private maxFps: number = 15) { - this.animate(0); - } -} \ No newline at end of file diff --git a/src/mol-plugin/util/loci-label-manager.ts b/src/mol-plugin/util/loci-label-manager.ts index 5b9f7b3ad..b49814798 100644 --- a/src/mol-plugin/util/loci-label-manager.ts +++ b/src/mol-plugin/util/loci-label-manager.ts @@ -35,6 +35,6 @@ export class LociLabelManager { } constructor(public ctx: PluginContext) { - ctx.events.canvas3d.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.loci) })); + ctx.events.canvas3d.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) })); } } \ No newline at end of file diff --git a/src/mol-plugin/util/selection/structure-loci.ts b/src/mol-plugin/util/structure-element-selection.ts similarity index 61% rename from src/mol-plugin/util/selection/structure-loci.ts rename to src/mol-plugin/util/structure-element-selection.ts index b7613c10b..b5f7d7b99 100644 --- a/src/mol-plugin/util/selection/structure-loci.ts +++ b/src/mol-plugin/util/structure-element-selection.ts @@ -4,16 +4,16 @@ * @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, StateSelection, StateObject } from 'mol-state'; -import { mapObjectMap } from 'mol-util/object'; +import { EmptyLoci, Loci } from 'mol-model/loci'; +import { Structure, StructureElement } from 'mol-model/structure'; +import { State, StateObject, StateSelection } from 'mol-state'; +import { PluginContext } from '../context'; +import { PluginStateObject } from '../state/objects'; +import { OrderedSet } from 'mol-data/int'; -export { StructureLociManager } +export { StructureElementSelectionManager }; -class StructureLociManager { +class StructureElementSelectionManager { private entries = new Map<string, SelectionEntry>(); // maps structure to a parent StateObjectCell @@ -34,28 +34,65 @@ class StructureLociManager { return this.entries.get(key)!; } - add(loci: StructureElement.Loci | Structure.Loci, type: 'selection' | 'highlight'): Loci { + add(loci: StructureElement.Loci): Loci { const entry = this.getEntry(loci.structure); if (!entry) return EmptyLoci; - const xs = entry.elements; - xs[type] = Structure.isLoci(loci) ? StructureElement.Loci.all(loci.structure) : StructureElement.Loci.union(xs[type], loci); - return xs[type]; + entry.selection = StructureElement.Loci.union(entry.selection, loci); + return entry.selection; } - remove(loci: StructureElement.Loci | Structure.Loci, type: 'selection' | 'highlight'): Loci { + remove(loci: StructureElement.Loci): Loci { const entry = this.getEntry(loci.structure); if (!entry) return EmptyLoci; - const xs = entry.elements; - xs[type] = Structure.isLoci(loci) ? StructureElement.Loci(loci.structure, []) : StructureElement.Loci.subtract(xs[type], loci); - return xs[type].elements.length === 0 ? EmptyLoci : xs[type]; + entry.selection = StructureElement.Loci.subtract(entry.selection, loci); + return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection; } - set(loci: StructureElement.Loci | Structure.Loci, type: 'selection' | 'highlight'): Loci { + set(loci: StructureElement.Loci): Loci { const entry = this.getEntry(loci.structure); if (!entry) return EmptyLoci; - const xs = entry.elements; - xs[type] = Structure.isLoci(loci) ? StructureElement.Loci.all(loci.structure) : loci; - return xs[type].elements.length === 0 ? EmptyLoci : xs[type]; + entry.selection = loci; + return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection; + } + + get(structure: Structure) { + const entry = this.getEntry(structure); + if (!entry) return EmptyLoci; + return entry.selection; + } + + has(loci: StructureElement.Loci) { + const entry = this.getEntry(loci.structure); + if (!entry) return false; + return StructureElement.Loci.areIntersecting(loci, entry.selection); + } + + tryGetRange(loci: StructureElement.Loci): StructureElement.Loci | undefined { + if (loci.elements.length !== 1) return; + const entry = this.getEntry(loci.structure); + if (!entry) return; + + let xs = loci.elements[0]; + let e: StructureElement.Loci['elements'][0] | undefined; + for (const _e of entry.selection.elements) { + if (xs.unit === _e.unit) { + e = _e; + break; + } + } + if (!e) return; + + const predIdx = OrderedSet.findPredecessorIndex(e.indices, OrderedSet.min(xs.indices)); + if (predIdx === 0) return; + + const fst = predIdx < OrderedSet.size(e.indices) + ? OrderedSet.getAt(e.indices, predIdx) + : OrderedSet.getAt(e.indices, predIdx - 1) + 1 as StructureElement.UnitIndex; + + return StructureElement.Loci(entry.selection.structure, [{ + unit: e.unit, + indices: OrderedSet.ofRange(fst, OrderedSet.max(xs.indices)) + }]); } private prevHighlight: StructureElement.Loci | undefined = void 0; @@ -131,17 +168,17 @@ class StructureLociManager { } interface SelectionEntry { - elements: { [category: string]: StructureElement.Loci } + selection: StructureElement.Loci } function SelectionEntry(s: Structure): SelectionEntry { return { - elements: { } + selection: StructureElement.Loci(s, []) }; } function remapSelectionEntry(e: SelectionEntry, s: Structure): SelectionEntry { return { - elements: mapObjectMap(e.elements, (l: StructureElement.Loci) => StructureElement.Loci.remap(l, s)) + selection: StructureElement.Loci.remap(e.selection, s) }; } \ No newline at end of file diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts index c20127bdb..b959ed7f1 100644 --- a/src/mol-repr/representation.ts +++ b/src/mol-repr/representation.ts @@ -107,7 +107,7 @@ interface Representation<D, P extends PD.Params = {}, S extends Representation.S destroy: () => void } namespace Representation { - export interface Loci { loci: ModelLoci, repr?: Representation.Any } + export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any } export namespace Loci { export function areEqual(a: Loci, b: Loci) { diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts index 3bb32b429..7a6160b03 100644 --- a/src/mol-util/input/input-observer.ts +++ b/src/mol-util/input/input-observer.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Subject } from 'rxjs'; +import { Subject, Observable } from 'rxjs'; import { Vec2 } from 'mol-math/linear-algebra'; @@ -50,13 +50,17 @@ export type ModifiersKeys = { meta: boolean } export namespace ModifiersKeys { - export const None: ModifiersKeys = { shift: false, alt: false, control: false, meta: false } + export const None: ModifiersKeys = { shift: false, alt: false, control: false, meta: false }; + + export function areEqual(a: ModifiersKeys, b: ModifiersKeys) { + return a.shift === b.shift && a.alt === b.alt && a.control === b.control && a.meta === b.meta; + } } export type ButtonsType = BitFlags<ButtonsType.Flag> export namespace ButtonsType { - export const has: (ss: ButtonsType, f: Flag) => boolean = BitFlags.has + export const has: (btn: ButtonsType, f: Flag) => boolean = BitFlags.has export const create: (fs: Flag) => ButtonsType = BitFlags.create export const enum Flag { @@ -138,16 +142,17 @@ interface InputObserver { noScroll: boolean noContextMenu: boolean - drag: Subject<DragInput>, + drag: Observable<DragInput>, // Equivalent to mouseUp and touchEnd - interactionEnd: Subject<undefined>, - wheel: Subject<WheelInput>, - pinch: Subject<PinchInput>, - click: Subject<ClickInput>, - move: Subject<MoveInput>, - leave: Subject<undefined>, - enter: Subject<undefined>, - resize: Subject<ResizeInput>, + interactionEnd: Observable<undefined>, + wheel: Observable<WheelInput>, + pinch: Observable<PinchInput>, + click: Observable<ClickInput>, + move: Observable<MoveInput>, + leave: Observable<undefined>, + enter: Observable<undefined>, + resize: Observable<ResizeInput>, + modifiers: Observable<ModifiersKeys> dispose: () => void } @@ -176,6 +181,7 @@ namespace InputObserver { let dragging: DraggingState = DraggingState.Stopped let disposed = false let buttons = 0 as ButtonsType + let isInside = false const drag = new Subject<DragInput>() const interactionEnd = new Subject<undefined>(); @@ -186,6 +192,7 @@ namespace InputObserver { const resize = new Subject<ResizeInput>() const leave = new Subject<undefined>() const enter = new Subject<undefined>() + const modifiersEvent = new Subject<ModifiersKeys>() attach() @@ -204,6 +211,7 @@ namespace InputObserver { leave, enter, resize, + modifiers: modifiersEvent, dispose } @@ -226,9 +234,8 @@ namespace InputObserver { element.addEventListener('touchend', onTouchEnd as any, false) element.addEventListener('blur', handleBlur) - element.addEventListener('keyup', handleMods as EventListener) - element.addEventListener('keydown', handleMods as EventListener) - element.addEventListener('keypress', handleMods as EventListener) + window.addEventListener('keyup', handleKeyUp as EventListener, false) + window.addEventListener('keydown', handleKeyDown as EventListener, false) window.addEventListener('resize', onResize, false) } @@ -252,9 +259,8 @@ namespace InputObserver { element.removeEventListener('touchend', onTouchEnd as any, false) element.removeEventListener('blur', handleBlur) - element.removeEventListener('keyup', handleMods as EventListener) - element.removeEventListener('keydown', handleMods as EventListener) - element.removeEventListener('keypress', handleMods as EventListener) + window.removeEventListener('keyup', handleKeyUp as EventListener, false) + window.removeEventListener('keydown', handleKeyDown as EventListener, false) window.removeEventListener('resize', onResize, false) } @@ -272,11 +278,25 @@ namespace InputObserver { } } - function handleMods (event: MouseEvent | KeyboardEvent) { - if ('altKey' in event) modifiers.alt = !!event.altKey - if ('shiftKey' in event) modifiers.shift = !!event.shiftKey - if ('ctrlKey' in event) modifiers.control = !!event.ctrlKey - if ('metaKey' in event) modifiers.meta = !!event.metaKey + function handleKeyDown (event: KeyboardEvent) { + let changed = false; + if (!modifiers.alt && event.altKey) { changed = true; modifiers.alt = true; } + if (!modifiers.shift && event.shiftKey) { changed = true; modifiers.shift = true; } + if (!modifiers.control && event.ctrlKey) { changed = true; modifiers.control = true; } + if (!modifiers.meta && event.metaKey) { changed = true; modifiers.meta = true; } + + if (changed && isInside) modifiersEvent.next(getModifiers()); + } + + function handleKeyUp (event: KeyboardEvent) { + let changed = false; + + if (modifiers.alt && !event.altKey) { changed = true; modifiers.alt = false; } + if (modifiers.shift && !event.shiftKey) { changed = true; modifiers.shift = false; } + if (modifiers.control && !event.ctrlKey) { changed = true; modifiers.control = false; } + if (modifiers.meta && !event.metaKey) { changed = true; modifiers.meta = false; } + + if (changed && isInside) modifiersEvent.next(getModifiers()); } function getCenterTouch (ev: TouchEvent): PointerEvent { @@ -413,10 +433,12 @@ namespace InputObserver { } function onMouseEnter (ev: Event) { + isInside = true; enter.next(); } function onMouseLeave (ev: Event) { + isInside = false; leave.next(); } -- GitLab