diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 506e725afd79931b1151313d4ac486d9c7b92a93..9a0eaee3b780e90947eb3133b40ddc4001c81cc3 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/src/mol-model/structure/structure/element.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { UniqueArray } from '../../../mol-data/generic'; @@ -224,6 +225,44 @@ namespace StructureElement { return Loci(loci.structure, elements); } + function getChainSegments(unit: Unit) { + switch (unit.kind) { + case Unit.Kind.Atomic: return unit.model.atomicHierarchy.chainAtomSegments + case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.chainElementSegments + case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.chainElementSegments + } + } + + export function extendToWholeChains(loci: Loci): Loci { + const elements: Loci['elements'][0][] = []; + + for (const lociElement of loci.elements) { + const newIndices: UnitIndex[] = []; + const unitElements = lociElement.unit.elements; + + const { index: chainIndex, offsets: chainOffsets } = getChainSegments(lociElement.unit) + + const indices = lociElement.indices, len = OrderedSet.size(indices); + let i = 0; + while (i < len) { + const cI = chainIndex[unitElements[OrderedSet.getAt(indices, i)]]; + i++; + while (i < len && chainIndex[unitElements[OrderedSet.getAt(indices, i)]] === cI) { + i++; + } + + for (let j = chainOffsets[cI], _j = chainOffsets[cI + 1]; j < _j; j++) { + const idx = OrderedSet.indexOf(unitElements, j); + if (idx >= 0) newIndices[newIndices.length] = idx as UnitIndex; + } + } + + elements[elements.length] = { unit: lociElement.unit, indices: SortedArray.ofSortedArray(newIndices) }; + } + + return Loci(loci.structure, elements); + } + const boundaryHelper = new BoundaryHelper(), tempPos = Vec3.zero(); export function getBoundary(loci: Loci): Boundary { boundaryHelper.reset(0); diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index 20058f9b953c68bd6da4292a3a583aa31393bec1..edf502302b420df87cfa7d6da20958e362ce7f04 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -10,22 +10,22 @@ import { PluginContext } from '../../../mol-plugin/context'; import { PluginStateObject as SO } from '../../state/objects'; import { labelFirst } from '../../../mol-theme/label'; import { PluginBehavior } from '../behavior'; -import { Interaction } from '../../util/interaction'; +import { Interactivity } from '../../util/interactivity'; import { StateTreeSpine } from '../../../mol-state/tree/spine'; export const HighlightLoci = PluginBehavior.create({ name: 'representation-highlight-loci', category: 'interaction', ctor: class extends PluginBehavior.Handler { - private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => { + private lociMarkProvider = (interactionLoci: Interactivity.Loci, action: MarkerAction) => { if (!this.ctx.canvas3d) return; - this.ctx.canvas3d.mark({ ...loci, repr: undefined }, action) + this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action) } register() { - this.ctx.lociHighlights.addProvider(this.lociMarkProvider) + this.ctx.interactivity.lociHighlights.addProvider(this.lociMarkProvider) } unregister() { - this.ctx.lociHighlights.removeProvider(this.lociMarkProvider) + this.ctx.interactivity.lociHighlights.removeProvider(this.lociMarkProvider) } }, display: { name: 'Highlight Loci on Canvas' } @@ -36,14 +36,14 @@ export const SelectLoci = PluginBehavior.create({ category: 'interaction', ctor: class extends PluginBehavior.Handler { private spine: StateTreeSpine.Impl - private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => { + private lociMarkProvider = (interactionLoci: Interactivity.Loci, action: MarkerAction) => { if (!this.ctx.canvas3d) return; - this.ctx.canvas3d.mark({ ...loci, repr: undefined }, action) + this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action) } register() { - this.ctx.lociSelections.addProvider(this.lociMarkProvider) + this.ctx.interactivity.lociSelections.addProvider(this.lociMarkProvider) - this.subscribeObservable(this.ctx.events.state.object.created, ({ ref, state }) => { + this.subscribeObservable(this.ctx.events.state.object.created, ({ ref }) => { const cell = this.ctx.state.dataState.cells.get(ref) if (cell && SO.isRepresentation3D(cell.obj)) { this.spine.current = cell @@ -56,7 +56,7 @@ export const SelectLoci = PluginBehavior.create({ }); } unregister() { - this.ctx.lociSelections.removeProvider(this.lociMarkProvider) + this.ctx.interactivity.lociSelections.removeProvider(this.lociMarkProvider) } constructor(ctx: PluginContext, params: {}) { super(ctx, params) diff --git a/src/mol-plugin/behavior/static/misc.ts b/src/mol-plugin/behavior/static/misc.ts index 5807476ddb72d3a2e5ab0e0ee35500a6c5d5f8e1..92e61436b7f55e23bf8157f8c46f9603aa05a96f 100644 --- a/src/mol-plugin/behavior/static/misc.ts +++ b/src/mol-plugin/behavior/static/misc.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { PluginContext } from '../../../mol-plugin/context'; @@ -9,6 +10,7 @@ import { PluginCommands } from '../../../mol-plugin/command'; export function registerDefault(ctx: PluginContext) { Canvas3DSetSettings(ctx); + InteractivitySetProps(ctx); } export function Canvas3DSetSettings(ctx: PluginContext) { @@ -17,3 +19,10 @@ export function Canvas3DSetSettings(ctx: PluginContext) { ctx.events.canvas3d.settingsUpdated.next(); }) } + +export function InteractivitySetProps(ctx: PluginContext) { + PluginCommands.Interactivity.SetProps.subscribe(ctx, e => { + ctx.interactivity.setProps(e.props); + ctx.events.interactivity.propsUpdated.next(); + }) +} diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index ce0c6a3192a05c55d5889a68ad8f20fb1823b55a..f29c1a5d7e9d152c96f4d54387203fa870837fae 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -11,6 +11,7 @@ import { Canvas3DProps } from '../mol-canvas3d/canvas3d'; import { PluginLayoutStateProps } from './layout'; import { StructureElement } from '../mol-model/structure'; import { PluginState } from './state'; +import { Interactivity } from './util/interactivity'; export * from './command/base'; @@ -43,6 +44,7 @@ export const PluginCommands = { } }, Interactivity: { + SetProps: PluginCommand<{ props: Partial<Interactivity.Props> }>(), Structure: { Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>(), Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>() diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 20c0e810c8f328abb220e30ab97111460ca55656..a11fe2ba1adba5317818bd0d5ce6cbd697151443 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -36,7 +36,7 @@ import { SubstructureParentHelper } from './util/substructure-parent-helper'; import { ModifiersKeys } from '../mol-util/input/input-observer'; import { isProductionMode, isDebugMode } from '../mol-util/debug'; import { Model, Structure } from '../mol-model/structure'; -import { Interaction } from './util/interaction'; +import { Interactivity } from './util/interactivity'; interface Log { entries: List<LogEntry> @@ -74,6 +74,9 @@ export class PluginContext { task: this.tasks.events, canvas3d: { settingsUpdated: this.ev() + }, + interactivity: { + propsUpdated: this.ev() } } as const @@ -83,8 +86,8 @@ export class PluginContext { isUpdating: this.ev.behavior<boolean>(false) }, interaction: { - highlight: this.ev.behavior<Interaction.HighlightEvent>({ current: Interaction.Loci.Empty }), - click: this.ev.behavior<Interaction.ClickEvent>({ current: Interaction.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 }) + highlight: this.ev.behavior<Interactivity.HighlightEvent>({ current: Interactivity.Loci.Empty }), + click: this.ev.behavior<Interactivity.ClickEvent>({ current: Interactivity.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 }) }, labels: { highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] }) @@ -93,10 +96,9 @@ export class PluginContext { readonly canvas3d: Canvas3D; readonly layout: PluginLayout = new PluginLayout(this); + readonly interactivity: Interactivity; readonly lociLabels: LociLabelManager; - readonly lociSelections: Interaction.LociSelectionManager; - readonly lociHighlights: Interaction.LociHighlightManager; readonly structureRepresentation = { registry: new StructureRepresentationRegistry(), @@ -238,9 +240,8 @@ export class PluginContext { this.initAnimations(); this.initCustomParamEditors(); + this.interactivity = new Interactivity(this); this.lociLabels = new LociLabelManager(this); - this.lociSelections = new Interaction.LociSelectionManager(this); - this.lociHighlights = new Interaction.LociHighlightManager(this); this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`); if (!isProductionMode) this.log.message(`Development mode enabled`); diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx index 239d5e316e921c3953bbfe48240951a4f20febfa..073bbccfef3d9509119c48b5d30a95c7d191ed8f 100644 --- a/src/mol-plugin/ui/sequence.tsx +++ b/src/mol-plugin/ui/sequence.tsx @@ -10,7 +10,7 @@ import { Structure, StructureSequence, Queries, StructureSelection, StructurePro import { PluginUIComponent } from './base'; import { StateTreeSpine } from '../../mol-state/tree/spine'; import { PluginStateObject as SO } from '../state/objects'; -import { Interaction } from '../util/interaction'; +import { Interactivity } from '../util/interactivity'; import { OrderedSet, Interval } from '../../mol-data/int'; import { Loci } from '../../mol-model/loci'; import { applyMarkerAction, MarkerAction } from '../../mol-util/marker-action'; @@ -156,13 +156,13 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen markerData: ValueBox.create(new Uint8Array(this.props.markerArray)) } - private lociHighlightProvider = (loci: Interaction.Loci, action: MarkerAction) => { + private lociHighlightProvider = (loci: Interactivity.Loci, action: MarkerAction) => { const { markerData } = this.state; const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action) if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) }) } - private lociSelectionProvider = (loci: Interaction.Loci, action: MarkerAction) => { + private lociSelectionProvider = (loci: Interactivity.Loci, action: MarkerAction) => { const { markerData } = this.state; const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action) if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) }) @@ -176,13 +176,13 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen } componentDidMount() { - this.plugin.lociHighlights.addProvider(this.lociHighlightProvider) - this.plugin.lociSelections.addProvider(this.lociSelectionProvider) + this.plugin.interactivity.lociHighlights.addProvider(this.lociHighlightProvider) + this.plugin.interactivity.lociSelections.addProvider(this.lociSelectionProvider) } componentWillUnmount() { - this.plugin.lociHighlights.removeProvider(this.lociHighlightProvider) - this.plugin.lociSelections.removeProvider(this.lociSelectionProvider) + this.plugin.interactivity.lociHighlights.removeProvider(this.lociHighlightProvider) + this.plugin.interactivity.lociSelections.removeProvider(this.lociSelectionProvider) } getLoci(seqId: number) { @@ -192,7 +192,7 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen } highlight(seqId?: number, modifiers?: ModifiersKeys) { - const ev = { current: Interaction.Loci.Empty, modifiers } + const ev = { current: Interactivity.Loci.Empty, modifiers } if (seqId !== undefined) { const loci = this.getLoci(seqId); if (loci.elements.length > 0) ev.current = { loci }; @@ -201,7 +201,7 @@ class EntitySequence extends PluginUIComponent<EntitySequenceProps, EntitySequen } click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) { - const ev = { current: Interaction.Loci.Empty, buttons, modifiers } + const ev = { current: Interactivity.Loci.Empty, buttons, modifiers } if (seqId !== undefined) { const loci = this.getLoci(seqId); if (loci.elements.length > 0) ev.current = { loci }; diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 12c97fdfbb8ea8abd201715333b23d32c9b53e74..539496b0d3e08d86863224c06c22e4d57ce185ce 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -14,6 +14,7 @@ import { Canvas3DParams } from '../../mol-canvas3d/canvas3d'; import { PluginLayoutStateParams } from '../../mol-plugin/layout'; import { ControlGroup, IconButton } from './controls/common'; import { resizeCanvas } from '../../mol-canvas3d/util'; +import { Interactivity } from '../util/interactivity'; interface ViewportState { noWebGl: boolean @@ -49,14 +50,14 @@ export class ViewportControls extends PluginUIComponent<{}, { isSettingsExpanded PluginCommands.Layout.Update.dispatch(this.plugin, { state: { [p.name]: p.value } }); } - componentDidMount() { - this.subscribe(this.plugin.events.canvas3d.settingsUpdated, e => { - this.forceUpdate(); - }); + setInteractivityProps = (p: { param: PD.Base<any>, name: string, value: any }) => { + PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { [p.name]: p.value } }); + } - this.subscribe(this.plugin.layout.events.updated, () => { - this.forceUpdate(); - }); + componentDidMount() { + this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate()); + this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate()); + this.subscribe(this.plugin.events.interactivity.propsUpdated, () => this.forceUpdate()); } icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) { @@ -75,6 +76,9 @@ export class ViewportControls extends PluginUIComponent<{}, { isSettingsExpanded <ControlGroup header='Layout' initialExpanded={true}> <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} /> </ControlGroup> + <ControlGroup header='Interactivity' initialExpanded={true}> + <ParameterControls params={Interactivity.Params} values={this.plugin.interactivity.props} onChange={this.setInteractivityProps} /> + </ControlGroup> <ControlGroup header='Viewport' initialExpanded={true}> <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} /> </ControlGroup> diff --git a/src/mol-plugin/util/interaction.ts b/src/mol-plugin/util/interactivity.ts similarity index 55% rename from src/mol-plugin/util/interaction.ts rename to src/mol-plugin/util/interactivity.ts index e97670cbd468d25596868ae385587f7bd44bd561..a334354816865b80f5b40c9832d383b4d128feb1 100644 --- a/src/mol-plugin/util/interaction.ts +++ b/src/mol-plugin/util/interactivity.ts @@ -12,8 +12,37 @@ import { StructureElement } from '../../mol-model/structure'; import { MarkerAction } from '../../mol-util/marker-action'; import { StructureElementSelectionManager } from './structure-element-selection'; import { PluginContext } from '../context'; +import { StructureElement as SE, Structure } from '../../mol-model/structure'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { PluginCommands } from '../command'; +import { capitalize } from '../../mol-util/string'; -export namespace Interaction { +export { Interactivity } + +class Interactivity { + readonly lociSelections: Interactivity.LociSelectionManager; + readonly lociHighlights: Interactivity.LociHighlightManager; + + private _props = PD.getDefaultValues(Interactivity.Params) + + get props() { return { ...this._props } } + setProps(props: Partial<Interactivity.Props>) { + Object.assign(this._props, props) + this.lociSelections.setProps(this._props) + this.lociHighlights.setProps(this._props) + } + + constructor(readonly ctx: PluginContext, props: Partial<Interactivity.Props> = {}) { + Object.assign(this._props, props) + + this.lociSelections = new Interactivity.LociSelectionManager(ctx, this._props); + this.lociHighlights = new Interactivity.LociHighlightManager(ctx, this._props); + + PluginCommands.Interactivity.SetProps.subscribe(ctx, e => this.setProps(e.props)); + } +} + +namespace Interactivity { export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any } export namespace Loci { @@ -23,6 +52,20 @@ export namespace Interaction { export const Empty: Loci = { loci: EmptyLoci }; } + const LociExpansion = { + 'none': (loci: ModelLoci) => loci, + 'residue': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeResidues(loci) : loci, + 'chain': (loci: ModelLoci) => SE.isLoci(loci) ? SE.Loci.extendToWholeChains(loci) : loci, + 'structure': (loci: ModelLoci) => SE.isLoci(loci) ? Structure.Loci(loci.structure) : loci + } + type LociExpansion = keyof typeof LociExpansion + const LociExpansionOptions = Object.keys(LociExpansion).map(n => [n, capitalize(n)]) as [LociExpansion, string][] + + export const Params = { + lociExpansion: PD.Select('residue', LociExpansionOptions), + } + export type Props = PD.Values<typeof Params> + export interface HighlightEvent { current: Loci, modifiers?: ModifiersKeys } export interface ClickEvent { current: Loci, buttons: ButtonsType, modifiers: ModifiersKeys } @@ -32,6 +75,12 @@ export namespace Interaction { protected providers: LociMarkProvider[] = []; protected sel: StructureElementSelectionManager + readonly props: Readonly<Props> = PD.getDefaultValues(Params) + + setProps(props: Partial<Props>) { + Object.assign(this.props, props) + } + addProvider(provider: LociMarkProvider) { this.providers.push(provider); } @@ -41,14 +90,8 @@ export namespace Interaction { // TODO clear, then re-apply remaining providers } - toggleSel(current: Loci<ModelLoci>) { - if (this.sel.has(current.loci)) { - this.sel.remove(current.loci); - this.mark(current, MarkerAction.Deselect); - } else { - this.sel.add(current.loci); - this.mark(current, MarkerAction.Select); - } + expandLoci(loci: ModelLoci) { + return LociExpansion[this.props.lociExpansion](loci) } protected mark(current: Loci<ModelLoci>, action: MarkerAction) { @@ -57,8 +100,9 @@ export namespace Interaction { abstract apply(e: MarkEvent): void - constructor(public ctx: PluginContext) { + constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) { this.sel = ctx.helpers.structureSelection + this.setProps(props) } } @@ -67,27 +111,28 @@ export namespace Interaction { apply(e: HighlightEvent) { const { current, modifiers } = e - if (StructureElement.isLoci(current.loci)) { - let loci: StructureElement.Loci = current.loci; + const expanded: Loci<ModelLoci> = { loci: this.expandLoci(current.loci), repr: current.repr } + if (StructureElement.isLoci(expanded.loci)) { + let loci: StructureElement.Loci = expanded.loci; if (modifiers && modifiers.shift) { loci = this.sel.tryGetRange(loci) || loci; } this.mark(this.prev, MarkerAction.RemoveHighlight); - const toHighlight = { loci, repr: current.repr }; + const toHighlight = { loci, repr: expanded.repr }; this.mark(toHighlight, MarkerAction.Highlight); this.prev = toHighlight; } else { - if (!Loci.areEqual(this.prev, current)) { + if (!Loci.areEqual(this.prev, expanded)) { this.mark(this.prev, MarkerAction.RemoveHighlight); - this.mark(current, MarkerAction.Highlight); - this.prev = current; + this.mark(expanded, MarkerAction.Highlight); + this.prev = expanded; } } } - constructor(public ctx: PluginContext) { - super(ctx) + constructor(ctx: PluginContext, props: Partial<Props> = {}) { + super(ctx, props) ctx.behaviors.interaction.highlight.subscribe(e => this.apply(e)); } } @@ -105,38 +150,39 @@ export namespace Interaction { apply(e: ClickEvent) { const { current, buttons, modifiers } = e - if (current.loci.kind === 'empty-loci') { + const expanded: Loci<ModelLoci> = { loci: this.expandLoci(current.loci), repr: current.repr } + if (expanded.loci.kind === 'empty-loci') { if (modifiers.control && buttons === ButtonsType.Flag.Secondary) { // clear the selection on Ctrl + Right-Click on empty const sels = this.sel.clear(); for (const s of sels) this.mark({ loci: s }, MarkerAction.Deselect); } - } else if (StructureElement.isLoci(current.loci)) { + } else if (StructureElement.isLoci(expanded.loci)) { if (modifiers.control && buttons === ButtonsType.Flag.Secondary) { // select only the current element on Ctrl + Right-Click - const old = this.sel.get(current.loci.structure); + const old = this.sel.get(expanded.loci.structure); this.mark({ loci: old }, MarkerAction.Deselect); - this.sel.set(current.loci); - this.mark(current, MarkerAction.Select); + this.sel.set(expanded.loci); + this.mark(expanded, MarkerAction.Select); } else if (modifiers.control && buttons === ButtonsType.Flag.Primary) { // toggle current element on Ctrl + Left-Click - this.toggleSel(current as Representation.Loci<StructureElement.Loci>); + this.toggleSel(expanded 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; + let loci: StructureElement.Loci = expanded.loci; if (modifiers && modifiers.shift) { loci = this.sel.tryGetRange(loci) || loci; } - this.toggleSel({ loci, repr: current.repr }); + this.toggleSel({ loci, repr: expanded.repr }); } } else { if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return; - for (let p of this.providers) p(current, MarkerAction.Toggle); + for (let p of this.providers) p(expanded, MarkerAction.Toggle); } } - constructor(public ctx: PluginContext) { - super(ctx) + constructor(ctx: PluginContext, props: Partial<Props> = {}) { + super(ctx, props) ctx.behaviors.interaction.click.subscribe(e => this.apply(e)); } }