Skip to content
Snippets Groups Projects
Commit cfcf9f68 authored by Alexander Rose's avatar Alexander Rose
Browse files

wip, plugin interactivity

parent bcb8419f
No related branches found
No related tags found
No related merge requests found
/**
* 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);
......
......@@ -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)
......
/**
* 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();
})
}
......@@ -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 }>()
......
......@@ -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`);
......
......@@ -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 };
......
......@@ -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>
......
......@@ -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));
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment