diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 5169f3f294581527fd08c255efce39de6923e58b..91c82d47bede7eed671fbcdfac53e63c7924b477 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/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) diff --git a/src/mol-model/structure/model/properties/utils/secondary-structure.ts b/src/mol-model/structure/model/properties/utils/secondary-structure.ts index 672d113a4bb468bddae55b0d2bf5305f83bbefb1..afa8199475f42a616da01e9c8ec056a416c0fd53 100644 --- a/src/mol-model/structure/model/properties/utils/secondary-structure.ts +++ b/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 diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts index 2031a00c06ce8c830154c9224ea42aa362593f79..0c09b6a56125a8a9a1e126d4b375c35aa75b0d91 100644 --- a/src/mol-model/structure/model/types.ts +++ b/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 { diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 24594303dae766cec31f905f2952992ca6a4159b..e9287c3386900cae4efbcc4a6d31f229b86b9314 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/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 \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts index b2caee3fefa673d61b9bcf5eb6cacc1c485bd4d5..556e3996393954b22f7bb3fdfcaaa2bccd50a8ee 100644 --- a/src/mol-plugin/behavior/dynamic/camera.ts +++ b/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)); }); diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index e5566019b12186bbe2de08e2ca3f34dd49b28a93..33a0962a4466acc7091e939bdf21e0f27787a37f 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/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); }); diff --git a/src/mol-plugin/behavior/static/misc.ts b/src/mol-plugin/behavior/static/misc.ts index aff031d5bd5b855955024898797cec2cb409d626..8a400f1ca42dcdb5866d6fee6e9e48a37e9d46c9 100644 --- a/src/mol-plugin/behavior/static/misc.ts +++ b/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(); }) } diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 6ff152876d98c634ba8b8c76e1a453bdcca379f4..7142dcd551c9bff133bb5186f7c44771d5456b75 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/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 } }); }); } diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index c188f4cb3c8a2f005ec3576f76ad0e4c240c250e..ddbd7604f74f35b60f93577ca01b2672083c9973 100644 --- a/src/mol-plugin/command.ts +++ b/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 }) }, diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 5a5632db23542727f958f6fc06d79196ef736176..33eecbc061b32351f5e8faa0b7ae8e14e00085f3 100644 --- a/src/mol-plugin/context.ts +++ b/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 }; diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 813fa0b5829462eb778563f5d63d1f0c9ef84c64..4b54f9d4a9305827841c62c6510c2e6801ea56d0 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/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() { diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index ce917a04dbbaa269c33d1c9115bdec8f818f5baf..734684305d1d65a727c8814ccef41b3d87fbf54e 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/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, () => { diff --git a/src/mol-plugin/util/canvas3d-identify.ts b/src/mol-plugin/util/canvas3d-identify.ts index 0a54889f02385057cb7fdc581b5e7f39e48bfdeb..a9fd5e13ce6b313edeb3d642a2e811d9b0bda1ab 100644 --- a/src/mol-plugin/util/canvas3d-identify.ts +++ b/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); } diff --git a/src/mol-plugin/util/loci-label-manager.ts b/src/mol-plugin/util/loci-label-manager.ts index a747b640894b9229b2549e2e1b6cfb9985285ca1..5b9f7b3ade567828903b627226f8338c1f38380c 100644 --- a/src/mol-plugin/util/loci-label-manager.ts +++ b/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) })); } } \ No newline at end of file diff --git a/src/mol-plugin/util/selection/structure-element.ts b/src/mol-plugin/util/selection/structure-element.ts new file mode 100644 index 0000000000000000000000000000000000000000..8fda7de0dc6f5bf0d64dbbc379da4effdf38f0be --- /dev/null +++ b/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 +} diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts index 8900e9ee121c7e06df5b36397f81d5c1cb3dc38d..c20127bdbbba6851e5b666289f003171a92d4b30 100644 --- a/src/mol-repr/representation.ts +++ b/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 }, diff --git a/src/mol-util/bit-flags.ts b/src/mol-util/bit-flags.ts index 7f0bb8870771381b93be9e199f14e8bd52a4394e..2a76338f92d25dc047fee6abcfe769c07b5756f5 100644 --- a/src/mol-util/bit-flags.ts +++ b/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; } diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts index cbfbd8dfd935be39069e1b0bebdff1c65ebdabbd..3bb32b42940648dde5d898b91bc063f24f8ba7a5 100644 --- a/src/mol-util/input/input-observer.ts +++ b/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() }) } }