Compare commits

...

40 Commits

Author SHA1 Message Date
Alexander Rose
87028c0a0b 0.2.4 2019-06-21 17:21:59 -07:00
Alexander Rose
8ea23e6965 Merge branch 'master' into seq 2019-06-21 17:19:26 -07:00
Alexander Rose
f9d8942814 basic support for missing residues 2019-06-21 16:55:28 -07:00
Alexander Rose
b40df2f1e3 sequence, on state tree change improvments 2019-06-21 16:55:12 -07:00
Alexander Rose
884cb0d9a4 sequence widget refactoring 2019-06-21 12:54:22 -07:00
Alexander Rose
ef1ccd4286 cif schema updates 2019-06-21 12:44:28 -07:00
Alexander Rose
898abda373 sequence & interactivity tweaks 2019-06-21 11:06:49 -07:00
Alexander Rose
e42c664a8c fixes: StructureElement.Loci.union, Structure.parent 2019-06-21 10:56:06 -07:00
Alexander Rose
987bf47827 save interactivity props in state 2019-06-21 09:29:03 -07:00
Alexander Rose
6201dd1d74 improved xtal symmetry support for props & sequence 2019-06-21 09:11:38 -07:00
David Sehnal
5ed17ce4e5 proteopedia-wrapper: evolutionary coloring on current representation 2019-06-21 12:47:14 +02:00
Alexander Rose
e301eca9c2 wip, sequence view, select options 2019-06-20 15:50:31 -07:00
Alexander Rose
8a4ef015a2 added StructureElement.set 2019-06-20 15:42:45 -07:00
Alexander Rose
67f3f3fdbb improved AtomsQueryParams.unitTest and StructureProperties.unit 2019-06-20 15:42:23 -07:00
Alexander Rose
adc5b559cd various tweaks 2019-06-20 15:35:55 -07:00
Alexander Rose
bbaa637118 wip, sequence selector 2019-06-19 17:45:50 -07:00
Alexander Rose
a3094b4d19 wip, per-chain sequence widget 2019-06-19 17:03:20 -07:00
Alexander Rose
c3f937e113 sequence widget refactoring 2019-06-19 14:17:49 -07:00
Alexander Rose
04df327939 renamed lociExpansion to granularity 2019-06-19 12:30:19 -07:00
Alexander Rose
b1a0f46ade improved link loci handling for interactivity 2019-06-19 12:18:53 -07:00
Alexander Rose
389e249862 use PurePluginUIComponent for Residue 2019-06-19 11:48:36 -07:00
Alexander Rose
cfcf9f6818 wip, plugin interactivity 2019-06-18 17:28:57 -07:00
Alexander Rose
bcb8419f37 init repr3d and seq view marker with global selection 2019-06-18 14:31:22 -07:00
Alexander Rose
7d24bcf1dc tweaked resolution quality settings 2019-06-18 14:24:47 -07:00
Alexander Rose
8d0f7a2dc7 factored-out marker-action 2019-06-18 11:48:59 -07:00
Alexander Rose
a7cb7beaa8 Merge branch 'master' into seq 2019-06-18 09:04:33 -07:00
David Sehnal
3c9b82dc04 proteopedia-plugin: Load asym unit fix 2019-06-18 15:42:29 +02:00
David Sehnal
9dba6d5371 proteopedia-wrapper: tweak 2019-06-18 13:53:04 +02:00
David Sehnal
a65bba0969 proteopedia-wrapper: better HET group focusing 2019-06-18 13:39:23 +02:00
David Sehnal
175e009152 proteopedia-wrapper: fix assembly loading 2019-06-18 13:01:38 +02:00
Alexander Rose
f6b2c0b2ba wip, sequence view 2019-06-17 16:44:38 -07:00
Alexander Rose
ea419c68ae removed old unused ui code 2019-06-17 15:41:03 -07:00
Alexander Rose
40cf348d40 simple (entity) sequence view 2019-06-17 15:40:51 -07:00
Alexander Rose
93ea759a71 StateTreeSpine.current tweaks 2019-06-17 15:39:12 -07:00
Alexander Rose
3e50377eb8 use as const to quickly make class props readonly 2019-06-17 15:29:37 -07:00
Alexander Rose
3fbd1f8dc4 StructureElementSelectionManager.tryGetRange in both directions 2019-06-17 15:27:22 -07:00
Alexander Rose
f03ce68513 plugin interaction helpers refactoring 2019-06-17 14:37:24 -07:00
Alexander Rose
50e2d542df add getModifiers input helper 2019-06-17 14:33:32 -07:00
Alexander Rose
e53e739d18 ignore non StructureElement loci in StructureElementSelectionManager 2019-06-17 14:32:47 -07:00
Alexander Rose
dfb7f7811f support coarse elements in atomGroups generator 2019-06-14 18:59:37 -07:00
77 changed files with 1507 additions and 814 deletions

View File

@@ -85,7 +85,7 @@ and navigate to `build/viewer`
Install CIFTools `npm install ciftools -g`
cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/mmcif.ts -p mmCIF
cifschema -mip ../../../../mol-data-o src/mol-io/reader/cif/schema/ccd.ts -p CCD
cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/ccd.ts -p CCD
cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/bird.ts -p BIRD
**GraphQL schemas**

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "0.2.3",
"version": "0.2.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "0.2.3",
"version": "0.2.4",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {

View File

@@ -1,3 +1,8 @@
== v3.2 ==
* Fixed assembly loading.
* Better HET group focus.
== v3.0 ==
* Fixed initial camera zoom.

View File

@@ -115,5 +115,6 @@ export enum StateElements {
Water = 'water',
WaterVisual = 'water-visual',
HetGroupFocus = 'het-group-focus'
HetGroupFocus = 'het-group-focus',
HetGroupFocusGroup = 'het-group-focus-group'
}

View File

@@ -149,7 +149,8 @@
addSeparator();
addHeader('Misc');
addControl('Apply Evo Cons', () => PluginWrapper.coloring.evolutionaryConservation());
addControl('Apply Evo Cons Style', () => PluginWrapper.coloring.evolutionaryConservation());
addControl('Apply Evo Cons Colors', () => PluginWrapper.coloring.evolutionaryConservation({ sequence: true, het: false, keepStyle: true }));
addControl('Default Visuals', () => PluginWrapper.updateStyle());
addSeparator();

View File

@@ -36,7 +36,7 @@ require('../../mol-plugin/skin/light.scss')
class MolStarProteopediaWrapper {
static VERSION_MAJOR = 3;
static VERSION_MINOR = 1;
static VERSION_MINOR = 2;
private _ev = RxEventHelper.create();
@@ -81,7 +81,7 @@ class MolStarProteopediaWrapper {
return b.apply(StateTransforms.Data.Download, { url, isBinary: false })
}
private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) {
private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats) {
const parsed = format === 'cif'
? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
: b.apply(StateTransforms.Model.TrajectoryFromPDB);
@@ -190,7 +190,7 @@ class MolStarProteopediaWrapper {
}
private loadedParams: LoadParams = { url: '', format: 'cif', assemblyId: '' };
async load({ url, format = 'cif', assemblyId = '', representationStyle }: LoadParams) {
async load({ url, format = 'cif', assemblyId = 'deposited', representationStyle }: LoadParams) {
let loadType: 'full' | 'update' = 'full';
const state = this.plugin.state.dataState;
@@ -203,14 +203,17 @@ class MolStarProteopediaWrapper {
if (loadType === 'full') {
await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref });
const modelTree = this.model(this.download(state.build().toRoot(), url), format, assemblyId);
const modelTree = this.model(this.download(state.build().toRoot(), url), format);
await this.applyState(modelTree);
const info = await this.doInfo(true);
const structureTree = this.structure((assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId);
const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId;
const structureTree = this.structure(asmId);
await this.applyState(structureTree);
} else {
const tree = state.build();
tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
const info = await this.doInfo(true);
const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId;
tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: asmId }));
await this.applyState(tree);
}
@@ -250,18 +253,26 @@ class MolStarProteopediaWrapper {
}
coloring = {
evolutionaryConservation: async () => {
await this.updateStyle({ sequence: { kind: 'spacefill' } }, true);
evolutionaryConservation: async (params?: { sequence?: boolean, het?: boolean, keepStyle?: boolean }) => {
if (!params || !params.keepStyle) {
await this.updateStyle({ sequence: { kind: 'spacefill' } }, true);
}
const state = this.state;
// const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D));
// for (const v of visuals) {
// }
const tree = state.build();
const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues };
tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
// for (const v of visuals) {
// }
if (!params || !!params.sequence) {
tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
}
if (params && !!params.het) {
tree.to(StateElements.HetVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
}
await PluginCommands.State.Update.dispatch(this.plugin, { state, tree });
}
@@ -295,30 +306,30 @@ class MolStarProteopediaWrapper {
PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update });
PluginCommands.Camera.Reset.dispatch(this.plugin, { });
},
focusFirst: async (resn: string) => {
focusFirst: async (compId: string) => {
if (!this.state.transforms.has(StateElements.Assembly)) return;
await PluginCommands.Camera.Reset.dispatch(this.plugin, { });
// const asm = (this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure).data;
const update = this.state.build();
update.delete(StateElements.HetGroupFocus);
update.delete(StateElements.HetGroupFocusGroup);
const surroundings = MS.struct.modifier.includeSurroundings({
0: MS.struct.filter.first([
MS.struct.generator.atomGroups({
'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), resn]),
'group-by': MS.struct.atomProperty.macromolecular.residueKey()
})
]),
radius: 5,
'as-whole-residues': true
});
const core = MS.struct.filter.first([
MS.struct.generator.atomGroups({
'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), compId]),
'group-by': MS.core.str.concat([MS.struct.atomProperty.core.operatorName(), MS.struct.atomProperty.macromolecular.residueKey()])
})
]);
const surroundings = MS.struct.modifier.includeSurroundings({ 0: core, radius: 5, 'as-whole-residues': true });
const sel = update.to(StateElements.Assembly)
.apply(StateTransforms.Model.StructureSelection, { label: resn, query: surroundings }, { ref: StateElements.HetGroupFocus });
const group = update.to(StateElements.Assembly).group(StateTransforms.Misc.CreateGroup, { label: compId }, { ref: StateElements.HetGroupFocusGroup });
sel.apply(StateTransforms.Representation.StructureRepresentation3D, this.createSurVisualParams());
group.apply(StateTransforms.Model.StructureSelection, { label: 'Core', query: core }, { ref: StateElements.HetGroupFocus })
.apply(StateTransforms.Representation.StructureRepresentation3D, this.createCoreVisualParams());
group.apply(StateTransforms.Model.StructureSelection, { label: 'Surroundings', query: surroundings })
.apply(StateTransforms.Representation.StructureRepresentation3D, this.createSurVisualParams());
// sel.apply(StateTransforms.Representation.StructureLabels3D, {
// target: { name: 'residues', params: { } },
// options: {
@@ -338,7 +349,7 @@ class MolStarProteopediaWrapper {
// const position = Vec3.sub(Vec3.zero(), sphere.center, asmCenter);
// Vec3.normalize(position, position);
// Vec3.scaleAndAdd(position, sphere.center, position, sphere.radius);
const snapshot = this.plugin.canvas3d.camera.getFocus(sphere.center, 0.75 * sphere.radius);
const snapshot = this.plugin.canvas3d.camera.getFocus(sphere.center, Math.max(sphere.radius, 5));
PluginCommands.Camera.SetSnapshot.dispatch(this.plugin, { snapshot, durationMs: 250 });
}
}
@@ -352,6 +363,15 @@ class MolStarProteopediaWrapper {
});
}
private createCoreVisualParams() {
const asm = this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure;
return StructureRepresentation3DHelpers.createParams(this.plugin, asm.data, {
repr: BuiltInStructureRepresentations['ball-and-stick'],
// color: [BuiltInColorThemes.uniform, () => ({ value: ColorNames.gray })],
// size: [BuiltInSizeThemes.uniform, () => ({ value: 0.33 } )]
});
}
snapshot = {
get: () => {
return this.plugin.state.getSnapshot();

View File

@@ -1,39 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react'
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export interface BooleanParamComponentProps {
label: string
param: PD.BooleanParam
value: boolean
onChange(v: boolean): void
}
export interface BooleanParamComponentState {
value: boolean
}
export class BooleanParamComponent extends React.Component<BooleanParamComponentProps, BooleanParamComponentState> {
state = {
value: this.props.value
}
onChange(value: boolean) {
this.setState({ value })
this.props.onChange(value)
}
render() {
return <div>
<span>{this.props.label} </span>
<button onClick={e => this.onChange(!this.state.value) }>
{this.state.value ? 'Off' : 'On'}
</button>
</div>;
}
}

View File

@@ -1,43 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react'
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { ColorNames } from '../../../mol-util/color/tables';
import { Color } from '../../../mol-util/color';
export interface ColorParamComponentProps {
label: string
param: PD.Color
value: Color
onChange(v: Color): void
}
export interface ColorParamComponentState {
value: Color
}
export class ColorParamComponent extends React.Component<ColorParamComponentProps, ColorParamComponentState> {
state = {
value: this.props.value
}
onChange(value: Color) {
this.setState({ value })
this.props.onChange(value)
}
render() {
return <div>
<span>{this.props.label} </span>
<select value={this.state.value} onChange={e => this.onChange(Color(parseInt(e.target.value))) }>
{Object.keys(ColorNames).map(name => {
return <option key={name} value={(ColorNames as { [k: string]: Color})[name]}>{name}</option>
})}
</select>
</div>;
}
}

View File

@@ -1,45 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react'
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export interface MultiSelectParamComponentProps<T extends string> {
label: string
param: PD.MultiSelect<T>
value: T[]
onChange(v: T[]): void
}
export interface MultiSelectParamComponentState<T extends string> {
value: T[]
}
export class MultiSelectParamComponent<T extends string> extends React.Component<MultiSelectParamComponentProps<T>, MultiSelectParamComponentState<T>> {
state = {
value: this.props.value
}
onChange(value: T[]) {
this.setState({ value })
this.props.onChange(value)
}
render() {
return <div>
<span>{this.props.label} </span>
<select multiple value={this.state.value} onChange={e => {
const value = Array.from(e.target.options).filter(option => option.selected).map(option => option.value)
this.onChange(value as T[])
}}>
{this.props.param.options.map(v => {
const [value, label] = v
return <option key={label} value={value}>{label}</option>
})}
</select>
</div>;
}
}

View File

@@ -1,45 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react'
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export interface NumberParamComponentProps {
label: string
param: PD.Numeric
value: number
onChange(v: number): void
}
export interface NumberParamComponentState {
value: number
}
export class NumberParamComponent extends React.Component<NumberParamComponentProps, NumberParamComponentState> {
state = {
value: this.props.value
}
onChange(valueStr: string) {
const value = this.props.param.step && Number.isInteger(this.props.param.step) ? parseInt(valueStr) : parseFloat(valueStr)
this.setState({ value })
this.props.onChange(value)
}
render() {
return <div>
<span>{this.props.label} </span>
<input type='range'
value={this.state.value}
min={this.props.param.min}
max={this.props.param.max}
step={this.props.param.step}
onChange={e => this.onChange(e.currentTarget.value)}
>
</input>
</div>;
}
}

View File

@@ -1,42 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react'
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export interface SelectParamComponentProps<T extends string> {
label: string
param: PD.Select<T>
value: T
onChange(v: T): void
}
export interface SelectParamComponentState<T extends string> {
value: T
}
export class SelectParamComponent<T extends string> extends React.Component<SelectParamComponentProps<T>, SelectParamComponentState<T>> {
state = {
value: this.props.value
}
onChange(value: T) {
this.setState({ value })
this.props.onChange(value)
}
render() {
return <div>
<span>{this.props.label} </span>
<select value={this.state.value} onChange={e => this.onChange(e.target.value as T) }>
{this.props.param.options.map(v => {
const [value, label] = v
return <option key={label} value={value}>{label}</option>
})}
</select>
</div>;
}
}

View File

@@ -1,41 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react'
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export interface TextParamComponentProps {
label: string
param: PD.Text
value: string
onChange(v: string): void
}
export interface TextParamComponentState {
value: string
}
export class TextParamComponent extends React.Component<TextParamComponentProps, TextParamComponentState> {
state = {
value: this.props.value
}
onChange(value: string) {
this.setState({ value })
this.props.onChange(value)
}
render() {
return <div>
<span>{this.props.label} </span>
<input type='text'
value={this.state.value}
onChange={e => this.onChange(e.currentTarget.value)}
>
</input>
</div>;
}
}

View File

@@ -1,64 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react'
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { BooleanParamComponent } from './parameter/boolean';
import { NumberParamComponent } from './parameter/number';
import { SelectParamComponent } from './parameter/select';
import { MultiSelectParamComponent } from './parameter/multi-select';
import { TextParamComponent } from './parameter/text';
import { ColorParamComponent } from './parameter/color';
import { camelCaseToWords } from '../../mol-util/string';
interface ParametersProps<P extends PD.Params> {
params: P
values: { [k in keyof P]: P[k]['defaultValue'] }
onChange<K extends keyof P>(k: K, v: P[K]['defaultValue']): void
}
type ParametersState = {}
function getParamComponent<P extends PD.Any>(label: string, param: PD.Any, value: P['defaultValue'], onChange: (v: P['defaultValue']) => void) {
switch (param.type) {
case 'boolean':
return <BooleanParamComponent label={label} param={param} value={value} onChange={onChange} />
case 'number':
return <NumberParamComponent label={label} param={param} value={value} onChange={onChange} />
case 'select':
return <SelectParamComponent label={label} param={param} value={value} onChange={onChange} />
case 'multi-select':
return <MultiSelectParamComponent label={label} param={param} value={value} onChange={onChange} />
case 'text':
return <TextParamComponent label={label} param={param} value={value} onChange={onChange} />
case 'color':
return <ColorParamComponent label={label} param={param} value={value} onChange={onChange} />
}
return ''
}
function getLabel(name: string, param: PD.Base<any>) {
return param.label === undefined ? camelCaseToWords(name) : param.label
}
export class ParametersComponent<P extends PD.Params> extends React.Component<ParametersProps<P>, ParametersState> {
onChange(k: string, value: any) {
this.props.onChange(k, value)
}
render() {
return <div style={{ width: '100%' }}>
{ Object.keys(this.props.params).map(k => {
const param = this.props.params[k]
const value = this.props.values[k]
const label = getLabel(k, param)
return <div key={k}>
{getParamComponent(label, param, value, v => this.onChange(k, v))}
</div>
})}
</div>;
}
}

View File

@@ -1,81 +0,0 @@
// /**
// * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
// *
// * @author David Sehnal <david.sehnal@gmail.com>
// */
// import * as React from 'react'
// import { Structure, StructureSequence, Queries, StructureSelection, StructureProperties, StructureQuery } from 'mol-model/structure';
// import { EmptyLoci } from 'mol-model/loci';
// export class SequenceView extends View<SequenceViewController, {}, {}> {
// render() {
// const s = this.controller.latestState.structure;
// if (!s) return <div className='molstar-sequence-view-wrap'>No structure available.</div>;
// const seqs = s.models[0].sequence.sequences;
// return <div className='molstar-sequence-view-wrap'>
// {seqs.map((seq, i) => <EntitySequence key={i} ctx={this.controller.context} seq={seq} structure={s} /> )}
// </div>;
// }
// }
// function createQuery(entityId: string, label_seq_id: number) {
// return Queries.generators.atoms({
// entityTest: ctx => StructureProperties.entity.id(ctx.element) === entityId,
// residueTest: ctx => StructureProperties.residue.label_seq_id(ctx.element) === label_seq_id
// });
// }
// // TODO: this is really ineffective and should be done using a canvas.
// class EntitySequence extends React.Component<{ ctx: Context, seq: StructureSequence.Entity, structure: Structure }> {
// raiseInteractityEvent(seqId?: number) {
// if (typeof seqId === 'undefined') {
// InteractivityEvents.HighlightLoci.dispatch(this.props.ctx, EmptyLoci);
// return;
// }
// const query = createQuery(this.props.seq.entityId, seqId);
// const loci = StructureSelection.toLoci(StructureQuery.run(query, this.props.structure));
// if (loci.elements.length === 0) InteractivityEvents.HighlightLoci.dispatch(this.props.ctx, EmptyLoci);
// else InteractivityEvents.HighlightLoci.dispatch(this.props.ctx, loci);
// }
// render() {
// const { ctx, seq } = this.props;
// const { offset, sequence } = seq.sequence;
// const elems: JSX.Element[] = [];
// for (let i = 0, _i = sequence.length; i < _i; i++) {
// elems[elems.length] = <ResidueView ctx={ctx} seqId={offset + i} letter={sequence[i]} parent={this} key={i} />;
// }
// return <div style={{ wordWrap: 'break-word' }}>
// <span style={{ fontWeight: 'bold' }}>{this.props.seq.entityId}:{offset}&nbsp;</span>
// {elems}
// </div>;
// }
// }
// class ResidueView extends React.Component<{ ctx: Context, seqId: number, letter: string, parent: EntitySequence }, { isHighlighted: boolean }> {
// state = { isHighlighted: false }
// mouseEnter = () => {
// this.setState({ isHighlighted: true });
// this.props.parent.raiseInteractityEvent(this.props.seqId);
// }
// mouseLeave = () => {
// this.setState({ isHighlighted: false });
// this.props.parent.raiseInteractityEvent();
// }
// render() {
// return <span onMouseEnter={this.mouseEnter} onMouseLeave={this.mouseLeave}
// style={{ cursor: 'pointer', backgroundColor: this.state.isHighlighted ? 'yellow' : void 0 }}>
// {this.props.letter}
// </span>;
// }
// }

View File

@@ -250,7 +250,7 @@ function updateOrtho(camera: Camera) {
let top = cy + dy
let bottom = cy - dy
if (viewOffset && viewOffset.enabled) {
if (viewOffset.enabled) {
const zoomW = zoom / (viewOffset.width / viewOffset.fullWidth)
const zoomH = zoom / (viewOffset.height / viewOffset.fullHeight)
const scaleW = (fullRight - fullLeft) / viewOffset.width
@@ -279,7 +279,7 @@ function updatePers(camera: Camera) {
let width = aspect * height
let left = -0.5 * width
if (viewOffset && viewOffset.enabled) {
if (viewOffset.enabled) {
left += viewOffset.offsetX * width / viewOffset.fullWidth
top -= viewOffset.offsetY * height / viewOffset.fullHeight
width *= viewOffset.width / viewOffset.fullWidth

View File

@@ -17,7 +17,7 @@ import { Representation } from '../mol-repr/representation';
import Scene from '../mol-gl/scene';
import { GraphicsRenderVariant } from '../mol-gl/webgl/render-item';
import { PickingId } from '../mol-geo/geometry/picking';
import { MarkerAction } from '../mol-geo/geometry/marker-data';
import { MarkerAction } from '../mol-util/marker-action';
import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
import { Camera } from './camera';
import { ParamDefinition as PD } from '../mol-util/param-definition';

View File

@@ -13,57 +13,6 @@ export type MarkerData = {
uMarkerTexDim: ValueCell<Vec2>
}
export enum MarkerAction {
Highlight,
RemoveHighlight,
Select,
Deselect,
Toggle,
Clear
}
export function applyMarkerAction(array: Uint8Array, start: number, end: number, action: MarkerAction) {
let changed = false
for (let i = start; i < end; ++i) {
let v = array[i]
switch (action) {
case MarkerAction.Highlight:
if (v % 2 === 0) {
v += 1
}
break
case MarkerAction.RemoveHighlight:
if (v % 2 !== 0) {
v -= 1
}
break
case MarkerAction.Select:
if (v < 2) v += 2
// v += 2
break
case MarkerAction.Deselect:
// if (v >= 2) {
// v -= 2
// }
v = v % 2
break
case MarkerAction.Toggle:
if (v >= 2) {
v -= 2
} else {
v += 2
}
break
case MarkerAction.Clear:
v = 0
break
}
changed = array[i] !== v || changed
array[i] = v
}
return changed
}
export function createMarkers(count: number, markerData?: MarkerData): MarkerData {
const markers = createTextureImage(Math.max(1, count), 1, Uint8Array, markerData && markerData.tMarker.ref.value.array)
if (markerData) {

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.311, IHM 1.0, CARB draft.
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.312, IHM 1.0, CARB draft.
*
* @author molstar/ciftools package
*/

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.311, IHM 1.0, CARB draft.
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.312, IHM 1.0, CARB draft.
*
* @author molstar/ciftools package
*/

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.311, IHM 1.0, CARB draft.
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.312, IHM 1.0, CARB draft.
*
* @author molstar/ciftools package
*/
@@ -1549,6 +1549,85 @@ export const mmCIF_Schema = {
*/
program_version: str,
},
/**
* Data items in the PDBX_UNOBS_OR_ZERO_OCC_RESIDUES category list the
* residues within the entry that are not observed or have zero occupancy.
*/
pdbx_unobs_or_zero_occ_residues: {
/**
* The value of _pdbx_unobs_or_zero_occ_residues.id must uniquely identify
* each item in the PDBX_UNOBS_OR_ZERO_OCC_RESIDUES list.
*
* This is an integer serial number.
*/
id: int,
/**
* The value of polymer flag indicates whether the unobserved or
* zero occupancy residue is part of a polymer chain or not
*/
polymer_flag: Aliased<'Y' | 'N'>(str),
/**
* The value of occupancy flag indicates whether the residue
* is unobserved (= 1) or the coordinates have an occupancy of zero (=0)
*/
occupancy_flag: Aliased<'1' | '0'>(int),
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.pdbx_PDB_model_num in the
* ATOM_SITE category.
*/
PDB_model_num: int,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.auth_asym_id in the
* ATOM_SITE category.
*/
auth_asym_id: str,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.auth_comp_id in the
* ATOM_SITE category.
*/
auth_comp_id: str,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.auth_seq_id in the
* ATOM_SITE category.
*/
auth_seq_id: str,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.pdbx_PDB_ins_code in the
* ATOM_SITE category.
*/
PDB_ins_code: str,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.label_asym_id in the
* ATOM_SITE category.
*/
label_asym_id: str,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.label_comp_id in the
* ATOM_SITE category.
*/
label_comp_id: str,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
* This data item is a pointer to _atom_site.label_seq_id in the
* ATOM_SITE category.
*/
label_seq_id: int,
},
/**
* Data items in the PDBX_STRUCT_MOD_RESIDUE category list the
* modified polymer components in the entry and provide some
@@ -2079,10 +2158,10 @@ export const mmCIF_Schema = {
pdbx_end_seq_num: int,
},
/**
* Data items in the PDBX_ENTITY_DESCRIPTOR category provide
* Data items in the PDBX_ENTITY_BRANCH_DESCRIPTOR category provide
* string descriptors of entity chemical structure.
*/
pdbx_entity_descriptor: {
pdbx_entity_branch_descriptor: {
/**
* This data item is a pointer to _entity_poly.entity_id in the ENTITY
* category.

View File

@@ -90,7 +90,7 @@ namespace Spacegroup {
export function getSymmetryOperator(spacegroup: Spacegroup, index: number, i: number, j: number, k: number): SymmetryOperator {
const operator = updateOperatorMatrix(spacegroup, index, i, j, k, Mat4.zero());
return SymmetryOperator.create(`${index + 1}_${5 + i}${5 + j}${5 + k}`, operator, { id: '', operList: [] }, '', Vec3.create(i, j, k));
return SymmetryOperator.create(`${index + 1}_${5 + i}${5 + j}${5 + k}`, operator, { id: '', operList: [] }, '', Vec3.create(i, j, k), index);
}
function getOperatorMatrix(ids: number[]) {

View File

@@ -6,6 +6,7 @@
import { Vec3, Mat4, Mat3, Quat } from '../linear-algebra/3d'
import { lerp as scalar_lerp } from '../../mol-math/interpolate';
import { defaults } from '../../mol-util';
interface SymmetryOperator {
readonly name: string,
@@ -21,6 +22,8 @@ interface SymmetryOperator {
readonly ncsId: string,
readonly hkl: Vec3,
/** spacegroup symmetry operator index, -1 if not applicable */
readonly spgrOp: number,
readonly matrix: Mat4,
// cache the inverse of the transform
@@ -35,12 +38,13 @@ namespace SymmetryOperator {
export const RotationTranslationEpsilon = 0.005;
export function create(name: string, matrix: Mat4, assembly: SymmetryOperator['assembly'], ncsId?: string, hkl?: Vec3): SymmetryOperator {
export function create(name: string, matrix: Mat4, assembly: SymmetryOperator['assembly'], ncsId?: string, hkl?: Vec3, spgrOp?: number): SymmetryOperator {
const _hkl = hkl ? Vec3.clone(hkl) : Vec3.zero();
spgrOp = defaults(spgrOp, -1)
ncsId = ncsId || ''
if (Mat4.isIdentity(matrix)) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl, ncsId };
if (Mat4.isIdentity(matrix)) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl, spgrOp, ncsId };
if (!Mat4.isRotationAndTranslation(matrix, RotationTranslationEpsilon)) throw new Error(`Symmetry operator (${name}) must be a composition of rotation and translation.`);
return { name, assembly, matrix, inverse: Mat4.invert(Mat4.zero(), matrix), isIdentity: false, hkl: _hkl, ncsId };
return { name, assembly, matrix, inverse: Mat4.invert(Mat4.zero(), matrix), isIdentity: false, hkl: _hkl, spgrOp, ncsId };
}
export function checkIfRotationAndTranslation(rot: Mat3, offset: Vec3) {
@@ -106,11 +110,11 @@ namespace SymmetryOperator {
/**
* Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix).
* Keep `name`, `assembly`, `ncsId` and `hkl` properties from second.
* Keep `name`, `assembly`, `ncsId`, `hkl` and `spgrOpId` properties from second.
*/
export function compose(first: SymmetryOperator, second: SymmetryOperator) {
const matrix = Mat4.mul(Mat4.zero(), second.matrix, first.matrix);
return create(second.name, matrix, second.assembly, second.ncsId, second.hkl);
return create(second.name, matrix, second.assembly, second.ncsId, second.hkl, second.spgrOp);
}
export interface CoordinateMapper<T extends number> { (index: T, slot: Vec3): Vec3 }

View File

@@ -12,7 +12,7 @@ import { Tensor, Vec3 } from '../../../mol-math/linear-algebra';
import { RuntimeContext } from '../../../mol-task';
import UUID from '../../../mol-util/uuid';
import { Model } from '../../../mol-model/structure/model/model';
import { Entities } from '../../../mol-model/structure/model/properties/common';
import { Entities, ChemicalComponent, MissingResidue } from '../../../mol-model/structure/model/properties/common';
import { CustomProperties } from '../../../mol-model/structure';
import { ModelSymmetry } from '../../../mol-model/structure/model/properties/symmetry';
import { createAssemblies } from './assembly';
@@ -23,7 +23,6 @@ import { getSecondaryStructure } from './secondary-structure';
import { getSequence } from './sequence';
import { sortAtomSite } from './sort';
import { StructConn } from './bonds/struct_conn';
import { ChemicalComponent } from '../../../mol-model/structure/model/properties/chemical-component';
import { getMoleculeType, MoleculeType, getEntityType } from '../../../mol-model/structure/model/types';
import { ModelFormat } from '../format';
import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from '../../../mol-model/structure/structure/carbohydrates/constants';
@@ -98,14 +97,36 @@ function getModifiedResidueNameMap(format: mmCIF_Format): Model['properties']['m
return { parentId, details };
}
function getMissingResidues(format: mmCIF_Format): Model['properties']['missingResidues'] {
const map = new Map<string, MissingResidue>();
const c = format.data.pdbx_unobs_or_zero_occ_residues
const getKey = (model_num: number, asym_id: string, seq_id: number) => {
return `${model_num}|${asym_id}|${seq_id}`
}
for (let i = 0, il = c._rowCount; i < il; ++i) {
const key = getKey(c.PDB_model_num.value(i), c.label_asym_id.value(i), c.label_seq_id.value(i))
map.set(key, { polymer_flag: c.polymer_flag.value(i), occupancy_flag: c.occupancy_flag.value(i) })
}
return {
has: (model_num: number, asym_id: string, seq_id: number) => {
return map.has(getKey(model_num, asym_id, seq_id))
},
get: (model_num: number, asym_id: string, seq_id: number) => {
return map.get(getKey(model_num, asym_id, seq_id))
},
size: map.size
}
}
function getChemicalComponentMap(format: mmCIF_Format): Model['properties']['chemicalComponentMap'] {
const map = new Map<string, ChemicalComponent>();
const { chem_comp } = format.data
if (chem_comp._rowCount > 0) {
const { id } = format.data.chem_comp
for (let i = 0, il = id.rowCount; i < il; ++i) {
map.set(id.value(i), Table.getRow(format.data.chem_comp, i))
}
const { id } = chem_comp
for (let i = 0, il = id.rowCount; i < il; ++i) {
map.set(id.value(i), Table.getRow(chem_comp, i))
}
return map
}
@@ -158,6 +179,7 @@ const getUniqueComponentNames = memoize1((format: mmCIF_Format) => {
export interface FormatData {
modifiedResidues: Model['properties']['modifiedResidues']
missingResidues: Model['properties']['missingResidues']
chemicalComponentMap: Model['properties']['chemicalComponentMap']
saccharideComponentMap: Model['properties']['saccharideComponentMap']
}
@@ -165,6 +187,7 @@ export interface FormatData {
function getFormatData(format: mmCIF_Format): FormatData {
return {
modifiedResidues: getModifiedResidueNameMap(format),
missingResidues: getMissingResidues(format),
chemicalComponentMap: getChemicalComponentMap(format),
saccharideComponentMap: getSaccharideComponentMap(format)
}
@@ -172,11 +195,12 @@ function getFormatData(format: mmCIF_Format): FormatData {
function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, formatData: FormatData, previous?: Model): Model {
const atomic = getAtomicHierarchyAndConformation(atom_site, sourceIndex, entities, formatData, previous);
const modelNum = atom_site.pdbx_PDB_model_num.value(0)
if (previous && atomic.sameAsPrevious) {
return {
...previous,
id: UUID.create22(),
modelNum: atom_site.pdbx_PDB_model_num.value(0),
modelNum,
atomicConformation: atomic.conformation,
_dynamicPropertyData: Object.create(null)
};
@@ -192,7 +216,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, sourceIn
label,
entry: label,
sourceData: format,
modelNum: atom_site.pdbx_PDB_model_num.value(0),
modelNum,
entities,
symmetry: getSymmetry(format),
sequence: getSequence(format.data, entities, atomic.hierarchy, formatData.modifiedResidues.parentId),

View File

@@ -10,12 +10,11 @@ import StructureSequence from './properties/sequence';
import { AtomicHierarchy, AtomicConformation } from './properties/atomic';
import { ModelSymmetry } from './properties/symmetry';
import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
import { Entities } from './properties/common';
import { Entities, ChemicalComponentMap, MissingResidues } from './properties/common';
import { CustomProperties } from '../common/custom-property';
import { SecondaryStructure } from './properties/seconday-structure';
import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
import { ModelFormat } from '../../../mol-model-formats/structure/format';
import { ChemicalComponentMap } from './properties/chemical-component';
/**
* Interface to the "source data" of the molecule.
@@ -49,6 +48,8 @@ export interface Model extends Readonly<{
parentId: ReadonlyMap<string, string>,
details: ReadonlyMap<string, string>
}>,
/** map that holds details about unobserved or zero occurrence residues */
readonly missingResidues: MissingResidues,
/** maps residue name to `ChemicalComponent` data */
readonly chemicalComponentMap: ChemicalComponentMap
/** maps residue name to `SaccharideComponent` data */

View File

@@ -1,13 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { mmCIF_Schema } from '../../../../mol-io/reader/cif/schema/mmcif';
import { Table } from '../../../../mol-data/db';
export type ChemicalComponent = Table.Row<mmCIF_Schema['chem_comp']>
export type ChemicalComponentMap = ReadonlyMap<string, ChemicalComponent>
// TODO add data for common chemical components

View File

@@ -1,13 +1,28 @@
/**
* 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 Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { mmCIF_Database as mmCIF } from '../../../../mol-io/reader/cif/schema/mmcif'
import { mmCIF_Database, mmCIF_Schema } from '../../../../mol-io/reader/cif/schema/mmcif'
import { Table } from '../../../../mol-data/db';
import { EntityIndex } from '../indexing';
export interface Entities {
data: mmCIF['entity'],
data: mmCIF_Database['entity'],
getEntityIndex(id: string): EntityIndex
}
export type ChemicalComponent = Table.Row<mmCIF_Schema['chem_comp']>
export type ChemicalComponentMap = ReadonlyMap<string, ChemicalComponent>
export type MissingResidue = Table.Row<Pick<
mmCIF_Schema['pdbx_unobs_or_zero_occ_residues'],
'polymer_flag' | 'occupancy_flag'>
>
export interface MissingResidues {
has(model_num: number, asym_id: string, seq_id: number): boolean
get(model_num: number, asym_id: string, seq_id: number): MissingResidue | undefined
readonly size: number
}

View File

@@ -5,11 +5,11 @@
*/
import { AtomicData } from '../atomic';
import { ChemicalComponentMap } from '../chemical-component';
import { AtomicIndex, AtomicDerivedData } from '../atomic/hierarchy';
import { ElementIndex, ResidueIndex } from '../../indexing';
import { MoleculeType, getMoleculeType, getComponentType } from '../../types';
import { getAtomIdForAtomRole } from '../../../../../mol-model/structure/util';
import { ChemicalComponentMap } from '../common';
export function getAtomicDerivedData(data: AtomicData, index: AtomicIndex, chemicalComponentMap: ChemicalComponentMap): AtomicDerivedData {
const { label_comp_id, _rowCount: n } = data.residues

View File

@@ -7,8 +7,8 @@
import { CoarseRanges, CoarseElementData } from '../coarse/hierarchy';
import { Segmentation, Interval } from '../../../../../mol-data/int';
import SortedRanges from '../../../../../mol-data/int/sorted-ranges';
import { ChemicalComponent } from '../chemical-component';
import { ElementIndex } from '../../indexing';
import { ChemicalComponent } from '../common';
// TODO assumes all coarse elements are part of a polymer
// TODO add gaps at the ends of the chains by comparing to the polymer sequence data

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2017 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 { StructureQuery } from '../query'
@@ -20,11 +21,13 @@ export const none: StructureQuery = ctx => StructureSelection.Sequence(ctx.input
export const all: StructureQuery = ctx => StructureSelection.Singletons(ctx.inputStructure, ctx.inputStructure);
export interface AtomsQueryParams {
/** Query to be executed for each unit once */
unitTest: QueryPredicate,
/** Query to be executed for each entity once */
entityTest: QueryPredicate,
/** Query to be executed for each chain once */
chainTest: QueryPredicate,
/** Query to be executed for each residue once */
/** Query to be executed for each residue (or coarse element) once */
residueTest: QueryPredicate,
/** Query to be executed for each atom */
atomTest: QueryPredicate,
@@ -38,10 +41,11 @@ function _true(ctx: QueryContextView) { return true; }
function _zero(ctx: QueryContextView) { return 0; }
export function atoms(params?: Partial<AtomsQueryParams>): StructureQuery {
if (!params || (!params.atomTest && !params.residueTest && !params.chainTest && !params.entityTest && !params.groupBy)) return all;
if (!!params.atomTest && !params.residueTest && !params.chainTest && !params.entityTest && !params.groupBy) return atomGroupsLinear(params.atomTest);
if (!params || (!params.atomTest && !params.residueTest && !params.chainTest && !params.entityTest && !params.unitTest && !params.groupBy)) return all;
if (!!params.atomTest && !params.residueTest && !params.chainTest && !params.entityTest && !params.unitTest && !params.groupBy) return atomGroupsLinear(params.atomTest);
const normalized: AtomsQueryParams = {
unitTest: params.unitTest || _true,
entityTest: params.entityTest || _true,
chainTest: params.chainTest || _true,
residueTest: params.residueTest || _true,
@@ -78,7 +82,7 @@ function atomGroupsLinear(atomTest: QueryPredicate): StructureQuery {
};
}
function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: AtomsQueryParams): StructureQuery {
function atomGroupsSegmented({ unitTest, entityTest, chainTest, residueTest, atomTest }: AtomsQueryParams): StructureQuery {
return ctx => {
const { inputStructure } = ctx;
const { units } = inputStructure;
@@ -86,31 +90,53 @@ function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: A
const builder = inputStructure.subsetBuilder(true);
for (const unit of units) {
if (unit.kind !== Unit.Kind.Atomic) continue;
l.unit = unit;
const elements = unit.elements;
if (!unitTest(ctx)) continue;
const { elements, model } = unit;
builder.beginUnit(unit.id);
const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements);
while (chainsIt.hasNext) {
const chainSegment = chainsIt.move();
l.element = elements[chainSegment.start];
// test entity and chain
if (!entityTest(ctx) || !chainTest(ctx)) continue;
residuesIt.setSegment(chainSegment);
while (residuesIt.hasNext) {
const residueSegment = residuesIt.move();
l.element = elements[residueSegment.start];
if (unit.kind === Unit.Kind.Atomic) {
const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements);
// test residue
if (!residueTest(ctx)) continue;
while (chainsIt.hasNext) {
const chainSegment = chainsIt.move();
l.element = elements[chainSegment.start];
// test entity and chain
if (!entityTest(ctx) || !chainTest(ctx)) continue;
for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
residuesIt.setSegment(chainSegment);
while (residuesIt.hasNext) {
const residueSegment = residuesIt.move();
l.element = elements[residueSegment.start];
// test residue
if (!residueTest(ctx)) continue;
for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
l.element = elements[j];
// test atom
if (atomTest(ctx)) {
builder.addElement(l.element);
}
}
}
}
} else {
const { chainElementSegments } = Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians
const chainsIt = Segmentation.transientSegments(chainElementSegments, elements);
while (chainsIt.hasNext) {
const chainSegment = chainsIt.move();
l.element = elements[chainSegment.start];
// test entity and chain
if (!entityTest(ctx) || !chainTest(ctx)) continue;
for (let j = chainSegment.start, _j = chainSegment.end; j < _j; j++) {
l.element = elements[j];
if (atomTest(ctx)) {
// test residue/coarse element
if (residueTest(ctx)) {
builder.addElement(l.element);
}
}
@@ -125,7 +151,7 @@ function atomGroupsSegmented({ entityTest, chainTest, residueTest, atomTest }: A
};
}
function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, groupBy }: AtomsQueryParams): StructureQuery {
function atomGroupsGrouped({ unitTest, entityTest, chainTest, residueTest, atomTest, groupBy }: AtomsQueryParams): StructureQuery {
return ctx => {
const { inputStructure } = ctx;
const { units } = inputStructure;
@@ -133,30 +159,53 @@ function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, group
const builder = new LinearGroupingBuilder(inputStructure);
for (const unit of units) {
if (unit.kind !== Unit.Kind.Atomic) continue;
l.unit = unit;
const elements = unit.elements;
if (!unitTest(ctx)) continue;
const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements);
while (chainsIt.hasNext) {
const chainSegment = chainsIt.move();
l.element = elements[chainSegment.start];
// test entity and chain
if (!entityTest(ctx) || !chainTest(ctx)) continue;
const { elements, model } = unit;
residuesIt.setSegment(chainSegment);
while (residuesIt.hasNext) {
const residueSegment = residuesIt.move();
l.element = elements[residueSegment.start];
if (unit.kind === Unit.Kind.Atomic) {
const chainsIt = Segmentation.transientSegments(model.atomicHierarchy.chainAtomSegments, elements);
const residuesIt = Segmentation.transientSegments(model.atomicHierarchy.residueAtomSegments, elements);
// test residue
if (!residueTest(ctx)) continue;
while (chainsIt.hasNext) {
const chainSegment = chainsIt.move();
l.element = elements[chainSegment.start];
// test entity and chain
if (!entityTest(ctx) || !chainTest(ctx)) continue;
for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
residuesIt.setSegment(chainSegment);
while (residuesIt.hasNext) {
const residueSegment = residuesIt.move();
l.element = elements[residueSegment.start];
// test residue
if (!residueTest(ctx)) continue;
for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
l.element = elements[j];
// test atom
if (atomTest(ctx)) {
builder.add(groupBy(ctx), unit.id, l.element);
}
}
}
}
} else {
const { chainElementSegments } = Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians
const chainsIt = Segmentation.transientSegments(chainElementSegments, elements);
while (chainsIt.hasNext) {
const chainSegment = chainsIt.move();
l.element = elements[chainSegment.start];
// test entity and chain
if (!entityTest(ctx) || !chainTest(ctx)) continue;
for (let j = chainSegment.start, _j = chainSegment.end; j < _j; j++) {
l.element = elements[j];
if (atomTest(ctx)) builder.add(groupBy(ctx), unit.id, l.element);
// test residue/coarse element
if (residueTest(ctx)) {
builder.add(groupBy(ctx), unit.id, l.element);
}
}
}
}

View File

@@ -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';
@@ -30,6 +31,12 @@ namespace StructureElement {
return { kind: 'element-location', unit: unit!, element: element || (0 as ElementIndex) };
}
export function set(a: StructureElement, unit?: Unit, element?: ElementIndex): StructureElement {
if (unit) a.unit = unit
if (element !== undefined) a.element = element
return a;
}
// TODO: when nominal types are available, make this indexed by UnitIndex
export type Set = SortedArray<ElementIndex>
@@ -150,11 +157,16 @@ namespace StructureElement {
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) };
map.delete(e.unit.id)
} else {
elements[elements.length] = e;
}
}
map.forEach((indices, id) => {
elements[elements.length] = { unit: xs.structure.unitMap.get(id)!, indices };
});
return Loci(xs.structure, elements);
}
@@ -195,24 +207,65 @@ namespace StructureElement {
const elements: Loci['elements'][0][] = [];
for (const lociElement of loci.elements) {
if (lociElement.unit.kind !== Unit.Kind.Atomic) elements[elements.length] = lociElement;
if (lociElement.unit.kind === Unit.Kind.Atomic) {
const unitElements = lociElement.unit.elements;
const h = lociElement.unit.model.atomicHierarchy;
const unitElements = lociElement.unit.elements;
const h = lociElement.unit.model.atomicHierarchy;
const { index: residueIndex, offsets: residueOffsets } = h.residueAtomSegments;
const { index: residueIndex, offsets: residueOffsets } = h.residueAtomSegments;
const newIndices: UnitIndex[] = [];
const indices = lociElement.indices, len = OrderedSet.size(indices);
let i = 0;
while (i < len) {
const rI = residueIndex[unitElements[OrderedSet.getAt(indices, i)]];
i++;
while (i < len && residueIndex[unitElements[OrderedSet.getAt(indices, i)]] === rI) {
i++;
}
for (let j = residueOffsets[rI], _j = residueOffsets[rI + 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) };
} else {
// coarse elements are already by-residue
elements[elements.length] = lociElement;
}
}
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 rI = residueIndex[unitElements[OrderedSet.getAt(indices, i)]];
const cI = chainIndex[unitElements[OrderedSet.getAt(indices, i)]];
i++;
while (i < len && residueIndex[unitElements[OrderedSet.getAt(indices, i)]] === rI) {
while (i < len && chainIndex[unitElements[OrderedSet.getAt(indices, i)]] === cI) {
i++;
}
for (let j = residueOffsets[rI], _j = residueOffsets[rI + 1]; j < _j; j++) {
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;
}

View File

@@ -111,8 +111,15 @@ const entity = {
}
const unit = {
id: StructureElement.property(l => l.unit.id),
operator_name: StructureElement.property(l => l.unit.conformation.operator.name),
model_num: StructureElement.property(l => l.unit.model.modelNum)
hkl: StructureElement.property(l => l.unit.conformation.operator.hkl),
spgrOp: StructureElement.property(l => l.unit.conformation.operator.spgrOp),
model_num: StructureElement.property(l => l.unit.model.modelNum),
pdbx_struct_assembly_id: StructureElement.property(l => l.unit.conformation.operator.assembly.id),
pdbx_struct_oper_list_ids: StructureElement.property(l => l.unit.conformation.operator.assembly.operList),
struct_ncs_oper_id: StructureElement.property(l => l.unit.conformation.operator.ncsId),
}
const StructureProperties = {

View File

@@ -141,6 +141,7 @@ class Structure {
return new Structure.ElementLocationIterator(this);
}
/** the root/top-most parent or `undefined` in case this is the root */
get parent() {
return this._props.parent;
}
@@ -240,7 +241,7 @@ class Structure {
constructor(units: ArrayLike<Unit>, parent: Structure | undefined, coordinateSystem?: SymmetryOperator) {
this.unitMap = this.initUnits(units);
this.units = units as ReadonlyArray<Unit>;
if (parent) this._props.parent = parent;
if (parent) this._props.parent = parent.parent || parent;
if (coordinateSystem) this._props.coordinateSystem = coordinateSystem;
else if (parent) this._props.coordinateSystem = parent.coordinateSystem;
}
@@ -512,6 +513,11 @@ namespace Structure {
return areEquivalent(a.parent || a, b.parent || b)
}
/** Check if the structures or their parents are equal */
export function areParentsEqual(a: Structure, b: Structure) {
return (a.parent || a) === (b.parent || b)
}
export class ElementLocationIterator implements Iterator<StructureElement> {
private current = StructureElement.create();
private unitIndex = 0;

View File

@@ -117,7 +117,7 @@ function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {
for (let u = 0; u < ncsCount; ++u) {
const ncsOp = ncsOperators![u]
const matrix = Mat4.mul(Mat4.zero(), symOp.matrix, ncsOp.matrix)
const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl);
const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl, symOp.spgrOp);
operators[operators.length] = operator;
}
} else {

View File

@@ -1,5 +1,5 @@
/**
* 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>
@@ -8,6 +8,7 @@
import { Unit, StructureElement } from '../../structure'
import Structure from '../structure';
import { LinkType } from '../../model/types';
import { SortedArray } from '../../../../mol-data/int';
export * from './links/data'
export * from './links/intra-compute'
@@ -61,6 +62,33 @@ namespace Link {
return true
}
export function toStructureElementLoci(loci: Loci): StructureElement.Loci {
const elements: StructureElement.Loci['elements'][0][] = []
const map = new Map<number, number[]>()
for (const lociLink of loci.links) {
const { aIndex, aUnit, bIndex, bUnit } = lociLink
if (aUnit === bUnit) {
if (map.has(aUnit.id)) map.get(aUnit.id)!.push(aIndex, bIndex)
else map.set(aUnit.id, [aIndex, bIndex])
} else {
if (map.has(aUnit.id)) map.get(aUnit.id)!.push(aIndex)
else map.set(aUnit.id, [aIndex])
if (map.has(bUnit.id)) map.get(bUnit.id)!.push(bIndex)
else map.set(bUnit.id, [bIndex])
}
}
map.forEach((indices: number[], id: number) => {
elements.push({
unit: loci.structure.unitMap.get(id)!,
indices: SortedArray.deduplicate(SortedArray.ofUnsortedArray(indices))
})
})
return StructureElement.Loci(loci.structure, elements);
}
export function getType(structure: Structure, link: Location<Unit.Atomic>): LinkType {
if (link.aUnit === link.bUnit) {
const links = link.aUnit.links;

View File

@@ -14,7 +14,7 @@ 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.canvas3d.click, ({ current, buttons, modifiers }) => {
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
if (!this.ctx.canvas3d || buttons !== ButtonsType.Flag.Primary || !ModifiersKeys.areEqual(modifiers, ModifiersKeys.None)) return;
const sphere = Loci.getBoundingSphere(current.loci);
@@ -25,7 +25,7 @@ export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extr
},
params: () => ({
minRadius: ParamDefinition.Numeric(8, { min: 1, max: 50, step: 1 }),
extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the boundning sphere radius of the Loci.' })
extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci.' })
}),
display: { name: 'Focus Loci on Select' }
});

View File

@@ -5,45 +5,27 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { MarkerAction } from '../../../mol-geo/geometry/marker-data';
import { EmptyLoci } from '../../../mol-model/loci';
import { StructureElement } from '../../../mol-model/structure';
import { MarkerAction } from '../../../mol-util/marker-action';
import { PluginContext } from '../../../mol-plugin/context';
import { Representation } from '../../../mol-repr/representation';
import { PluginStateObject as SO } from '../../state/objects';
import { labelFirst } from '../../../mol-theme/label';
import { ButtonsType } from '../../../mol-util/input/input-observer';
import { PluginBehavior } from '../behavior';
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 {
register(): void {
let prev: Representation.Loci = { loci: EmptyLoci, repr: void 0 };
const sel = this.ctx.helpers.structureSelection;
this.subscribeObservable(this.ctx.behaviors.canvas3d.highlight, ({ current, modifiers }) => {
if (!this.ctx.canvas3d) return;
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);
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;
}
}
});
private lociMarkProvider = (interactionLoci: Interactivity.Loci, action: MarkerAction) => {
if (!this.ctx.canvas3d) return;
this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action)
}
register() {
this.ctx.interactivity.lociHighlights.addProvider(this.lociMarkProvider)
}
unregister() {
this.ctx.interactivity.lociHighlights.removeProvider(this.lociMarkProvider)
}
},
display: { name: 'Highlight Loci on Canvas' }
@@ -53,52 +35,33 @@ export const SelectLoci = PluginBehavior.create({
name: 'representation-select-loci',
category: 'interaction',
ctor: class extends PluginBehavior.Handler {
register(): void {
const sel = this.ctx.helpers.structureSelection;
private spine: StateTreeSpine.Impl
private lociMarkProvider = (interactionLoci: Interactivity.Loci, action: MarkerAction) => {
if (!this.ctx.canvas3d) return;
this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action)
}
register() {
this.ctx.interactivity.lociSelections.addProvider(this.lociMarkProvider)
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);
}
}
this.subscribeObservable(this.ctx.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
if (!this.ctx.canvas3d) return;
if (current.loci.kind === 'empty-loci') {
if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
// clear the selection on Ctrl + Right-Click on empty
const sels = sel.clear();
for (const s of sels) this.ctx.canvas3d.mark({ loci: s }, MarkerAction.Deselect);
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
const so = this.spine.getRootOfType(SO.Molecule.Structure)
if (so) {
const loci = this.ctx.helpers.structureSelection.get(so.data)
this.lociMarkProvider({ loci }, MarkerAction.Select)
}
} else 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);
}
});
}
unregister() {
this.ctx.interactivity.lociSelections.removeProvider(this.lociMarkProvider)
}
constructor(ctx: PluginContext, params: {}) {
super(ctx, params)
this.spine = new StateTreeSpine.Impl(ctx.state.dataState.cells)
}
},
display: { name: 'Select Loci on Canvas' }
});
@@ -108,7 +71,7 @@ export const DefaultLociLabelProvider = PluginBehavior.create({
category: 'interaction',
ctor: class implements PluginBehavior<undefined> {
private f = labelFirst;
register(): void { this.ctx.lociLabels.addProvider(this.f); }
register() { this.ctx.lociLabels.addProvider(this.f); }
unregister() { this.ctx.lociLabels.removeProvider(this.f); }
constructor(protected ctx: PluginContext) { }
},

View File

@@ -121,7 +121,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
}
});
this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
if (buttons !== ButtonsType.Flag.Secondary) return;
if (current.loci.kind === 'empty-loci') {

View File

@@ -195,7 +195,7 @@ export namespace VolumeStreaming {
}
});
this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
if (buttons !== ButtonsType.Flag.Secondary || this.params.view.name !== 'selection-box') return;
if (current.loci.kind === 'empty-loci') {

View File

@@ -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();
})
}

View File

@@ -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 }>()

View File

@@ -8,7 +8,7 @@ import { shallowMergeArray } from '../mol-util/object';
import { RxEventHelper } from '../mol-util/rx-event-helper';
export class PluginComponent<State> {
private _ev: RxEventHelper;
private _ev: RxEventHelper | undefined;
protected get ev() {
return this._ev || (this._ev = RxEventHelper.create());

View File

@@ -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 { List } from 'immutable';
@@ -32,10 +33,19 @@ import { TaskManager } from './util/task-manager';
import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
import { StructureElementSelectionManager } from './util/structure-element-selection';
import { SubstructureParentHelper } from './util/substructure-parent-helper';
import { Representation } from '../mol-repr/representation';
import { ModifiersKeys } from '../mol-util/input/input-observer';
import { isProductionMode, isDebugMode } from '../mol-util/debug';
import { Model, Structure } from '../mol-model/structure';
import { Interactivity } from './util/interactivity';
interface Log {
entries: List<LogEntry>
readonly entry: (e: LogEntry) => void
readonly error: (msg: string) => void
readonly message: (msg: string) => void
readonly info: (msg: string) => void
readonly warn: (msg: string) => void
}
export class PluginContext {
private disposed = false;
@@ -64,41 +74,45 @@ export class PluginContext {
task: this.tasks.events,
canvas3d: {
settingsUpdated: this.ev()
},
interactivity: {
propsUpdated: this.ev()
}
};
} as const
readonly behaviors = {
state: {
isAnimating: this.ev.behavior<boolean>(false),
isUpdating: this.ev.behavior<boolean>(false)
},
canvas3d: {
highlight: this.ev.behavior<Canvas3D.HighlightEvent>({ current: Representation.Loci.Empty }),
click: this.ev.behavior<Canvas3D.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 })
interaction: {
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: [] })
}
};
} as const
readonly canvas3d: Canvas3D;
readonly layout: PluginLayout = new PluginLayout(this);
readonly interactivity: Interactivity;
readonly lociLabels: LociLabelManager;
readonly structureRepresentation = {
registry: new StructureRepresentationRegistry(),
themeCtx: { colorThemeRegistry: ColorTheme.createRegistry(), sizeThemeRegistry: SizeTheme.createRegistry() } as ThemeRegistryContext
}
} as const
readonly volumeRepresentation = {
registry: new VolumeRepresentationRegistry(),
themeCtx: { colorThemeRegistry: ColorTheme.createRegistry(), sizeThemeRegistry: SizeTheme.createRegistry() } as ThemeRegistryContext
}
} as const
readonly dataFormat = {
registry: new DataFormatRegistry()
}
} as const
readonly customModelProperties = new CustomPropertyRegistry<Model>();
readonly customStructureProperties = new CustomPropertyRegistry<Structure>();
@@ -107,7 +121,7 @@ export class PluginContext {
readonly helpers = {
structureSelection: new StructureElementSelectionManager(this),
substructureParent: new SubstructureParentHelper(this)
};
} as const;
initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
try {
@@ -125,7 +139,7 @@ export class PluginContext {
}
}
readonly log = {
readonly log: Log = {
entries: List<LogEntry>(),
entry: (e: LogEntry) => this.events.log.next(e),
error: (msg: string) => this.events.log.next(LogEntry.error(msg)),
@@ -226,6 +240,7 @@ export class PluginContext {
this.initAnimations();
this.initCustomParamEditors();
this.interactivity = new Interactivity(this);
this.lociLabels = new LociLabelManager(this);
this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`);

View File

@@ -77,10 +77,7 @@ export const DefaultPluginSpec: PluginSpec = {
AnimateAssemblyUnwind,
AnimateUnitsExplode,
AnimateStateInterpolation
],
layout: {
controls: { top: 'none' }
}
]
}
export function createPlugin(target: HTMLElement, spec?: PluginSpec): PluginContext {

View File

@@ -54,7 +54,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
this.events.updated.next();
}
private root: HTMLElement;
private root: HTMLElement | undefined;
private rootState: RootState | undefined = void 0;
private expandedViewport: HTMLMetaElement;
@@ -78,7 +78,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
let body = document.getElementsByTagName('body')[0];
let head = document.getElementsByTagName('head')[0];
if (!body || !head) return;
if (!body || !head || !this.root) return;
if (this.state.isExpanded) {
let children = head.children;

View File

@@ -8,42 +8,42 @@
select, button, input[type=text] {
@extend .msp-form-control;
}
button {
@extend .msp-btn;
@extend .msp-btn-block;
}
}
.msp-control-row {
.msp-control-row {
position: relative;
height: $row-height;
background: $default-background;
margin-top: 1px;
> span {
line-height: $row-height;
display: block;
width: $control-label-width + $control-spacing;
text-align: right;
padding: 0 $control-spacing;
padding: 0 $control-spacing;
color: color-lower-contrast($font-color, 15%);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@include non-selectable;
@include non-selectable;
}
select, button, input[type=text] {
@extend .msp-form-control;
}
button {
@extend .msp-btn;
@extend .msp-btn-block;
}
> div:nth-child(2) {
background: $msp-form-control-background;
position: absolute;
@@ -58,21 +58,21 @@
position: relative;
}
.msp-toggle-button {
.msp-toggle-button {
.msp-icon {
display: inline-block;
margin-right: 6px;
}
> div > button:hover {
border-color: color-increase-contrast($msp-form-control-background, 5%) !important;
border: none;
outline-offset: -1px !important;
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
}
}
.msp-slider {
.msp-slider {
> div:first-child {
position: absolute;
top: 0;
@@ -91,14 +91,14 @@
top: 0;
bottom: 0;
}
input[type=text] {
padding-right: 6px;
padding-left: 4px;
font-size: 80%;
text-align: right;
}
// input[type=range] {
// width: 100%;
// }
@@ -135,14 +135,14 @@
bottom: 0;
font-size: 80%;
}
input[type=text] {
padding-right: 4px;
padding-left: 4px;
font-size: 80%;
text-align: center;
}
// input[type=range] {
// width: 100%;
// }
@@ -154,24 +154,24 @@
margin: 0;
text-align: center;
padding-right: $control-spacing;
padding-left: $control-spacing;
padding-left: $control-spacing;
&:hover {
border-color: color-increase-contrast($msp-form-control-background, 5%) !important;
border: none;
outline-offset: -1px !important;
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
}
}
.msp-color-picker {
position: absolute;
z-index: 100000;
background: $default-background;
border-top: 1px solid $default-background;
padding-bottom: $control-spacing / 2;
width: 100%;
width: 100%;
// input[type=text] {
// background: $msp-form-control-background !important;
// }
@@ -217,7 +217,7 @@
height: 2 * $row-height / 3 !important;
line-height: 2 * $row-height / 3 !important;
font-size: 70% !important;
background: $default-background !important;
background: $default-background !important;
color: color-lower-contrast($font-color, 15%) !important;
}
}
@@ -231,13 +231,13 @@
.msp-control-subgroup {
margin-top: 1px;
.msp-control-row {
margin-left: $control-spacing !important;
> span {
width: $control-label-width !important;
}
> div:nth-child(2) {
left: $control-label-width !important;
}
@@ -254,7 +254,7 @@
width: $control-label-width + $control-spacing;
text-align: left;
background: transparent;
.msp-icon {
line-height: $row-height - 3;
width: $row-height - 1;

View File

@@ -0,0 +1,29 @@
.msp-sequence {
position: absolute;
right: 0;
top: 0;
left: 0;
bottom: 0;
background: $sequence-background;
}
.msp-sequence-select {
float: left;
width: $sequence-select-width;
}
.msp-sequence-wrapper {
word-break: break-word;
padding: $info-vertical-padding $control-spacing $info-vertical-padding $control-spacing;
user-select: none;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
font-size: 90%;
}
.msp-sequence-wrapper {
span {
cursor: pointer;
}
}

View File

@@ -1,12 +1,12 @@
@mixin non-selectable {
-webkit-user-select: none; /* Chrome/Safari */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+ */
/* Rules below not implemented in browsers yet */
-o-user-select: none;
user-select: none;
cursor: default;
}
@@ -16,18 +16,18 @@
}
::-webkit-scrollbar-track {
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
border-radius: 0;
background-color: color-lower-contrast($control-background, 4%);
}
::-webkit-scrollbar-thumb {
border-radius: 0;
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.9);
//-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.9);
background-color: color-lower-contrast($control-background, 8%);
}
@import 'components/controls-base';
@import 'components/controls-base';
@import 'components/controls';
@import 'components/slider';
@import 'components/panel';
@@ -37,6 +37,7 @@
@import 'components/tasks';
@import 'components/viewport';
@import 'components/log';
@import 'components/sequence';
@import 'components/transformer';
@import 'components/toast';
@import 'components/help';

View File

@@ -1,5 +1,5 @@
// measures
// measures
$control-label-width: 110px;
$row-height: 32px;
@@ -28,14 +28,14 @@ $standard-top-height: 2 * $row-height + 1;
// TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Visual' | 'Selection' | 'Action' | 'Behaviour'
// DO NOT CHANGE THESE!!
$entity-color-Root: $default-background;
$entity-color-Data: color-lower-contrast(#95a5a6, 15%);
$entity-color-Selection: color-lower-contrast(#e74c3c, 15%);
$entity-color-Action: color-lower-contrast(#34495e, 10%);
$entity-color-Object: color-lower-contrast(#2ecc71, 10%);
$entity-color-Behaviour: color-lower-contrast(#9b59b6, 10%);
$entity-color-Visual: color-lower-contrast(#3498db, 5%);
$entity-color-Group: color-lower-contrast(#e67e22, 5%);
$entity-color-Root: $default-background;
$entity-color-Data: color-lower-contrast(#95a5a6, 15%);
$entity-color-Selection: color-lower-contrast(#e74c3c, 15%);
$entity-color-Action: color-lower-contrast(#34495e, 10%);
$entity-color-Object: color-lower-contrast(#2ecc71, 10%);
$entity-color-Behaviour: color-lower-contrast(#9b59b6, 10%);
$entity-color-Visual: color-lower-contrast(#3498db, 5%);
$entity-color-Group: color-lower-contrast(#e67e22, 5%);
//////////////////////////////////////////////////
// COLORS and COMPUTED COLORS
@@ -43,8 +43,8 @@ $entity-color-Group: color-lower-contrast(#e67e22, 5%);
$slider-disabledColor: #ccc;
$control-background: color-increase-contrast($default-background, 6.5%);
$border-color: color-increase-contrast($default-background, 15%);
$msp-form-control-background: color-lower-contrast($default-background, 2.5%);
$border-color: color-increase-contrast($default-background, 15%);
$msp-form-control-background: color-lower-contrast($default-background, 2.5%);
// buttons
$msp-btn-link-font-color: $font-color;
@@ -72,7 +72,11 @@ $highlight-info-font-color: $hover-font-color;
$highlight-info-additional-font-color: color-lower-contrast($hover-font-color, 20%);
// entity state
$entity-color-fully-visible: $font-color;
$entity-color-fully-visible: $font-color;
$entity-color-not-visible: color-lower-contrast($font-color, 66%);
$entity-color-partialy-visible: color-lower-contrast($font-color, 33%);
$entity-tag-color: color-lower-contrast($font-color, 20%);
// sequence
$sequence-background: $default-background;
$sequence-select-width: 300px;

View File

@@ -16,6 +16,7 @@ import { PluginCommands } from './command';
import { PluginAnimationManager } from './state/animation/manager';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { UUID } from '../mol-util';
import { Interactivity } from './util/interactivity';
export { PluginState }
class PluginState {
@@ -53,12 +54,11 @@ class PluginState {
camera: p.camera ? {
current: this.plugin.canvas3d.camera.getSnapshot(),
transitionStyle: p.cameraTranstion.name,
transitionDurationInMs: (params && params.cameraTranstion && params.cameraTranstion.name === 'animate' && params.cameraTranstion.params.durationInMs) || void 0
transitionDurationInMs: (params && params.cameraTranstion && params.cameraTranstion.name === 'animate') ? params.cameraTranstion.params.durationInMs : undefined
} : void 0,
cameraSnapshots: p.cameraSnapshots ? this.cameraSnapshots.getStateSnapshot() : void 0,
canvas3d: p.canvas3d ? {
props: this.plugin.canvas3d.props
} : void 0,
canvas3d: p.canvas3d ? { props: this.plugin.canvas3d.props } : void 0,
interactivity: p.interactivity ? { props: this.plugin.interactivity.props } : void 0,
durationInMs: params && params.durationInMs
};
}
@@ -69,7 +69,10 @@ class PluginState {
if (snapshot.behaviour) await this.plugin.runTask(this.behaviorState.setSnapshot(snapshot.behaviour));
if (snapshot.data) await this.plugin.runTask(this.dataState.setSnapshot(snapshot.data));
if (snapshot.canvas3d) {
if (snapshot.canvas3d.props) await PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: snapshot.canvas3d.props || { } });
if (snapshot.canvas3d.props) await PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: snapshot.canvas3d.props });
}
if (snapshot.interactivity) {
if (snapshot.interactivity.props) await PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: snapshot.interactivity.props });
}
if (snapshot.cameraSnapshots) this.cameraSnapshots.setStateSnapshot(snapshot.cameraSnapshots);
if (snapshot.animation) {
@@ -125,6 +128,7 @@ namespace PluginState {
animation: PD.Boolean(true),
startAnimation: PD.Boolean(false),
canvas3d: PD.Boolean(true),
interactivity: PD.Boolean(true),
camera: PD.Boolean(true),
// TODO: make camera snapshots same as the StateSnapshots with "child states?"
cameraSnapshots: PD.Boolean(false),
@@ -153,6 +157,9 @@ namespace PluginState {
canvas3d?: {
props?: Canvas3DProps
},
interactivity?: {
props?: Interactivity.Props
},
durationInMs?: number
}
}

View File

@@ -20,6 +20,7 @@ import { BackgroundTaskProgress } from './task';
import { Viewport, ViewportControls } from './viewport';
import { StateTransform } from '../../mol-state';
import { UpdateTransformControl } from './state/update-transform';
import { SequenceView } from './sequence';
export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
@@ -92,7 +93,7 @@ class Layout extends PluginUIComponent {
<div className={`msp-plugin-content ${layout.isExpanded ? 'msp-layout-expanded' : 'msp-layout-standard msp-layout-standard-outside'}`}>
<div className={this.layoutVisibilityClassName}>
{this.region('main', ViewportWrapper)}
{layout.showControls && controls.top !== 'none' && this.region('top', controls.top)}
{layout.showControls && controls.top !== 'none' && this.region('top', controls.top || SequenceView)}
{layout.showControls && controls.left !== 'none' && this.region('left', controls.left || State)}
{layout.showControls && controls.right !== 'none' && this.region('right', controls.right || ControlsWrapper)}
{layout.showControls && controls.bottom !== 'none' && this.region('bottom', controls.bottom || Log)}

View File

@@ -0,0 +1,206 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react'
import { PluginUIComponent } from './base';
import { StateTreeSpine } from '../../mol-state/tree/spine';
import { PluginStateObject as SO } from '../state/objects';
import { Sequence } from './sequence/sequence';
import { Structure, StructureElement, StructureProperties as SP } from '../../mol-model/structure';
import { SequenceWrapper } from './sequence/util';
import { PolymerSequenceWrapper } from './sequence/polymer';
import { StructureElementSelectionManager } from '../util/structure-element-selection';
import { MarkerAction } from '../../mol-util/marker-action';
import { ParameterControls } from './controls/parameters';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
function opKey(l: StructureElement) {
const ids = SP.unit.pdbx_struct_oper_list_ids(l)
const ncs = SP.unit.struct_ncs_oper_id(l)
const hkl = SP.unit.hkl(l)
const spgrOp = SP.unit.spgrOp(l)
return `${ids.sort().join(',')}|${ncs}|${hkl}|${spgrOp}`
}
function getSequenceWrapper(state: SequenceViewState, structureSelection: StructureElementSelectionManager): SequenceWrapper.Any | undefined {
const { structure, entity, chain, operator } = state
const l = StructureElement.create()
for (let i = 0, il = structure.units.length; i < il; ++i) {
const unit = structure.units[i]
if (unit.polymerElements.length === 0) continue
StructureElement.set(l, unit, unit.elements[0])
if (SP.entity.id(l) !== entity) continue
if (SP.chain.label_asym_id(l) !== chain) continue
if (opKey(l) !== operator) continue
// console.log('new PolymerSequenceWrapper', structureSelection.get(structure))
const sw = new PolymerSequenceWrapper({ structure, unit })
sw.markResidue(structureSelection.get(structure), MarkerAction.Select)
return sw
}
}
function getEntityOptions(structure: Structure) {
const options: [string, string][] = []
const l = StructureElement.create()
const seen = new Set<string>()
structure.units.forEach(unit => {
if (unit.polymerElements.length === 0) return
StructureElement.set(l, unit, unit.elements[0])
const id = SP.entity.id(l)
if (seen.has(id)) return
const label = `${id}: ${SP.entity.pdbx_description(l).join(', ')}`
options.push([ id, label ])
seen.add(id)
})
if (options.length === 0) options.push(['', 'No entities'])
return options
}
function getChainOptions(structure: Structure, entityId: string) {
const options: [string, string][] = []
const l = StructureElement.create()
const seen = new Set<string>()
structure.units.forEach(unit => {
if (unit.polymerElements.length === 0) return
StructureElement.set(l, unit, unit.elements[0])
if (SP.entity.id(l) !== entityId) return
const id = SP.chain.label_asym_id(l)
if (seen.has(id)) return
const label = `${id}: ${SP.chain.auth_asym_id(l)}`
options.push([ id, label ])
seen.add(id)
})
if (options.length === 0) options.push(['', 'No chains'])
return options
}
function getOperatorOptions(structure: Structure, entityId: string, label_asym_id: string) {
const options: [string, string][] = []
const l = StructureElement.create()
const seen = new Set<string>()
structure.units.forEach(unit => {
if (unit.polymerElements.length === 0) return
StructureElement.set(l, unit, unit.elements[0])
if (SP.entity.id(l) !== entityId) return
if (SP.chain.label_asym_id(l) !== label_asym_id) return
const id = opKey(l)
if (seen.has(id)) return
const label = unit.conformation.operator.name
options.push([ id, label ])
seen.add(id)
})
if (options.length === 0) options.push(['', 'No operators'])
return options
}
type SequenceViewState = { structure: Structure, entity: string, chain: string, operator: string }
export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
private spine: StateTreeSpine.Impl
state = { structure: Structure.Empty, entity: '', chain: '', operator: '' }
constructor(props: {}, context?: any) {
super(props, context);
this.spine = new StateTreeSpine.Impl(this.plugin.state.dataState.cells);
}
componentDidMount() {
this.subscribe(this.plugin.state.behavior.currentObject, o => {
const current = this.plugin.state.dataState.cells.get(o.ref)!;
this.spine.current = current
if (!Structure.areParentsEqual(this.state.structure, this.getStructure())) {
this.setState(this.getInitialState())
}
});
this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
const current = this.spine.current;
if (!current || current.sourceRef !== ref || current.state !== state) return;
this.setState(this.getInitialState())
});
}
private getStructure() {
const so = this.spine.getRootOfType(SO.Molecule.Structure)
return (so && so.data) || Structure.Empty
}
private getSequenceWrapper() {
return getSequenceWrapper(this.state, this.plugin.helpers.structureSelection)
}
private getInitialState(): SequenceViewState {
const structure = this.getStructure()
const entity = getEntityOptions(structure)[0][0]
const chain = getChainOptions(structure, entity)[0][0]
const operator = getOperatorOptions(structure, entity, chain)[0][0]
return { structure, entity, chain, operator }
}
private get params() {
const { structure, entity, chain } = this.state
const entityOptions = getEntityOptions(structure)
const chainOptions = getChainOptions(structure, entity)
const operatorOptions = getOperatorOptions(structure, entity, chain)
return {
entity: PD.Select(entityOptions[0][0], entityOptions),
chain: PD.Select(chainOptions[0][0], chainOptions),
operator: PD.Select(operatorOptions[0][0], operatorOptions)
}
}
private setParamProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
const state = { ...this.state }
switch (p.name) {
case 'entity':
state.entity = p.value
state.chain = getChainOptions(state.structure, state.entity)[0][0]
state.operator = getOperatorOptions(state.structure, state.entity, state.chain)[0][0]
break
case 'chain':
state.chain = p.value
state.operator = getOperatorOptions(state.structure, state.entity, state.chain)[0][0]
break
case 'operator':
state.operator = p.value
break
}
this.setState(state)
}
render() {
if (this.state.structure === Structure.Empty) return <div className='msp-sequence'>
<div className='msp-sequence-wrapper'>No structure available</div>
</div>;
const sequenceWrapper = this.getSequenceWrapper()
return <div className='msp-sequence'>
<div className='msp-sequence-select'>
<ParameterControls params={this.params} values={this.state} onChange={this.setParamProps} />
</div>
{sequenceWrapper !== undefined
? <Sequence sequenceWrapper={sequenceWrapper} />
: <div className='msp-sequence-wrapper'>No sequence available</div>}
</div>;
}
}

View File

@@ -0,0 +1,124 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { StructureSelection, StructureQuery, Structure, Queries, StructureProperties as SP, StructureElement, Unit } from '../../../mol-model/structure';
import { SequenceWrapper } from './util';
import { OrderedSet, Interval } from '../../../mol-data/int';
import { Loci } from '../../../mol-model/loci';
import { Sequence } from '../../../mol-model/sequence';
import { MissingResidues } from '../../../mol-model/structure/model/properties/common';
import { ColorNames } from '../../../mol-util/color/tables';
export type StructureUnit = { structure: Structure, unit: Unit }
export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> {
private readonly location: StructureElement
private readonly sequence: Sequence
private readonly missing: MissingResidues
private readonly modelNum: number
private readonly asymId: string
seqId(i: number) {
return this.sequence.offset + i + 1
}
residueLabel(i: number) {
return this.sequence.sequence[i]
}
residueColor(i: number) {
return this.missing.has(this.modelNum, this.asymId, this.seqId(i))
? ColorNames.grey
: ColorNames.black
}
eachResidue(loci: Loci, apply: (interval: Interval) => boolean) {
let changed = false
const { structure, unit } = this.data
if (!StructureElement.isLoci(loci)) return false
if (!Structure.areParentsEquivalent(loci.structure, structure)) return false
const { location } = this
for (const e of loci.elements) {
let rIprev = -1
location.unit = e.unit
const { index: residueIndex } = e.unit.model.atomicHierarchy.residueAtomSegments
OrderedSet.forEach(e.indices, v => {
location.element = e.unit.elements[v]
const rI = residueIndex[location.element]
// avoid checking for the same residue multiple times
if (rI !== rIprev) {
if (SP.unit.id(location) !== unit.id) return
if (apply(getSeqIdInterval(location))) changed = true
rIprev = rI
}
})
}
return changed
}
getLoci(seqId: number) {
const query = createResidueQuery(this.data.unit.id, seqId);
return StructureSelection.toLoci2(StructureQuery.run(query, this.data.structure));
}
constructor(data: StructureUnit) {
const l = StructureElement.create(data.unit, data.unit.elements[0])
const sequence = data.unit.model.sequence.byEntityKey[SP.entity.key(l)].sequence
const markerArray = new Uint8Array(sequence.sequence.length)
super(data, markerArray, sequence.sequence.length)
this.sequence = sequence
this.location = StructureElement.create()
this.missing = data.unit.model.properties.missingResidues
this.modelNum = data.unit.model.modelNum
this.asymId = SP.chain.label_asym_id(l)
}
}
function createResidueQuery(unitId: number, label_seq_id: number) {
return Queries.generators.atoms({
unitTest: ctx => {
return SP.unit.id(ctx.element) === unitId
},
residueTest: ctx => {
if (ctx.element.unit.kind === Unit.Kind.Atomic) {
return SP.residue.label_seq_id(ctx.element) === label_seq_id
} else {
return (
SP.coarse.seq_id_begin(ctx.element) <= label_seq_id &&
SP.coarse.seq_id_end(ctx.element) >= label_seq_id
)
}
}
});
}
/** Zero-indexed */
function getSeqIdInterval(location: StructureElement): Interval {
const { unit, element } = location
const { model } = unit
switch (unit.kind) {
case Unit.Kind.Atomic:
const residueIndex = model.atomicHierarchy.residueAtomSegments.index[element]
const seqId = model.atomicHierarchy.residues.label_seq_id.value(residueIndex)
return Interval.ofSingleton(seqId - 1)
case Unit.Kind.Spheres:
return Interval.ofRange(
model.coarseHierarchy.spheres.seq_id_begin.value(element) - 1,
model.coarseHierarchy.spheres.seq_id_end.value(element) - 1
)
case Unit.Kind.Gaussians:
return Interval.ofRange(
model.coarseHierarchy.gaussians.seq_id_begin.value(element) - 1,
model.coarseHierarchy.gaussians.seq_id_end.value(element) - 1
)
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react'
import { PurePluginUIComponent } from '../base';
import { getButtons, getModifiers } from '../../../mol-util/input/input-observer';
import { Sequence } from './sequence';
import { Color } from '../../../mol-util/color';
export class Residue extends PurePluginUIComponent<{ seqId: number, label: string, parent: Sequence<any>, marker: number, color: Color }> {
mouseEnter = (e: React.MouseEvent) => {
const modifiers = getModifiers(e.nativeEvent)
this.props.parent.highlight(this.props.seqId, modifiers);
}
mouseLeave = () => {
this.props.parent.highlight();
}
mouseDown = (e: React.MouseEvent) => {
const buttons = getButtons(e.nativeEvent)
const modifiers = getModifiers(e.nativeEvent)
this.props.parent.click(this.props.seqId, buttons, modifiers);
e.stopPropagation() // so that `parent.mouseDown` is not called
}
getBackgroundColor() {
// TODO make marker color configurable
if (this.props.marker === 0) return ''
if (this.props.marker % 2 === 0) return 'rgb(51, 255, 25)' // selected
if (this.props.marker === undefined) console.error('unexpected marker value')
return 'rgb(255, 102, 153)' // highlighted
}
render() {
return <span
onMouseEnter={this.mouseEnter}
onMouseLeave={this.mouseLeave}
onMouseDown={this.mouseDown}
style={{
color: Color.toStyle(this.props.color),
backgroundColor: this.getBackgroundColor()
}}>
{this.props.label}
</span>;
}
}

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react'
import { PluginUIComponent } from '../base';
import { Interactivity } from '../../util/interactivity';
import { MarkerAction } from '../../../mol-util/marker-action';
import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../../mol-util/input/input-observer';
import { ValueBox } from '../../../mol-util';
import { Residue } from './residue';
import { SequenceWrapper } from './util';
type SequenceProps = { sequenceWrapper: SequenceWrapper.Any }
type SequenceState = { markerData: ValueBox<Uint8Array> }
function getState(markerData: ValueBox<Uint8Array>) {
return { markerData: ValueBox.withValue(markerData, markerData.value) }
}
// TODO: this is really inefficient and should be done using a canvas.
export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, SequenceState> {
state = {
markerData: ValueBox.create(this.props.sequenceWrapper.markerArray)
}
private setMarkerData(markerData: ValueBox<Uint8Array>) {
this.setState(getState(markerData))
}
private lociHighlightProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
if (changed) this.setMarkerData(this.state.markerData)
}
private lociSelectionProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
if (changed) this.setMarkerData(this.state.markerData)
}
static getDerivedStateFromProps(nextProps: SequenceProps, prevState: SequenceState): SequenceState | null {
if (prevState.markerData.value !== nextProps.sequenceWrapper.markerArray) {
return getState(ValueBox.create(nextProps.sequenceWrapper.markerArray))
}
return null
}
componentDidMount() {
this.plugin.interactivity.lociHighlights.addProvider(this.lociHighlightProvider)
this.plugin.interactivity.lociSelections.addProvider(this.lociSelectionProvider)
}
componentWillUnmount() {
this.plugin.interactivity.lociHighlights.removeProvider(this.lociHighlightProvider)
this.plugin.interactivity.lociSelections.removeProvider(this.lociSelectionProvider)
}
highlight(seqId?: number, modifiers?: ModifiersKeys) {
const ev = { current: Interactivity.Loci.Empty, modifiers }
if (seqId !== undefined) {
const loci = this.props.sequenceWrapper.getLoci(seqId);
if (loci.elements.length > 0) ev.current = { loci };
}
this.plugin.behaviors.interaction.highlight.next(ev)
}
click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
const ev = { current: Interactivity.Loci.Empty, buttons, modifiers }
if (seqId !== undefined) {
const loci = this.props.sequenceWrapper.getLoci(seqId);
if (loci.elements.length > 0) ev.current = { loci };
}
this.plugin.behaviors.interaction.click.next(ev)
}
contextMenu = (e: React.MouseEvent) => {
e.preventDefault()
}
mouseDown = (e: React.MouseEvent) => {
const buttons = getButtons(e.nativeEvent)
const modifiers = getModifiers(e.nativeEvent)
this.click(undefined, buttons, modifiers);
}
render() {
const { markerData } = this.state;
const sw = this.props.sequenceWrapper
const elems: JSX.Element[] = [];
for (let i = 0, il = sw.length; i < il; ++i) {
elems[elems.length] = <Residue
seqId={sw.seqId(i)}
label={sw.residueLabel(i)}
parent={this}
marker={markerData.value[i]}
color={sw.residueColor(i)}
key={i}
/>;
}
return <div
className='msp-sequence-wrapper'
onContextMenu={this.contextMenu}
onMouseDown={this.mouseDown}
>
{elems}
</div>;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Interval } from '../../../mol-data/int';
import { Loci } from '../../../mol-model/loci';
import { MarkerAction, applyMarkerAction } from '../../../mol-util/marker-action';
import { StructureElement } from '../../../mol-model/structure';
import { Color } from '../../../mol-util/color';
export { SequenceWrapper }
abstract class SequenceWrapper<D> {
abstract seqId(i: number): number
abstract residueLabel(i: number): string
abstract residueColor(i: number): Color
abstract eachResidue(loci: Loci, apply: (interval: Interval) => boolean): boolean
abstract getLoci(seqId: number): StructureElement.Loci
markResidue(loci: Loci, action: MarkerAction) {
return this.eachResidue(loci, (i: Interval) => {
return applyMarkerAction(this.markerArray, i, action)
})
}
constructor(readonly data: D, readonly markerArray: Uint8Array, readonly length: number) {
}
}
namespace SequenceWrapper {
export type Any = SequenceWrapper<any>
}

View File

@@ -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>
@@ -109,8 +113,8 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
const canvas3d = this.plugin.canvas3d;
this.subscribe(canvas3d.input.resize, this.handleResize);
this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.canvas3d.click.next(e));
this.subscribe(canvas3d.interaction.highlight, e => this.plugin.behaviors.canvas3d.highlight.next(e));
this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.interaction.click.next(e));
this.subscribe(canvas3d.interaction.highlight, e => this.plugin.behaviors.interaction.highlight.next(e));
this.subscribe(this.plugin.layout.events.updated, () => {
setTimeout(this.handleResize, 50);
});

View File

@@ -0,0 +1,199 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Loci as ModelLoci, EmptyLoci } from '../../mol-model/loci';
import { ModifiersKeys, ButtonsType } from '../../mol-util/input/input-observer';
import { Representation } from '../../mol-repr/representation';
import { StructureElement, Link } 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 { 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 {
export function areEqual(a: Loci, b: Loci) {
return a.repr === b.repr && ModelLoci.areEqual(a.loci, b.loci);
}
export const Empty: Loci = { loci: EmptyLoci };
}
const Granularity = {
'element': (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 Granularity = keyof typeof Granularity
const GranularityOptions = Object.keys(Granularity).map(n => [n, capitalize(n)]) as [Granularity, string][]
export const Params = {
granularity: PD.Select('residue', GranularityOptions),
}
export type Props = PD.Values<typeof Params>
export interface HighlightEvent { current: Loci, modifiers?: ModifiersKeys }
export interface ClickEvent { current: Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
export type LociMarkProvider = (loci: Loci, action: MarkerAction) => void
export abstract class LociMarkManager<MarkEvent extends any> {
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);
}
removeProvider(provider: LociMarkProvider) {
this.providers = this.providers.filter(p => p !== provider);
// TODO clear, then re-apply remaining providers
}
normalizedLoci(interactivityLoci: Loci) {
let { loci, repr } = interactivityLoci
if (this.props.granularity !== 'element' && Link.isLoci(loci)) {
// convert Link.Loci to a StructureElement.Loci so granularity can be applied
loci = Link.toStructureElementLoci(loci)
}
loci = Granularity[this.props.granularity](loci)
if (StructureElement.isLoci(loci) && loci.structure.parent) {
loci = StructureElement.Loci.remap(loci, loci.structure.parent)
}
return { loci, repr }
}
protected mark(current: Loci<ModelLoci>, action: MarkerAction) {
for (let p of this.providers) p(current, action);
}
abstract apply(e: MarkEvent): void
constructor(public readonly ctx: PluginContext, props: Partial<Props> = {}) {
this.sel = ctx.helpers.structureSelection
this.setProps(props)
}
}
export class LociHighlightManager extends LociMarkManager<HighlightEvent> {
private prev: Loci = { loci: EmptyLoci, repr: void 0 };
apply(e: HighlightEvent) {
const { current, modifiers } = e
const normalized: Loci<ModelLoci> = this.normalizedLoci(current)
if (StructureElement.isLoci(normalized.loci)) {
let loci: StructureElement.Loci = normalized.loci;
if (modifiers && modifiers.shift) {
loci = this.sel.tryGetRange(loci) || loci;
}
this.mark(this.prev, MarkerAction.RemoveHighlight);
const toHighlight = { loci, repr: normalized.repr };
this.mark(toHighlight, MarkerAction.Highlight);
this.prev = toHighlight;
} else {
if (!Loci.areEqual(this.prev, normalized)) {
this.mark(this.prev, MarkerAction.RemoveHighlight);
this.mark(normalized, MarkerAction.Highlight);
this.prev = normalized;
}
}
}
constructor(ctx: PluginContext, props: Partial<Props> = {}) {
super(ctx, props)
ctx.behaviors.interaction.highlight.subscribe(e => this.apply(e));
}
}
export class LociSelectionManager extends LociMarkManager<ClickEvent> {
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);
}
}
apply(e: ClickEvent) {
const { current, buttons, modifiers } = e
const normalized: Loci<ModelLoci> = this.normalizedLoci(current)
if (normalized.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(normalized.loci)) {
if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
// select only the current element on Ctrl + Right-Click
const old = this.sel.get(normalized.loci.structure);
this.mark({ loci: old }, MarkerAction.Deselect);
this.sel.set(normalized.loci);
this.mark(normalized, MarkerAction.Select);
} else if (modifiers.control && buttons === ButtonsType.Flag.Primary) {
// toggle current element on Ctrl + Left-Click
this.toggleSel(normalized 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 = normalized.loci;
if (modifiers.shift) {
loci = this.sel.tryGetRange(loci) || loci;
}
this.toggleSel({ loci, repr: normalized.repr });
}
} else {
if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return;
for (let p of this.providers) p(normalized, MarkerAction.Toggle);
}
}
constructor(ctx: PluginContext, props: Partial<Props> = {}) {
super(ctx, props)
ctx.behaviors.interaction.click.subscribe(e => this.apply(e));
}
}
}

View File

@@ -35,6 +35,6 @@ export class LociLabelManager {
}
constructor(public ctx: PluginContext) {
ctx.behaviors.canvas3d.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) }));
ctx.behaviors.interaction.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) }));
}
}

View File

@@ -2,6 +2,7 @@
* Copyright (c) 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 { OrderedSet } from '../../mol-data/int';
@@ -29,25 +30,37 @@ class StructureElementSelectionManager {
return this.entries.get(ref)!;
}
add(loci: StructureElement.Loci): Loci {
const entry = this.getEntry(loci.structure);
if (!entry) return EmptyLoci;
entry.selection = StructureElement.Loci.union(entry.selection, loci);
return entry.selection;
add(loci: Loci): Loci {
if (StructureElement.isLoci(loci)) {
const entry = this.getEntry(loci.structure);
if (entry) {
entry.selection = StructureElement.Loci.union(entry.selection, loci);
return entry.selection;
}
}
return EmptyLoci
}
remove(loci: StructureElement.Loci): Loci {
const entry = this.getEntry(loci.structure);
if (!entry) return EmptyLoci;
entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection;
remove(loci: Loci): Loci {
if (StructureElement.isLoci(loci)) {
const entry = this.getEntry(loci.structure);
if (entry) {
entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection;
}
}
return EmptyLoci
}
set(loci: StructureElement.Loci): Loci {
const entry = this.getEntry(loci.structure);
if (!entry) return EmptyLoci;
entry.selection = loci;
return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection;
set(loci: Loci): Loci {
if (StructureElement.isLoci(loci)) {
const entry = this.getEntry(loci.structure);
if (entry) {
entry.selection = loci;
return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection;
}
}
return EmptyLoci;
}
clear() {
@@ -69,13 +82,18 @@ class StructureElementSelectionManager {
return entry.selection;
}
has(loci: StructureElement.Loci) {
const entry = this.getEntry(loci.structure);
if (!entry) return false;
return StructureElement.Loci.areIntersecting(loci, entry.selection);
has(loci: Loci) {
if (StructureElement.isLoci(loci)) {
const entry = this.getEntry(loci.structure);
if (entry) {
return StructureElement.Loci.areIntersecting(loci, entry.selection);
}
}
return false;
}
tryGetRange(loci: StructureElement.Loci): StructureElement.Loci | undefined {
tryGetRange(loci: Loci): StructureElement.Loci | undefined {
if (!StructureElement.isLoci(loci)) return;
if (loci.elements.length !== 1) return;
const entry = this.getEntry(loci.structure);
if (!entry) return;
@@ -90,31 +108,18 @@ class StructureElementSelectionManager {
}
if (!e) return;
let predIdx = OrderedSet.findPredecessorIndex(e.indices, OrderedSet.min(xs.indices));
if (predIdx === 0) return;
let fst;
if (predIdx < OrderedSet.size(e.indices)) {
fst = OrderedSet.getAt(e.indices, predIdx)
if (fst > OrderedSet.min(xs.indices)) fst = OrderedSet.getAt(e.indices, predIdx - 1) + 1 as StructureElement.UnitIndex;
} else {
fst = 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))
}]);
return tryGetElementRange(entry.selection.structure, e, xs)
}
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;
accumulateInteractiveHighlight(loci: Loci) {
if (StructureElement.isLoci(loci)) {
if (this.prevHighlight) {
this.prevHighlight = StructureElement.Loci.union(this.prevHighlight, loci);
} else {
this.prevHighlight = loci;
}
}
return this.prevHighlight;
}
@@ -162,8 +167,39 @@ function SelectionEntry(s: Structure): SelectionEntry {
};
}
/** remap `selection-entry` to be related to `structure` if possible */
function remapSelectionEntry(e: SelectionEntry, s: Structure): SelectionEntry {
return {
selection: StructureElement.Loci.remap(e.selection, s)
};
}
/**
* Assumes `ref` and `ext` belong to the same unit in the same structure
*/
function tryGetElementRange(structure: Structure, ref: StructureElement.Loci['elements'][0], ext: StructureElement.Loci['elements'][0]) {
const refMin = OrderedSet.min(ref.indices)
const refMax = OrderedSet.max(ref.indices)
const extMin = OrderedSet.min(ext.indices)
const extMax = OrderedSet.max(ext.indices)
let min: number
let max: number
if (refMax < extMin) {
min = refMax + 1
max = extMax
} else if (extMax < refMin) {
min = extMin
max = refMin - 1
} else {
// TODO handle range overlap cases
return
}
return StructureElement.Loci(structure, [{
unit: ref.unit,
indices: OrderedSet.ofRange(min as StructureElement.UnitIndex, max as StructureElement.UnitIndex)
}]);
}

View File

@@ -13,7 +13,7 @@ import { Subject } from 'rxjs';
import { GraphicsRenderObject } from '../mol-gl/render-object';
import { Task } from '../mol-task';
import { PickingId } from '../mol-geo/geometry/picking';
import { MarkerAction } from '../mol-geo/geometry/marker-data';
import { MarkerAction } from '../mol-util/marker-action';
import { Loci as ModelLoci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
import { Overpaint } from '../mol-theme/overpaint';
import { Transparency } from '../mol-theme/transparency';

View File

@@ -14,7 +14,8 @@ import { LocationIterator } from '../../mol-geo/util/location-iterator';
import { VisualUpdateState } from '../util';
import { ShapeGroupColorTheme } from '../../mol-theme/color/shape-group';
import { ShapeGroupSizeTheme } from '../../mol-theme/size/shape-group';
import { createMarkers, MarkerAction } from '../../mol-geo/geometry/marker-data';
import { createMarkers } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
import { ValueCell } from '../../mol-util';
import { createColors } from '../../mol-geo/geometry/color-data';
import { createSizes } from '../../mol-geo/geometry/size-data';

View File

@@ -15,7 +15,7 @@ import { createEmptyTheme, Theme } from '../../mol-theme/theme';
import { Task } from '../../mol-task';
import { PickingId } from '../../mol-geo/geometry/picking';
import { EmptyLoci, Loci } from '../../mol-model/loci';
import { MarkerAction } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => ComplexVisual<P>): StructureRepresentation<P> {
let version = 0

View File

@@ -23,7 +23,7 @@ import { ColorTheme } from '../../mol-theme/color';
import { ValueCell, deepEqual } from '../../mol-util';
import { createSizes } from '../../mol-geo/geometry/size-data';
import { createColors } from '../../mol-geo/geometry/color-data';
import { MarkerAction } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
import { Mat4 } from '../../mol-math/linear-algebra';
import { Overpaint } from '../../mol-theme/overpaint';
import { Transparency } from '../../mol-theme/transparency';

View File

@@ -18,7 +18,7 @@ import { createEmptyTheme, Theme } from '../../mol-theme/theme';
import { Task } from '../../mol-task';
import { PickingId } from '../../mol-geo/geometry/picking';
import { Loci, EmptyLoci, isEmptyLoci } from '../../mol-model/loci';
import { MarkerAction } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
export const UnitsParams = {
...StructureParams,

View File

@@ -19,7 +19,8 @@ import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
import { Interval } from '../../mol-data/int';
import { VisualUpdateState } from '../util';
import { ColorTheme } from '../../mol-theme/color';
import { createMarkers, MarkerAction } from '../../mol-geo/geometry/marker-data';
import { createMarkers } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
import { ValueCell, deepEqual } from '../../mol-util';
import { createSizes } from '../../mol-geo/geometry/size-data';
import { createColors } from '../../mol-geo/geometry/color-data';

View File

@@ -82,37 +82,37 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
detail = 3
radialSegments = 36
linearSegments = 18
resolution = 0.3
resolution = 0.1
break
case 'higher':
detail = 3
radialSegments = 28
linearSegments = 14
resolution = 0.5
resolution = 0.3
break
case 'high':
detail = 2
radialSegments = 20
linearSegments = 10
resolution = 1.0
resolution = 0.5
break
case 'medium':
detail = 1
radialSegments = 12
linearSegments = 8
resolution = 2.0
resolution = 1
break
case 'low':
detail = 0
radialSegments = 8
linearSegments = 3
resolution = 3
resolution = 2
break
case 'lower':
detail = 0
radialSegments = 4
linearSegments = 2
resolution = 5
resolution = 4
break
case 'lowest':
detail = 0

View File

@@ -8,7 +8,7 @@ import { RuntimeContext } from '../mol-task'
import { GraphicsRenderObject } from '../mol-gl/render-object'
import { PickingId } from '../mol-geo/geometry/picking';
import { Loci, isEmptyLoci } from '../mol-model/loci';
import { MarkerAction, applyMarkerAction } from '../mol-geo/geometry/marker-data';
import { MarkerAction, applyMarkerAction } from '../mol-util/marker-action';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { WebGLContext } from '../mol-gl/webgl/context';
import { Theme } from '../mol-theme/theme';
@@ -65,9 +65,7 @@ namespace Visual {
const { tMarker } = renderObject.values
function apply(interval: Interval) {
const start = Interval.start(interval)
const end = Interval.end(interval)
return applyMarkerAction(tMarker.ref.value.array, start, end, action)
return applyMarkerAction(tMarker.ref.value.array, interval, action)
}
const changed = lociApply(loci, apply)

View File

@@ -20,7 +20,7 @@ import { ColorTheme } from '../../mol-theme/color';
import { ValueCell } from '../../mol-util';
import { createSizes } from '../../mol-geo/geometry/size-data';
import { createColors } from '../../mol-geo/geometry/color-data';
import { MarkerAction } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
import { Mat4 } from '../../mol-math/linear-algebra';
import { Overpaint } from '../../mol-theme/overpaint';
import { Transparency } from '../../mol-theme/transparency';

View File

@@ -162,7 +162,7 @@ class State {
return cell && cell.obj;
}
} finally {
this.spine.setCurrent();
this.spine.current = undefined;
if (updated) this.events.changed.next();
this.events.isUpdating.next(false);
@@ -607,7 +607,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
throw new Error(`No suitable parent found for '${currentRef}'`);
}
ctx.spine.setCurrent(current);
ctx.spine.current = current;
const parent = parentCell.obj!;
current.sourceRef = parentCell.transform.ref;

View File

@@ -18,14 +18,14 @@ interface StateTreeSpine {
namespace StateTreeSpine {
export class Impl implements StateTreeSpine {
private current: StateObjectCell | undefined = void 0;
setCurrent(cell?: StateObjectCell) {
this.current = cell;
}
private _current: StateObjectCell | undefined = void 0;
get current() { return this._current; }
set current(cell: StateObjectCell | undefined) { this._current = cell; }
getAncestorOfType<T extends StateObject.Ctor>(t: T): StateObject.From<T> | undefined {
if (!this.current) return void 0;
let cell = this.current;
if (!this._current) return void 0;
let cell = this._current;
while (true) {
cell = this.cells.get(cell.transform.parent)!;
if (!cell.obj) return void 0;
@@ -35,16 +35,16 @@ namespace StateTreeSpine {
}
getRootOfType<T extends StateObject.Ctor>(t: T): StateObject.From<T> | undefined {
if (!this.current) return void 0;
let cell = this.current;
if (!this._current) return void 0;
let cell = this._current; // check current first
let ret: StateObjectCell | undefined = void 0;
while (true) {
cell = this.cells.get(cell.transform.parent)!;
if (!cell.obj) return void 0;
if (cell.obj.type === t.type) {
ret = cell;
}
if (cell.transform.ref === StateTransform.RootRef) return ret ? ret.obj as StateObject.From<T> : void 0;
cell = this.cells.get(cell.transform.parent)!; // assign parent for next check
}
}

View File

@@ -34,8 +34,8 @@ namespace Task {
class Impl<T> implements Task<T> {
readonly id: number;
run(observer?: Progress.Observer, updateRateMs?: number): Promise<T> {
if (observer) return ExecuteObservable(this, observer, updateRateMs as number || 250);
run(observer?: Progress.Observer, updateRateMs = 250): Promise<T> {
if (observer) return ExecuteObservable(this, observer, updateRateMs);
return this.f(SyncRuntimeContext);
}

View File

@@ -10,7 +10,7 @@ import { Vec2 } from '../../mol-math/linear-algebra';
import { BitFlags, noop } from '../../mol-util';
function getButtons(event: MouseEvent | Touch) {
export function getButtons(event: MouseEvent | Touch) {
if (typeof event === 'object') {
if ('buttons' in event) {
return event.buttons
@@ -37,6 +37,15 @@ function getButtons(event: MouseEvent | Touch) {
return 0
}
export function getModifiers(event: MouseEvent | Touch) {
return {
alt: 'altKey' in event ? event.altKey : false,
shift: 'shiftKey' in event ? event.shiftKey : false,
control: 'ctrlKey' in event ? event.ctrlKey : false,
meta: 'metaKey' in event ? event.metaKey : false
}
}
export const DefaultInputObserverProps = {
noScroll: true,
noContextMenu: true,

View File

@@ -0,0 +1,60 @@
import { OrderedSet } from '../mol-data/int';
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
export enum MarkerAction {
Highlight,
RemoveHighlight,
Select,
Deselect,
Toggle,
Clear
}
export function applyMarkerAction(array: Uint8Array, set: OrderedSet, action: MarkerAction) {
let changed = false;
OrderedSet.forEach(set, i => {
let v = array[i];
switch (action) {
case MarkerAction.Highlight:
if (v % 2 === 0) {
v += 1;
}
break;
case MarkerAction.RemoveHighlight:
if (v % 2 !== 0) {
v -= 1;
}
break;
case MarkerAction.Select:
if (v < 2)
v += 2;
// v += 2
break;
case MarkerAction.Deselect:
// if (v >= 2) {
// v -= 2
// }
v = v % 2;
break;
case MarkerAction.Toggle:
if (v >= 2) {
v -= 2;
}
else {
v += 2;
}
break;
case MarkerAction.Clear:
v = 0;
break;
}
changed = array[i] !== v || changed;
array[i] = v;
})
return changed;
}

View File

@@ -323,10 +323,9 @@ export namespace MonadicParser {
return RegExp('^(?:' + re.source + ')', flags(re));
}
export function regexp(re: RegExp, groupNumber?: number) {
export function regexp(re: RegExp, group = 0) {
const anchored = anchoredRegexp(re);
const expected = '' + re;
const group = groupNumber || 0;
return new MonadicParser(function (input, i) {
const match = anchored.exec(input.slice(i));
if (match) {

View File

@@ -293,8 +293,6 @@ export namespace ParamDefinition {
return areEqual(p.params, a, b);
} else if (p.type === 'mapped') {
const u = a as NamedParams, v = b as NamedParams;
if (!u) return !v;
if (!u || !v) return false;
if (u.name !== v.name) return false;
const map = p.map(u.name);
return isParamEqual(map, u.params, v.params);

View File

@@ -9,7 +9,7 @@ import { resizeCanvas } from '../../mol-canvas3d/util';
import { Representation } from '../../mol-repr/representation';
import { Canvas3D } from '../../mol-canvas3d/canvas3d';
import { labelFirst } from '../../mol-theme/label';
import { MarkerAction } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
import { EveryLoci } from '../../mol-model/loci';
import { RuntimeContext, Progress } from '../../mol-task';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';