Compare commits

...

82 Commits

Author SHA1 Message Date
Alexander Rose
3415fe0847 0.7.1-dev.2 2020-05-09 11:54:33 -07:00
Alexander Rose
1569958a29 debug-mode url param, canvas3d console stats 2020-05-09 11:52:09 -07:00
Alexander Rose
3543faa0c2 fix snapshot loading 2020-05-09 11:03:58 -07:00
Alexander Rose
251dbf3877 0.7.1-dev.1 2020-05-08 15:30:57 -07:00
Alexander Rose
32d35efef0 add grid size to volume transform description and incr version 2020-05-08 15:27:39 -07:00
Alexander Rose
8b6428a61d 0.7.0 2020-05-08 10:45:43 -07:00
Alexander Rose
dda43370cf add setProductionMode, setDebugMode exports 2020-05-08 10:42:38 -07:00
Alexander Rose
92c1e979c0 add version to viewer module export 2020-05-08 10:36:26 -07:00
Alexander Rose
ad38a33943 basic support for aligning coarse structures 2020-05-08 10:20:09 -07:00
Alexander Rose
88c276a4c7 inline option for ToggleSelectionModeButton 2020-05-07 16:16:32 -07:00
Alexander Rose
0a3d19235d 0.7.0-dev.21 2020-05-06 20:16:36 -07:00
Alexander Rose
0d90fd1f06 tweak version script
- so the build contains the correct version number
2020-05-06 20:14:52 -07:00
Alexander Rose
02d3274e83 0.7.0-dev.20 2020-05-06 18:49:10 -07:00
Alexander Rose
2531af2b94 use cursor icon for selection mode 2020-05-06 18:02:53 -07:00
Alexander Rose
850328be4e fix coloring of bonds via overpaint 2020-05-06 17:29:55 -07:00
Alexander Rose
f8ce9cbb65 determine type of sequence for alignment 2020-05-06 15:38:51 -07:00
Alexander Rose
2af9d1cabf limit by chains superposition to polymers 2020-05-06 11:32:31 -07:00
Alexander Rose
e8d1737d40 backbone/sidechain query fixes
- handle non-polymer components in polymers
2020-05-06 10:56:01 -07:00
Alexander Rose
0328e93518 interactivity: selectOnly, only deselect for the structure of the given loci 2020-05-06 10:37:11 -07:00
Alexander Rose
8a4ab9bdb9 more selection helper fixes
- use structure from last decorator as reference
- handle that oldObj is not defined for inserts
2020-05-05 22:10:53 -07:00
Alexander Rose
410cdb193d selection manager fixes
- add removed/updated events to substructure-parent-helper
- remap selections
2020-05-05 17:03:04 -07:00
Alexander Rose
a278337b4c Merge branch 'master' of https://github.com/molstar/molstar 2020-05-05 11:08:46 -07:00
Alexander Rose
b1308de0b9 StructureSelectionManager improvements
- remap/clear referenceLoci onUpdate
- remap/clear history onUpdate
- removed unused prevHighlight
- support group-by-structure in modifyHistory
2020-05-05 11:08:05 -07:00
Alexander Rose
9705078970 set writeDepth specifically for points and text geo 2020-05-05 10:12:51 -07:00
Alexander Rose
b1ca98e945 ignore atoms with zero occupancy for bond computation
- assuming they are not actually atoms
2020-05-05 09:54:47 -07:00
David Sehnal
35054eaca9 0.7.0-dev.19 2020-05-05 17:45:21 +02:00
David Sehnal
2747c743c9 fix ElementLocationIterator.advance
- extra element was added if last unit size === 1
2020-05-05 17:41:17 +02:00
Alexander Rose
031d08a8d4 Merge branch 'master' of https://github.com/molstar/molstar 2020-05-04 19:31:09 -07:00
Alexander Rose
7cc6c4a9c8 superposition fixes 2020-05-04 19:30:57 -07:00
David Sehnal
ff27098514 refactored PluginSpec.config 2020-05-05 00:32:43 +02:00
David Sehnal
545cd65066 Merge branch 'master' of https://github.com/molstar/molstar 2020-05-04 22:32:53 +02:00
David Sehnal
84bfc6e7a9 VolumeStreamingControls show "update" button with "reset params" button 2020-05-04 22:31:55 +02:00
David Sehnal
2f71c4c5e4 VolumeStreamingCustomControls bounded ranges for fine grained control 2020-05-04 22:09:28 +02:00
Alexander Rose
1448f7aeb6 added PluginConfig.Structure.SizeThresholds 2020-05-04 13:00:45 -07:00
David Sehnal
79d66a5cfc revert select binding change 2020-05-04 19:42:33 +02:00
Alexander Rose
2ec19ac04c fixed rsrz label 2020-05-04 10:08:48 -07:00
David Sehnal
f62a6d4512 Selection mode: change binding to right-click
- this way the non-selection mode behavior stays the same and the right click adds functionality
2020-05-04 17:48:37 +02:00
David Sehnal
4fbcee3953 Basic Selection mode UI help
- could use some improvement
2020-05-04 17:42:31 +02:00
David Sehnal
12bb283b97 StructureSelectionManager.clear also clears selection history 2020-05-04 17:19:48 +02:00
David Sehnal
13d776c7cb Icon tweaks
- measurement remove
- selection mode
2020-05-04 17:14:40 +02:00
David Sehnal
f45b48c6e1 slider CSS fix 2020-05-04 17:00:02 +02:00
David Sehnal
ff14c94a90 PluginContext.isBusy fix + related UI fix 2020-05-04 12:08:57 +02:00
David Sehnal
0a0ef35b74 CombinedColorControl: RGB input as separate boxes 2020-05-04 11:48:45 +02:00
David Sehnal
e3dc10c085 ObjectListEditor: bugfix 2020-05-04 11:31:30 +02:00
David Sehnal
46113bf3d4 PluginStateAnimation.canApply 2020-05-03 20:43:49 +02:00
David Sehnal
0f3ef61f7d isBusy behavior bugfix
- was causing the animation button not to display if the state was loaded too fast
2020-05-03 13:10:52 +02:00
Alexander Rose
86aae08257 assembly-symmetry: dihedral cage fixes 2020-05-02 12:59:54 -07:00
Alexander Rose
06bf2c39a1 add pdb-dev download bcif encoding 2020-05-02 12:59:31 -07:00
Alexander Rose
66a23bc2a2 alignment fixes 2020-05-02 12:58:32 -07:00
David Sehnal
51e86f1e43 Superposition UI: add "toggle selection" button to help 2020-05-02 13:40:27 +02:00
Alexander Rose
78c70b3f5b structure superposition by chains or by atoms
- superposition by chains can be guided by sequence alignment
- TODO not working with already transformed structures in the general case
2020-05-01 19:08:31 -07:00
Alexander Rose
8f52ffe061 move state/session io warning to snapshot ui 2020-05-01 13:06:25 -07:00
Alexander Rose
e95b91ab84 typo 2020-04-30 15:51:54 -07:00
Alexander Rose
f4dbd66496 Merge branch 'master' of https://github.com/molstar/molstar 2020-04-30 15:49:31 -07:00
Alexander Rose
5895df0499 tweaked elementInstances granularity label 2020-04-30 15:49:16 -07:00
Alexander Rose
6d0d88f3be added sidechain queries 2020-04-30 15:44:41 -07:00
David Sehnal
7e71428cc3 RemoteStateSnapshots: fix unmounted setState 2020-05-01 00:15:22 +02:00
David Sehnal
2e215440f7 Merge branch 'master' of https://github.com/molstar/molstar 2020-05-01 00:09:29 +02:00
David Sehnal
c04fa56c6c fixed expandConnected query, added expand to cov/metallic bonds 2020-05-01 00:09:13 +02:00
Alexander Rose
6c70b5e38f take structure volume into account in getQualityProps for resolution 2020-04-30 15:00:40 -07:00
Alexander Rose
ad9160a4a3 guard against overly high surface resolution 2020-04-30 15:00:07 -07:00
Alexander Rose
c747d3928e always apply marker in shader 2020-04-30 11:34:50 -07:00
David Sehnal
68e8d67054 mol-plugin: PluginState.dispose unregisters behaviors 2020-04-30 18:29:47 +02:00
David Sehnal
9e8fc76d28 StructureSelectionManager: loci events 2020-04-30 16:02:16 +02:00
Alexander Rose
d8970305de cellpack: cleanup
- renamed GetAllAssamblyinOne to StructureFromAssemblies
- removed GetAllAssamblyinOneStructure
2020-04-29 22:21:22 -07:00
Alexander Rose
824675a658 cellpack: split color themes 2020-04-29 21:49:37 -07:00
Alexander Rose
090ad613cc Merge pull request #35 from corredD/forkdev
Forkdev
2020-04-29 20:49:00 -07:00
Alexander Rose
ea35e09c96 focus repr carbon color tweaks 2020-04-29 14:19:45 -07:00
Alexander Rose
d8b9a1a560 basic nw sequence alignment 2020-04-29 13:54:00 -07:00
Alexander Rose
c259f58e63 make carbon color configurable in element symbol color theme 2020-04-29 13:33:31 -07:00
Alexander Rose
9d4c2a1147 support setting pdb/emdb provider in viewer index.html 2020-04-29 13:32:58 -07:00
David Sehnal
f13c3fe38b Viewport resize handling fix 2020-04-29 15:14:35 +02:00
David Sehnal
60409df145 better Viewport resize handling 2020-04-29 15:13:53 +02:00
autin
1d7321cd6f Merge branch 'master' of https://github.com/molstar/molstar into forkdev
# Conflicts:
#	src/extensions/cellpack/property.ts
2020-04-29 10:01:39 +02:00
Alexander Rose
eb68ccbf6b very basic handle-helper 2020-04-28 23:59:54 -07:00
autin
56ea62af71 Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2020-04-28 11:06:27 +02:00
autin
b4808f2909 resampling optional. added the latest models 2020-04-27 12:32:20 +02:00
autin
1a2e9eaa84 Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2020-04-27 09:24:00 +02:00
autin
7e3cca5780 Merge branch 'master' of https://github.com/molstar/molstar into forkdev
# Conflicts:
#	src/extensions/cellpack/model.ts
2020-04-26 11:50:33 +02:00
autin
0b9371527e Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2020-04-25 08:30:19 +02:00
autin
7aee2d805d eslint fix 2020-04-24 15:01:21 +02:00
autin
06f03f399a lipid membrane tiles as assembly support, added ingredient colors if provided. 2020-04-24 14:57:14 +02:00
85 changed files with 2060 additions and 516 deletions

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "0.7.0-dev.18",
"version": "0.7.1-dev.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "0.7.0-dev.18",
"version": "0.7.1-dev.2",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -34,7 +34,8 @@
"model-server-watch": "nodemon --watch lib lib/servers/servers/model/server.js",
"volume-server-test": "node lib/servers/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
"plugin-state": "node lib/servers/servers/plugin-state/index.js",
"preversion": "npm run test && npm run build",
"preversion": "npm run test",
"version": "npm run build",
"postversion": "git push && git push --tags"
},
"files": [

View File

@@ -45,17 +45,24 @@
return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
}
var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
if (debugMode) molstar.setDebugMode(debugMode);
var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase();
var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase();
var viewer = new molstar.Viewer('app', {
layoutShowControls: !hideControls,
viewportShowExpand: false,
pdbProvider: pdbProvider || 'pdbe',
emdbProvider: emdbProvider || 'pdbe',
});
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
var snapshotUrl = getParam('snapshot-url', '[^&]+').trim();
var snapshotUrlType = getParam('snapshot-url-type', '[^&]+').toLowerCase().trim();
var snapshotUrlType = getParam('snapshot-url-type', '[^&]+').toLowerCase().trim() || 'molj';
if (snapshotUrl && snapshotUrlType) viewer.loadSnapshotFromUrl(snapshotUrl, snapshotUrlType);
var structureUrl = getParam('structure-url', '[^&]+').trim();

View File

@@ -23,8 +23,12 @@ import { ObjectKeys } from '../../mol-util/type-helpers';
import { PluginState } from '../../mol-plugin/state';
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
require('mol-plugin-ui/skin/light.scss');
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setProductionMode, setDebugMode } from '../../mol-util/debug';
const Extensions = {
'cellpack': PluginSpec.Behavior(CellPack),
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
@@ -83,18 +87,18 @@ export class Viewer {
...DefaultPluginSpec.components,
remoteState: o.layoutShowRemoteState ? 'default' : 'none',
},
config: DefaultPluginSpec.config
config: [
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
[PluginConfig.State.DefaultServer, o.pluginStateServer],
[PluginConfig.State.CurrentServer, o.pluginStateServer],
[PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
[PluginConfig.Download.DefaultPdbProvider, o.pdbProvider],
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider]
]
};
spec.config?.set(PluginConfig.Viewport.ShowExpand, o.viewportShowExpand);
spec.config?.set(PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode);
spec.config?.set(PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation);
spec.config?.set(PluginConfig.State.DefaultServer, o.pluginStateServer);
spec.config?.set(PluginConfig.State.CurrentServer, o.pluginStateServer);
spec.config?.set(PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer);
spec.config?.set(PluginConfig.Download.DefaultPdbProvider, o.pdbProvider);
spec.config?.set(PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider);
const element = document.getElementById(elementId);
if (!element) throw new Error(`Could not get element with id '${elementId}'`);
this.plugin = createPlugin(element, spec);
@@ -150,7 +154,10 @@ export class Viewer {
source: {
name: 'pdb-dev' as const,
params: {
id: pdbDev,
provider: {
id: pdbDev,
encoding: 'bcif',
},
options: params.source.params.options,
}
}

View File

@@ -6,7 +6,7 @@
import { Mat4 } from '../../mol-math/linear-algebra';
import { QueryContext, StructureSelection } from '../../mol-model/structure';
import { superposeStructures } from '../../mol-model/structure/structure/util/superposition';
import { superpose } from '../../mol-model/structure/structure/util/superposition';
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
import { PluginContext } from '../../mol-plugin/context';
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
@@ -79,7 +79,7 @@ export function dynamicSuperpositionTest(plugin: PluginContext, src: string[], c
const xs = plugin.managers.structure.hierarchy.current.structures;
const selections = xs.map(s => StructureSelection.toLociWithCurrentUnits(query(new QueryContext(s.cell.obj!.data))));
const transforms = superposeStructures(selections);
const transforms = superpose(selections);
await siteVisual(plugin, xs[0].cell, pivot, rest);
for (let i = 1; i < selections.length; i++) {

View File

@@ -249,9 +249,7 @@ class MolStarProteopediaWrapper {
toggleSpin() {
if (!this.plugin.canvas3d) return;
const trackball = this.plugin.canvas3d.props.trackball;
const spinning = trackball.spin;
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
if (!spinning) PluginCommands.Camera.Reset(this.plugin, { });
}
viewport = {

View File

@@ -4,29 +4,28 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ThemeDataContext } from '../../mol-theme/theme';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Color } from '../../mol-util/color';
import { getPalette } from '../../mol-util/color/palette';
import { ColorTheme, LocationColor } from '../../mol-theme/color';
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
import { StructureElement, Bond } from '../../mol-model/structure';
import { Location } from '../../mol-model/location';
import { CellPackInfoProvider } from './property';
import { distinctColors } from '../../mol-util/color/distinct';
import { Hcl } from '../../mol-util/color/spaces/hcl';
import { ThemeDataContext } from '../../../mol-theme/theme';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Color } from '../../../mol-util/color';
import { getPalette } from '../../../mol-util/color/palette';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
import { StructureElement, Bond } from '../../../mol-model/structure';
import { Location } from '../../../mol-model/location';
import { CellPackInfoProvider } from '../property';
import { distinctColors } from '../../../mol-util/color/distinct';
import { Hcl } from '../../../mol-util/color/spaces/hcl';
const DefaultColor = Color(0xCCCCCC);
const Description = 'Gives every model in a CellPack packing a unique color similar to other models in the packing.';
const Description = 'Gives every model in a CellPack packing a unique generated color similar to other models in the packing.';
export const CellPackColorThemeParams = {};
export type CellPackColorThemeParams = typeof CellPackColorThemeParams
export function getCellPackColorThemeParams(ctx: ThemeDataContext) {
return CellPackColorThemeParams; // TODO return copy
export const CellPackGenerateColorThemeParams = {};
export type CellPackGenerateColorThemeParams = typeof CellPackGenerateColorThemeParams
export function getCellPackGenerateColorThemeParams(ctx: ThemeDataContext) {
return CellPackGenerateColorThemeParams; // TODO return copy
}
export function CellPackColorTheme(ctx: ThemeDataContext, props: PD.Values<CellPackColorThemeParams>): ColorTheme<CellPackColorThemeParams> {
export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Values<CellPackGenerateColorThemeParams>): ColorTheme<CellPackGenerateColorThemeParams> {
let color: LocationColor;
let legend: ScaleLegend | TableLegend | undefined;
@@ -34,7 +33,8 @@ export function CellPackColorTheme(ctx: ThemeDataContext, props: PD.Values<CellP
if (ctx.structure && info) {
const colors = distinctColors(info.packingsCount);
const hcl = Hcl.fromColor(Hcl(), colors[info.packingIndex]);
let hcl = Hcl.fromColor(Hcl(), colors[info.packingIndex]);
const hue = [Math.max(0, hcl[0] - 35), Math.min(360, hcl[0] + 35)] as [number, number];
const { models } = ctx.structure.root;
@@ -70,7 +70,7 @@ export function CellPackColorTheme(ctx: ThemeDataContext, props: PD.Values<CellP
}
return {
factory: CellPackColorTheme,
factory: CellPackGenerateColorTheme,
granularity: 'instance',
color,
props,
@@ -79,13 +79,13 @@ export function CellPackColorTheme(ctx: ThemeDataContext, props: PD.Values<CellP
};
}
export const CellPackColorThemeProvider: ColorTheme.Provider<CellPackColorThemeParams, 'cellpack'> = {
name: 'cellpack',
label: 'CellPack',
export const CellPackGenerateColorThemeProvider: ColorTheme.Provider<CellPackGenerateColorThemeParams, 'cellpack-generate'> = {
name: 'cellpack-generate',
label: 'CellPack Generate',
category: ColorTheme.Category.Chain,
factory: CellPackColorTheme,
getParams: getCellPackColorThemeParams,
defaultValues: PD.getDefaultValues(CellPackColorThemeParams),
factory: CellPackGenerateColorTheme,
getParams: getCellPackGenerateColorThemeParams,
defaultValues: PD.getDefaultValues(CellPackGenerateColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => {
return (
!!ctx.structure && ctx.structure.elementCount > 0 &&
@@ -93,4 +93,5 @@ export const CellPackColorThemeProvider: ColorTheme.Provider<CellPackColorThemeP
!!CellPackInfoProvider.get(ctx.structure).value
);
}
};
};

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ThemeDataContext } from '../../../mol-theme/theme';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Color } from '../../../mol-util/color';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
import { StructureElement } from '../../../mol-model/structure';
import { Location } from '../../../mol-model/location';
import { CellPackInfoProvider } from '../property';
const DefaultColor = Color(0xCCCCCC);
const Description = 'Gives every model in a CellPack the color provied in the packing data.';
export const CellPackProvidedColorThemeParams = {};
export type CellPackProvidedColorThemeParams = typeof CellPackProvidedColorThemeParams
export function getCellPackProvidedColorThemeParams(ctx: ThemeDataContext) {
return CellPackProvidedColorThemeParams; // TODO return copy
}
export function CellPackProvidedColorTheme(ctx: ThemeDataContext, props: PD.Values<CellPackProvidedColorThemeParams>): ColorTheme<CellPackProvidedColorThemeParams> {
let color: LocationColor;
let legend: ScaleLegend | TableLegend | undefined;
const info = ctx.structure && CellPackInfoProvider.get(ctx.structure).value;
if (ctx.structure && info?.colors) {
const { models } = ctx.structure.root;
const modelColor = new Map<number, Color>();
for (let i = 0, il = models.length; i < il; ++i) {
const idx = models[i].trajectoryInfo.index;
modelColor.set(models[i].trajectoryInfo.index, info.colors[idx]);
}
color = (location: Location): Color => {
return StructureElement.Location.is(location)
? modelColor.get(location.unit.model.trajectoryInfo.index)!
: DefaultColor;
};
} else {
color = () => DefaultColor;
}
return {
factory: CellPackProvidedColorTheme,
granularity: 'instance',
color,
props,
description: Description,
legend
};
}
export const CellPackProvidedColorThemeProvider: ColorTheme.Provider<CellPackProvidedColorThemeParams, 'cellpack-provided'> = {
name: 'cellpack-provided',
label: 'CellPack Provided',
category: ColorTheme.Category.Chain,
factory: CellPackProvidedColorTheme,
getParams: getCellPackProvidedColorThemeParams,
defaultValues: PD.getDefaultValues(CellPackProvidedColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => {
return (
!!ctx.structure && ctx.structure.elementCount > 0 &&
ctx.structure.models[0].trajectoryInfo.size > 1 &&
!!CellPackInfoProvider.get(ctx.structure).value?.colors
);
}
};

View File

@@ -191,8 +191,14 @@ function GetMiniFrame(points: Vec3[], normals: Vec3[]) {
const rpTmpVec1 = Vec3();
export function getMatFromResamplePoints(points: NumberArray, segmentLength: number) {
const new_points = ResampleControlPoints(points, segmentLength);
export function getMatFromResamplePoints(points: NumberArray, segmentLength: number, resample: boolean) {
let new_points: Vec3[] = [];
if (resample) new_points = ResampleControlPoints(points, segmentLength);
else {
for (let idx = 0; idx < points.length / 3; ++idx){
new_points.push(Vec3.fromArray(Vec3.zero(), points, idx * 3));
}
}
const npoints = new_points.length;
const new_normal = GetSmoothNormals(new_points);
const frames = GetMiniFrame(new_points, new_normal);

View File

@@ -59,6 +59,7 @@ export interface Ingredient {
radii?: [Radii];
/** Number of `curveX` properties in the object where `X` is a 0-indexed number */
nbCurve?: number;
uLength?: number;
/** Curve properties are Vec3[] but that is not expressable in TypeScript */
[curveX: string]: unknown;
/** the orientation in the membrane */
@@ -66,6 +67,8 @@ export interface Ingredient {
/** offset along membrane */
offset?: Vec3;
ingtype?: string;
color?: Vec3;
confidence?: number;
}
export interface IngredientSource {

View File

@@ -5,9 +5,9 @@
*/
import { PluginBehavior } from '../../mol-plugin/behavior';
import { CellPackColorThemeProvider } from './color';
import { LoadCellPackModel } from './model';
import { CellPackGenerateColorThemeProvider } from './color/generate';
import { CellPackProvidedColorThemeProvider } from './color/provided';
export const CellPack = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
name: 'cellpack',
@@ -19,12 +19,14 @@ export const CellPack = PluginBehavior.create<{ autoAttach: boolean, showTooltip
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
register(): void {
this.ctx.state.data.actions.add(LoadCellPackModel);
this.ctx.representation.structure.themes.colorThemeRegistry.add(CellPackColorThemeProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.add(CellPackGenerateColorThemeProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.add(CellPackProvidedColorThemeProvider);
}
unregister() {
this.ctx.state.data.actions.remove(LoadCellPackModel);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(CellPackColorThemeProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(CellPackGenerateColorThemeProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(CellPackProvidedColorThemeProvider);
}
}
});

View File

@@ -17,7 +17,7 @@ import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
import { SymmetryOperator } from '../../mol-math/geometry';
import { Task, RuntimeContext } from '../../mol-task';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl } from './state';
import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies } from './state';
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
import { getMatFromResamplePoints } from './curve';
import { compile } from '../../mol-script/runtime/query/compiler';
@@ -27,6 +27,7 @@ import { Column } from '../../mol-data/db';
import { createModels } from '../../mol-model-formats/structure/basic/parser';
import { CellpackPackingPreset, CellpackMembranePreset } from './preset';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import { readFromFile } from '../../mol-util/data-source';
import { objectForEach } from '../../mol-util/object';
@@ -108,7 +109,7 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
}
let query;
if (source.selection){
const asymIds: string[] = source.selection.replace(' :', '').split(' or');
const asymIds: string[] = source.selection.replace(' ', '').replace(':', '').split('or');
query = MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
@@ -155,11 +156,15 @@ function getResultTransforms(results: Ingredient['results'], legacy: boolean) {
function getCurveTransforms(ingredient: Ingredient) {
const n = ingredient.nbCurve || 0;
const instances: Mat4[] = [];
const segmentLength = ingredient.radii
? (ingredient.radii[0].radii
let segmentLength = 3.4;
if (ingredient.uLength){
segmentLength = ingredient.uLength;
} else if (ingredient.radii){
segmentLength = ingredient.radii[0].radii
? ingredient.radii[0].radii[0] * 2.0
: 3.4)
: 3.4;
: 3.4;
}
let resampling: boolean = false;
for (let i = 0; i < n; ++i) {
const cname = `curve${i}`;
if (!(cname in ingredient)) {
@@ -171,12 +176,17 @@ function getCurveTransforms(ingredient: Ingredient) {
// TODO handle curve with 2 or less points
continue;
}
// test for resampling
let distance: number = Vec3.distance(_points[0], _points[1]);
if (distance >= segmentLength + 2.0) {
console.info(distance);
resampling = true;
}
const points = new Float32Array(_points.length * 3);
for (let i = 0, il = _points.length; i < il; ++i) Vec3.toArray(_points[i], points, i * 3);
const newInstances = getMatFromResamplePoints(points, segmentLength);
const newInstances = getMatFromResamplePoints(points, segmentLength, resampling);
instances.push(...newInstances);
}
return instances;
}
@@ -290,7 +300,6 @@ function getCifCurve(name: string, transforms: Mat4[], model: Model) {
async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredient, transforms: Mat4[], model: Model) {
const cif = getCifCurve(name, transforms, model);
const curveModelTask = Task.create('Curve Model', async ctx => {
const format = MmcifFormat.fromFrame(cif);
const models = await createModels(format.data.db, format, ctx);
@@ -308,10 +317,10 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
const file = ingredientFiles[source.pdb];
if (!file) {
// TODO can these be added to the library?
if (name === 'HIV1_CAhex_0_1_0') return;
if (name === 'HIV1_CAhexCyclophilA_0_1_0') return;
if (name === 'iLDL') return;
if (name === 'peptides') return;
if (name === 'HIV1_CAhex_0_1_0') return; // 1VU4CtoH_hex.pdb
if (name === 'HIV1_CAhexCyclophilA_0_1_0') return; // 1AK4fitTo1VU4hex.pdb
if (name === 'iLDL') return; // EMD-5239
if (name === 'peptides') return; // peptide.pdb
if (name === 'lypoglycane') return;
}
@@ -369,12 +378,20 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
const assets: Asset.Wrapper[] = [];
const trajCache = new TrajectoryCache();
const structures: Structure[] = [];
const colors: Color[] = [];
let skipColors: boolean = false;
for (const iName in ingredients) {
if (ctx.shouldUpdate) await ctx.update(iName);
const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache);
if (ingredientStructure) {
structures.push(ingredientStructure.structure);
assets.push(...ingredientStructure.assets);
const c = ingredients[iName].color;
if (c){
colors.push(Color.fromNormalizedRgb(c[0], c[1], c[2]));
} else {
skipColors = true;
}
}
}
@@ -399,7 +416,7 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
trajectoryInfo.size = il;
trajectoryInfo.index = i;
}
return { structure, assets };
return { structure, assets, colors: skipColors ? undefined : colors };
});
}
@@ -435,11 +452,25 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
break;
}
}
if (!file){
// check for cif directly
const cifileName = `${name}.cif`;
for (const f of params.ingredients.files) {
if (cifileName === f.name) {
file = f;
break;
}
}
}
}
let b = state.build().toRoot();
if (file) {
b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: true, label: file.name }, { state: { isGhost: true } });
if (file.name.endsWith('.cif')) {
b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: false, label: file.name }, { state: { isGhost: true } });
} else if (file.name.endsWith('.bcif')) {
b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: true, label: file.name }, { state: { isGhost: true } });
}
} else {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
@@ -448,12 +479,13 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.StructureFromModel)
.apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
.commit({ revertOnError: true });
const membraneParams = {
representation: params.preset.representation,
};
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
}
@@ -510,7 +542,7 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
representation: params.preset.representation,
};
await CellpackPackingPreset.apply(packing, packingParams, plugin);
if ( packings[i].location === 'surface' ){
if ( packings[i].location === 'surface' && params.membrane){
await loadMembrane(plugin, packings[i].name, state, params);
}
}
@@ -518,18 +550,21 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
const LoadCellPackModelParams = {
source: PD.MappedStatic('id', {
'id': PD.Select('influenza_model1.json', [
['blood_hiv_immature_inside.json', 'blood_hiv_immature_inside'],
['BloodHIV1.0_mixed_fixed_nc1.cpr', 'BloodHIV1.0_mixed_fixed_nc1'],
['HIV-1_0.1.6-8_mixed_radii_pdb.cpr', 'HIV-1_0.1.6-8_mixed_radii_pdb'],
['hiv_lipids.bcif', 'hiv_lipids'],
['influenza_model1.json', 'influenza_model1'],
['ExosomeModel.json', 'ExosomeModel'],
['Mycoplasma1.5_mixed_pdb_fixed.cpr', 'Mycoplasma1.5_mixed_pdb_fixed'],
'id': PD.Select('InfluenzaModel2.json', [
['blood_hiv_immature_inside.json', 'Blood HIV immature'],
['HIV_immature_model.json', 'HIV immature'],
['BloodHIV1.0_mixed_fixed_nc1.cpr', 'Blood HIV'],
['HIV-1_0.1.6-8_mixed_radii_pdb.cpr', 'HIV'],
['influenza_model1.json', 'Influenza envelope'],
['InfluenzaModel2.json', 'Influenza Complete'],
['ExosomeModel.json', 'Exosome Model'],
['Mycoplasma1.5_mixed_pdb_fixed.cpr', 'Mycoplasma simple'],
['MycoplasmaModel.json', 'Mycoplasma WholeCell model'],
] as const, { description: 'Download the model definition with `id` from the server at `baseUrl.`' }),
'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.' }),
}, { options: [['id', 'Id'], ['file', 'File']] }),
baseUrl: PD.Text(DefaultCellPackBaseUrl),
membrane: PD.Boolean(true),
ingredients : PD.Group({
files: PD.FileList({ accept: '.cif,.bcif,.pdb' })
}, { isExpanded: true }),
@@ -545,9 +580,5 @@ export const LoadCellPackModel = StateAction.build({
params: LoadCellPackModelParams,
from: PSO.Root
})(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
if (params.source.name === 'id' && params.source.params === 'hiv_lipids.bcif') {
await loadMembrane(ctx, 'hiv_lipids', state, params);
} else {
await loadPackings(ctx, taskCtx, state, params);
}
await loadPackings(ctx, taskCtx, state, params);
}));

View File

@@ -8,7 +8,9 @@ import { StateObjectRef } from '../../mol-state';
import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../mol-plugin-state/builder/structure/representation-preset';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ColorNames } from '../../mol-util/color/names';
import { CellPackColorThemeProvider } from './color';
import { CellPackGenerateColorThemeProvider } from './color/generate';
import { CellPackInfoProvider } from './property';
import { CellPackProvidedColorThemeProvider } from './color/provided';
export const CellpackPackingPresetParams = {
traceOnly: PD.Boolean(true),
@@ -40,8 +42,10 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
Object.assign(reprProps, { sizeFactor: 2 });
}
const info = structureCell.obj?.data && CellPackInfoProvider.get(structureCell.obj?.data).value;
const color = info?.colors ? CellPackProvidedColorThemeProvider.name : CellPackGenerateColorThemeProvider.name;
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
const color = CellPackColorThemeProvider.name;
const representations = {
polymer: builder.buildRepresentation<any>(update, components.polymer, { type: params.representation, typeParams: { ...typeParams, ...reprProps }, color }, { tag: 'polymer' })
};
@@ -85,6 +89,7 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
};
await update.commit({ revertOnError: true });
return { components, representations };
}
});

View File

@@ -8,15 +8,17 @@ import { CustomStructureProperty } from '../../mol-model-props/common/custom-str
import { Structure } from '../../mol-model/structure';
import { CustomProperty } from '../../mol-model-props/common/custom-property';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Color } from '../../mol-util/color';
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
export type CellPackInfoValue = {
packingsCount: number
packingIndex: number
colors?: Color[]
}
const CellPackInfoParams = {
info: PD.Value<CellPackInfoValue>({ packingsCount: 1, packingIndex: 0 }, { isHidden: true })
info: PD.Value<CellPackInfoValue>({ packingsCount: 1, packingIndex: 0, colors: undefined }, { isHidden: true })
};
type CellPackInfoParams = PD.Values<typeof CellPackInfoParams>

View File

@@ -13,6 +13,8 @@ import { IngredientFiles } from './util';
import { Asset } from '../../mol-util/assets';
import { PluginContext } from '../../mol-plugin/context';
import { CellPackInfoProvider } from './property';
import { Structure, StructureSymmetry, Unit } from '../../mol-model/structure';
import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
export const DefaultCellPackBaseUrl = 'https://mesoscope.scripps.edu/data/cellPACK_data/cellPACK_database_1.1.0/';
@@ -71,10 +73,9 @@ const StructureFromCellpack = PluginStateTransform.BuiltIn({
ingredientFiles[file.name] = file;
}
}
const { structure, assets } = await createStructureFromCellPack(plugin, packing, params.baseUrl, ingredientFiles).runInContext(ctx);
const { structure, assets, colors } = await createStructureFromCellPack(plugin, packing, params.baseUrl, ingredientFiles).runInContext(ctx);
await CellPackInfoProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, structure, {
info: { packingsCount: a.data.packings.length, packingIndex: params.packing }
info: { packingsCount: a.data.packings.length, packingIndex: params.packing, colors }
});
(cache as any).assets = assets;
@@ -94,4 +95,110 @@ const StructureFromCellpack = PluginStateTransform.BuiltIn({
}
}
}
});
});
export { StructureFromAssemblies };
type StructureFromAssemblies = typeof StructureFromAssemblies
const StructureFromAssemblies = PluginStateTransform.BuiltIn({
name: 'Structure from all assemblies',
display: { name: 'Structure from all assemblies' },
from: PSO.Molecule.Model,
to: PSO.Molecule.Structure,
params: {
}
})({
canAutoUpdate({ newParams }) {
return true;
},
apply({ a, params }) {
return Task.create('Build Structure', async ctx => {
// TODO: optimze
// TODO: think of ways how to fast-track changes to this for animations
const model = a.data;
let initial_structure = Structure.ofModel(model);
const structures: Structure[] = [];
let structure: Structure = initial_structure;
// the list of asambly *?
const symmetry = ModelSymmetry.Provider.get(model);
if (symmetry && symmetry.assemblies.length !== 0){
for (const a of symmetry.assemblies) {
const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
structures.push(s);
}
const builder = Structure.Builder({ label: name });
let offsetInvariantId = 0;
for (const s of structures) {
let maxInvariantId = 0;
for (const u of s.units) {
const invariantId = u.invariantId + offsetInvariantId;
if (u.invariantId > maxInvariantId) maxInvariantId = u.invariantId;
builder.addUnit(u.kind, u.model, u.conformation.operator, u.elements, Unit.Trait.None, invariantId);
}
offsetInvariantId += maxInvariantId + 1;
}
structure = builder.getStructure();
for( let i = 0, il = structure.models.length; i < il; ++i) {
const { trajectoryInfo } = structure.models[i];
trajectoryInfo.size = il;
trajectoryInfo.index = i;
}
}
return new PSO.Molecule.Structure(structure, { label: a.label, description: `${a.description}` });
});
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});
// export { GetAllAssamblyinOneStructure };
// type GetAllAssamblyinOneStructure = typeof GetAllAssamblyinOneStructure
// const GetAllAssamblyinOneStructure = PluginStateTransform.BuiltIn({
// name: 'get assambly from structure',
// display: { name: 'get assambly from structure' },
// isDecorator: true,
// from: PSO.Molecule.Structure,
// to: PSO.Molecule.Structure,
// params(a) {
// return { };
// }
// })({
// apply({ a, params }) {
// return Task.create('Build Structure Assemblies', async ctx => {
// // TODO: optimze
// // TODO: think of ways how to fast-track changes to this for animations
// const initial_structure = a.data;
// const structures: Structure[] = [];
// let structure: Structure = initial_structure;
// // the list of asambly *?
// const symmetry = ModelSymmetry.Provider.get(initial_structure.model);
// if (symmetry){
// if (symmetry.assemblies.length !== 0) {
// for (const a of symmetry.assemblies) {
// const s = await StructureSymmetry.buildAssembly(initial_structure, a.id!).runInContext(ctx);
// structures.push(s);
// }
// const builder = Structure.Builder({ label: name });
// let offsetInvariantId = 0;
// for (const s of structures) {
// let maxInvariantId = 0;
// for (const u of s.units) {
// const invariantId = u.invariantId + offsetInvariantId;
// if (u.invariantId > maxInvariantId) maxInvariantId = u.invariantId;
// builder.addUnit(u.kind, u.model, u.conformation.operator, u.elements, Unit.Trait.None, invariantId);
// }
// offsetInvariantId += maxInvariantId + 1;
// }
// structure = builder.getStructure();
// for( let i = 0, il = structure.models.length; i < il; ++i) {
// const { trajectoryInfo } = structure.models[i];
// trajectoryInfo.size = il;
// trajectoryInfo.index = i;
// }
// }
// }
// return new PSO.Molecule.Structure(structure, { label: a.label, description: `${a.description}` });
// });
// }
// });

View File

@@ -186,12 +186,22 @@ const getSymbolCage = memoize1((symbol: string): Cage | undefined => {
if (symbol.startsWith('D') || symbol.startsWith('C')) {
// z axis is prism axis, x/y axes cut through edge midpoints
const fold = parseInt(symbol.substr(1));
let cage: Cage;
if (fold === 2) {
return PrismCage(polygon(4, false));
cage = PrismCage(polygon(4, false));
} else if (fold === 3) {
return WedgeCage();
cage = WedgeCage();
} else if (fold > 3) {
return PrismCage(polygon(fold, false));
cage = PrismCage(polygon(fold, false));
} else {
return;
}
if (fold % 2 === 0) {
return cage;
} else {
const m = Mat4.identity();
Mat4.rotate(m, m, 1 / fold * Math.PI / 2, Vec3.unitZ);
return transformCage(cloneCage(cage), m);
}
} else if (symbol === 'O') {
// x/y/z axes cut through order 4 vertices
@@ -237,7 +247,7 @@ function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.Rota
pair = axes.filter(a => a.order === 2);
} else if (fold >= 3) {
const aN = axes.filter(a => a.order === fold)[0];
const a2 = axes.filter(a => a.order === 2)[0];
const a2 = axes.filter(a => a.order === 2)[1];
pair = [aN, a2];
}
} else if (symbol === 'O') {
@@ -267,7 +277,19 @@ function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.Rota
Vec3.sub(dir, eye, target);
if (Vec3.dot(dir, up) < 0) Vec3.negate(up, up);
Mat4.targetTo(t, eye, target, up);
Mat4.scaleUniformly(t, t, size * getSymbolScale(symbol));
if (symbol.startsWith('D')) {
const { sphere } = structure.lookup3d.boundary;
let sizeXY = (sphere.radius * 2) * 0.8; // fallback for missing extrema
if (Sphere3D.hasExtrema(sphere)) {
const n = Mat3.directionTransform(Mat3(), t);
const dirs = unitCircleDirections.map(d => Vec3.transformMat3(Vec3(), d, n));
sizeXY = getMaxProjectedDistance(sphere.extrema, dirs, sphere.center) * 1.6;
}
Mat4.scale(t, t, Vec3.create(sizeXY, sizeXY, Vec3.distance(aA.start, aA.end) * 0.9));
} else {
Mat4.scaleUniformly(t, t, size * getSymbolScale(symbol));
}
} else {
if (Vec3.dot(Vec3.unitY, Vec3.sub(tmpV, aA.end, aA.start)) === 0) {
Vec3.copy(up, Vec3.unitY);
@@ -283,7 +305,6 @@ function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.Rota
const dirs = unitCircleDirections.map(d => Vec3.transformMat3(Vec3(), d, n));
sizeXY = getMaxProjectedDistance(sphere.extrema, dirs, sphere.center);
}
Mat4.scale(t, t, Vec3.create(sizeXY, sizeXY, size * 0.9));
}
}

View File

@@ -222,12 +222,12 @@ function densityFitLabel(loci: Loci): string | undefined {
if (rsrzSeen.size) {
const rsrzCount = `<small>(${rsrzSeen.size} ${rsrzSeen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`;
const rsrzAvg = rsrzSum / rsrzSeen.size;
summary.push(`Real Space R ${rsrzCount}: ${rsrzAvg.toFixed(2)}`);
summary.push(`Real-Space R Z-score ${rsrzCount}: ${rsrzAvg.toFixed(2)}`);
}
if (rsccSeen.size) {
const rsccCount = `<small>(${rsccSeen.size} ${rsccSeen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`;
const rsccAvg = rsccSum / rsccSeen.size;
summary.push(`Real Space Correlation Coefficient ${rsccCount}: ${rsccAvg.toFixed(2)}`);
summary.push(`Real-Space Correlation Coefficient ${rsccCount}: ${rsccAvg.toFixed(2)}`);
}
if (summary.length) {

View File

@@ -36,6 +36,7 @@ import { Sphere3D } from '../mol-math/geometry';
import { isDebugMode } from '../mol-util/debug';
import { CameraHelperParams } from './helper/camera-helper';
import { produce } from 'immer';
import { HandleHelper, HandleHelperParams } from './helper/handle-helper';
export const Canvas3DParams = {
camera: PD.Group({
@@ -60,7 +61,8 @@ export const Canvas3DParams = {
postprocessing: PD.Group(PostprocessingParams),
renderer: PD.Group(RendererParams),
trackball: PD.Group(TrackballControlsParams),
debug: PD.Group(DebugHelperParams)
debug: PD.Group(DebugHelperParams),
handle: PD.Group(HandleHelperParams),
};
export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
@@ -188,12 +190,13 @@ namespace Canvas3D {
const controls = TrackballControls.create(input, camera, p.trackball);
const renderer = Renderer.create(webgl, p.renderer);
const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
const handleHelper = new HandleHelper(webgl, p.handle);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper, {
const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper, handleHelper, {
cameraHelper: p.camera.helper
});
const pickPass = new PickPass(webgl, renderer, scene, camera, 0.5);
const pickPass = new PickPass(webgl, renderer, scene, camera, handleHelper, 0.5);
const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing);
const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample);
@@ -205,6 +208,7 @@ namespace Canvas3D {
function getLoci(pickingId: PickingId) {
let loci: Loci = EmptyLoci;
let repr: Representation.Any = Representation.Empty;
loci = handleHelper.getLoci(pickingId);
reprRenderObjects.forEach((_, _repr) => {
const _loci = _repr.getLoci(pickingId);
if (!isEmptyLoci(_loci)) {
@@ -224,10 +228,12 @@ namespace Canvas3D {
if (repr) {
changed = repr.mark(loci, action);
} else {
changed = handleHelper.mark(loci, action);
reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
}
if (changed) {
scene.update(void 0, true);
handleHelper.scene.update(void 0, true);
const prevPickDirty = pickPass.pickDirty;
draw(true);
pickPass.pickDirty = prevPickDirty; // marking does not change picking buffers
@@ -356,10 +362,19 @@ namespace Canvas3D {
camera.setState({ radiusMax: scene.boundingSphere.radius }, 0);
reprCount.next(reprRenderObjects.size);
if (isDebugMode) consoleStats();
return true;
}
function consoleStats() {
console.table(scene.renderables.map(r => ({
drawCount: r.values.drawCount.ref.value,
instanceCount: r.values.instanceCount.ref.value,
materialId: r.materialId,
})));
}
function add(repr: Representation.Any) {
registerAutoUpdate(repr);
@@ -378,6 +393,7 @@ namespace Canvas3D {
reprRenderObjects.set(repr, newRO);
scene.update(repr.renderObjects, false);
if (isDebugMode) consoleStats();
}
function remove(repr: Representation.Any) {
@@ -388,6 +404,7 @@ namespace Canvas3D {
renderObjects.forEach(o => scene.remove(o));
reprRenderObjects.delete(repr);
scene.update(repr.renderObjects, false, true);
if (isDebugMode) consoleStats();
}
}
@@ -428,7 +445,8 @@ namespace Canvas3D {
multiSample: { ...multiSample.props },
renderer: { ...renderer.props },
trackball: { ...controls.props },
debug: { ...debugHelper.props }
debug: { ...debugHelper.props },
handle: { ...handleHelper.props },
};
}
@@ -535,11 +553,12 @@ namespace Canvas3D {
if (props.renderer) renderer.setProps(props.renderer);
if (props.trackball) controls.setProps(props.trackball);
if (props.debug) debugHelper.setProps(props.debug);
if (props.handle) handleHelper.setProps(props.handle);
requestDraw(true);
},
getImagePass: (props: Partial<ImageProps> = {}) => {
return new ImagePass(webgl, renderer, scene, camera, debugHelper, props);
return new ImagePass(webgl, renderer, scene, camera, debugHelper, handleHelper, props);
},
get props() {
@@ -563,7 +582,8 @@ namespace Canvas3D {
multiSample: { ...multiSample.props },
renderer: { ...renderer.props },
trackball: { ...controls.props },
debug: { ...debugHelper.props }
debug: { ...debugHelper.props },
handle: { ...handleHelper.props },
};
},
get input() {

View File

@@ -0,0 +1,208 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { WebGLContext } from '../../mol-gl/webgl/context';
import Scene from '../../mol-gl/scene';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
import { Vec3, Mat4, Mat3 } from '../../mol-math/linear-algebra';
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { ColorNames } from '../../mol-util/color/names';
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
import { ValueCell } from '../../mol-util';
import { Sphere3D } from '../../mol-math/geometry';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import produce from 'immer';
import { Shape } from '../../mol-model/shape';
import { PickingId } from '../../mol-geo/geometry/picking';
import { Camera } from '../camera';
import { DataLoci, EmptyLoci, Loci } from '../../mol-model/loci';
import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
import { Visual } from '../../mol-repr/visual';
import { Interval } from '../../mol-data/int';
const HandleParams = {
...Mesh.Params,
alpha: { ...Mesh.Params.alpha, defaultValue: 1 },
ignoreLight: { ...Mesh.Params.ignoreLight, defaultValue: true },
colorX: PD.Color(ColorNames.red, { isEssential: true }),
colorY: PD.Color(ColorNames.green, { isEssential: true }),
colorZ: PD.Color(ColorNames.blue, { isEssential: true }),
scale: PD.Numeric(0.33, { min: 0.1, max: 2, step: 0.1 }, { isEssential: true }),
};
type HandleParams = typeof HandleParams
type HandleProps = PD.Values<HandleParams>
export const HandleHelperParams = {
handle: PD.MappedStatic('off', {
on: PD.Group(HandleParams),
off: PD.Group({})
}, { cycle: true, description: 'Show handle tool' }),
};
export type HandleHelperParams = typeof HandleHelperParams
export type HandleHelperProps = PD.Values<HandleHelperParams>
export class HandleHelper {
scene: Scene
props: HandleHelperProps = {
handle: { name: 'off', params: {} }
}
private renderObject: GraphicsRenderObject | undefined
private _transform = Mat4();
getBoundingSphere(out: Sphere3D, instanceId: number) {
if (this.renderObject) {
Sphere3D.copy(out, this.renderObject.values.invariantBoundingSphere.ref.value);
Mat4.fromArray(this._transform, this.renderObject.values.aTransform.ref.value, instanceId * 16);
Sphere3D.transform(out, out, this._transform);
}
return out;
}
setProps(props: Partial<HandleHelperProps>) {
this.props = produce(this.props, p => {
if (props.handle !== undefined) {
p.handle.name = props.handle.name;
if (props.handle.name === 'on') {
this.scene.clear();
const params = { ...props.handle.params, scale: props.handle.params.scale * this.webgl.pixelRatio };
this.renderObject = createHandleRenderObject(params);
this.scene.add(this.renderObject);
this.scene.commit();
p.handle.params = { ...props.handle.params };
}
}
});
}
get isEnabled() {
return this.props.handle.name === 'on';
}
// TODO could be a lists of position/rotation if we want to show more than one handle tool,
// they would be distingishable by their instanceId
update(camera: Camera, position: Vec3, rotation: Mat3) {
if (!this.renderObject) return;
Mat4.setTranslation(this.renderObject.values.aTransform.ref.value as unknown as Mat4, position);
Mat4.fromMat3(this.renderObject.values.aTransform.ref.value as unknown as Mat4, rotation);
// TODO make invariant to camera scaling by adjusting renderObject transform
ValueCell.update(this.renderObject.values.aTransform, this.renderObject.values.aTransform.ref.value);
this.scene.update([this.renderObject], true);
}
getLoci(pickingId: PickingId) {
const { objectId, groupId, instanceId } = pickingId;
if (!this.renderObject || objectId !== this.renderObject.id) return EmptyLoci;
return HandleLoci(this, groupId, instanceId);
}
private eachGroup = (loci: Loci, apply: (interval: Interval) => boolean): boolean => {
if (!this.renderObject) return false;
if (!isHandleLoci(loci)) return false;
let changed = false;
const groupCount = this.renderObject.values.uGroupCount.ref.value;
const { elements } = loci;
for (const { groupId, instanceId } of elements) {
const idx = instanceId * groupCount + groupId;
if (apply(Interval.ofSingleton(idx))) changed = true;
}
return changed;
}
mark(loci: Loci, action: MarkerAction) {
if (!MarkerActions.is(MarkerActions.Highlighting, action)) return false;
if (!isHandleLoci(loci)) return false;
if (loci.data !== this) return false;
return Visual.mark(this.renderObject, loci, action, this.eachGroup);
}
constructor(private webgl: WebGLContext, props: Partial<HandleHelperProps> = {}) {
this.scene = Scene.create(webgl);
this.setProps(props);
}
}
function createHandleMesh(scale: number, mesh?: Mesh) {
const state = MeshBuilder.createState(512, 256, mesh);
const radius = 0.05 * scale;
const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
state.currentGroup = HandleGroup.TranslateScreenXY;
addSphere(state, Vec3.origin, radius * 3, 2);
state.currentGroup = HandleGroup.TranslateObjectX;
addSphere(state, x, radius, 2);
addCylinder(state, Vec3.origin, x, 1, cylinderProps);
state.currentGroup = HandleGroup.TranslateObjectY;
addSphere(state, y, radius, 2);
addCylinder(state, Vec3.origin, y, 1, cylinderProps);
state.currentGroup = HandleGroup.TranslateObjectZ;
addSphere(state, z, radius, 2);
addCylinder(state, Vec3.origin, z, 1, cylinderProps);
// TODO add more helper geometries for the other HandleGroup options
// TODO add props to create subset of geometries
return MeshBuilder.getMesh(state);
}
export const HandleGroup = {
None: 0,
TranslateScreenXY: 1,
// TranslateScreenZ: 2,
TranslateObjectX: 3,
TranslateObjectY: 4,
TranslateObjectZ: 5,
// TranslateObjectXY: 6,
// TranslateObjectXZ: 7,
// TranslateObjectYZ: 8,
// RotateScreenZ: 9,
// RotateObjectX: 10,
// RotateObjectY: 11,
// RotateObjectZ: 12,
} as const;
function HandleLoci(handleHelper: HandleHelper, groupId: number, instanceId: number) {
return DataLoci('handle', handleHelper, [{ groupId, instanceId }],
(boundingSphere: Sphere3D) => handleHelper.getBoundingSphere(boundingSphere, instanceId),
() => `Handle Helper | Group Id ${groupId} | Instance Id ${instanceId}`);
}
export type HandleLoci = ReturnType<typeof HandleLoci>
export function isHandleLoci(x: Loci): x is HandleLoci {
return x.kind === 'data-loci' && x.tag === 'handle';
}
function getHandleShape(props: HandleProps, shape?: Shape<Mesh>) {
const scale = 10 * props.scale;
const mesh = createHandleMesh(scale, shape?.geometry);
mesh.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale + scale / 4));
const getColor = (groupId: number) => {
switch (groupId) {
case HandleGroup.TranslateObjectX: return props.colorX;
case HandleGroup.TranslateObjectY: return props.colorY;
case HandleGroup.TranslateObjectZ: return props.colorZ;
default: return ColorNames.grey;
}
};
return Shape.create('handle', {}, mesh, getColor, () => 1, () => '');
}
function createHandleRenderObject(props: HandleProps) {
const shape = getHandleShape(props);
return Shape.createRenderObject(shape, props);
}

View File

@@ -13,6 +13,7 @@ import { Texture } from '../../mol-gl/webgl/texture';
import { Camera } from '../camera';
import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { HandleHelper } from '../helper/handle-helper';
export const DrawPassParams = {
cameraHelper: PD.Group(CameraHelperParams)
@@ -29,7 +30,7 @@ export class DrawPass {
private depthTarget: RenderTarget | null
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper, props: Partial<DrawPassProps> = {}) {
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper, private handleHelper: HandleHelper, props: Partial<DrawPassProps> = {}) {
const { gl, extensions, resources } = webgl;
const width = gl.drawingBufferWidth;
const height = gl.drawingBufferHeight;
@@ -89,12 +90,15 @@ export class DrawPass {
}
private renderInternal(variant: 'color' | 'depth', transparentBackground: boolean) {
const { renderer, scene, camera, debugHelper, cameraHelper } = this;
const { renderer, scene, camera, debugHelper, cameraHelper, handleHelper } = this;
renderer.render(scene, camera, variant, true, transparentBackground);
if (debugHelper.isEnabled) {
debugHelper.syncVisibility();
renderer.render(debugHelper.scene, camera, variant, false, transparentBackground);
}
if (handleHelper.isEnabled) {
renderer.render(handleHelper.scene, camera, variant, false, transparentBackground);
}
if (cameraHelper.isEnabled) {
cameraHelper.update(camera);
renderer.render(cameraHelper.scene, cameraHelper.camera, variant, false, transparentBackground);

View File

@@ -15,6 +15,7 @@ import { PostprocessingPass, PostprocessingParams } from './postprocessing';
import { MultiSamplePass, MultiSampleParams } from './multi-sample';
import { Camera } from '../camera';
import { Viewport } from '../camera/util';
import { HandleHelper } from '../helper/handle-helper';
export const ImageParams = {
transparentBackground: PD.Boolean(false),
@@ -40,12 +41,12 @@ export class ImagePass {
get width() { return this._width; }
get height() { return this._height; }
constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, props: Partial<ImageProps>) {
constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, handleHelper: HandleHelper, props: Partial<ImageProps>) {
const p = { ...PD.getDefaultValues(ImageParams), ...props };
this._transparentBackground = p.transparentBackground;
this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper, p.drawPass);
this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper, handleHelper, p.drawPass);
this.postprocessing = new PostprocessingPass(webgl, this._camera, this.drawPass, p.postprocessing);
this.multiSample = new MultiSamplePass(webgl, this._camera, this.drawPass, this.postprocessing, p.multiSample);

View File

@@ -11,6 +11,7 @@ import Scene from '../../mol-gl/scene';
import { PickingId } from '../../mol-geo/geometry/picking';
import { decodeFloatRGB } from '../../mol-util/float-packing';
import { Camera } from '../camera';
import { HandleHelper } from '../helper/handle-helper';
export class PickPass {
pickDirty = true
@@ -27,7 +28,7 @@ export class PickPass {
private pickWidth: number
private pickHeight: number
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private pickBaseScale: number) {
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private handleHelper: HandleHelper, private pickBaseScale: number) {
const { gl } = webgl;
const width = gl.drawingBufferWidth;
const height = gl.drawingBufferHeight;
@@ -65,14 +66,18 @@ export class PickPass {
}
render() {
const { renderer, scene, camera } = this;
const { renderer, scene, camera, handleHelper: { scene: handleScene } } = this;
renderer.setViewport(0, 0, this.pickWidth, this.pickHeight);
this.objectPickTarget.bind();
renderer.render(scene, camera, 'pickObject', true, false);
renderer.render(handleScene, camera, 'pickObject', false, false);
this.instancePickTarget.bind();
renderer.render(scene, camera, 'pickInstance', true, false);
renderer.render(handleScene, camera, 'pickInstance', false, false);
this.groupPickTarget.bind();
renderer.render(scene, camera, 'pickGroup', true, false);
renderer.render(handleScene, camera, 'pickGroup', false, false);
this.pickDirty = false;
}

View File

@@ -207,5 +207,6 @@ export namespace Points {
!props.pointFilledCircle ||
(props.pointFilledCircle && props.pointEdgeBleach === 0)
);
state.writeDepth = state.opaque;
}
}

View File

@@ -283,6 +283,7 @@ export namespace Text {
BaseGeometry.updateRenderableState(state, props);
state.pickable = false;
state.opaque = false;
state.writeDepth = true;
}
}

View File

@@ -1,13 +1,10 @@
export default `
// only mark elements with an alpha above the picking threshold
if (gl_FragColor.a >= uPickingAlphaThreshold) {
float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win
if (marker > 0.1) {
if (intMod(marker, 2.0) > 0.1) {
gl_FragColor.rgb = mix(uHighlightColor, gl_FragColor.rgb, 0.3);
} else {
gl_FragColor.rgb = mix(uSelectColor, gl_FragColor.rgb, 0.3);
}
float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win
if (marker > 0.1) {
if (intMod(marker, 2.0) > 0.1) {
gl_FragColor.rgb = mix(uHighlightColor, gl_FragColor.rgb, 0.3);
} else {
gl_FragColor.rgb = mix(uSelectColor, gl_FragColor.rgb, 0.3);
}
}
`;

View File

@@ -255,11 +255,12 @@ namespace Loci {
};
export type Granularity = keyof typeof Granularity
export const GranularityOptions = ParamDefinition.objectToOptions(Granularity, k => {
if (k.indexOf('Instances') > 0) return [stringToWords(k), 'With Symmetry'];
switch (k) {
case 'element': return'Atom/Coarse Element';
case 'elementInstances': return ['Atom/Coarse Element Instances', 'With Symmetry'];
case 'structure': return'Structure/Shape';
default: return stringToWords(k);
default: return k.indexOf('Instances')
? [stringToWords(k), 'With Symmetry'] : stringToWords(k);
}
});

View File

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { align } from '../alignment';
describe('Alignment', () => {
it('basic', () => {
// 3PQR: Rhodopsin
const seqA = 'MNGTEGPNFYVPFSNKTGVVRSPFEAPQYYLAEPWQFSMLAAYMFLLIMLGFPINFLTLYVTVQHKKLRTPLNYILLNLAVADLFMVFGGFTTTLYTSLHGYFVFGPTGCNLEGFFATLGGEIALWSLVVLAIERYVVVCKPMSNFRFGENHAIMGVAFTWVMALACAAPPLVGWSRYIPEGMQCSCGIDYYTPHEETNNESFVIYMFVVHFIIPLIVIFFCYGQLVFTVKEAAAQQQESATTQKAEKEVTRMVIIMVIAFLICWLPYAGVAFYIFTHQGSDFGPIFMTIPAFFAKTSAVYNPVIYIMMNKQFRNCMVTTLCCGKNPLGDDEASTTVSKTETSQVAPA';
// 3SN6: Endolysin,Beta-2 adrenergic
const seqB = 'DYKDDDDAENLYFQGNIFEMLRIDEGLRLKIYKDTEGYYTIGIGHLLTKSPSLNAAKSELDKAIGRNTNGVITKDEAEKLFNQDVDAAVRGILRNAKLKPVYDSLDAVRRAALINMVFQMGETGVAGFTNSLRMLQQKRWDEAAVNLAKSRWYNQTPNRAKRVITTFRTGTWDAYAADEVWVVGMGIVMSLIVLAIVFGNVLVITAIAKFERLQTVTNYFITSLACADLVMGLAVVPFGAAHILTKTWTFGNFWCEFWTSIDVLCVTASIETLCVIAVDRYFAITSPFKYQSLLTKNKARVIILMVWIVSGLTSFLPIQMHWYRATHQEAINCYAEETCCDFFTNQAYAIASSIVSFYVPLVIMVFVYSRVFQEAKRQLQKIDKSEGRFHVQNLSQVEQDGRTGHGLRRSSKFCLKEHKALKTLGIIMGTFTLCWLPFFIVNIVHVIQDNLIRKEVYILLNWIGYVNSGFNPLIYCRSPDFRIAFQELLCLRRSSLKAYGNGYSSNGNTGEQSG';
const { aliA, aliB, score } = align(seqA, seqB, {
gapPenalty: -11,
gapExtensionPenalty: -1,
substMatrix: 'blosum62'
});
expect(aliA).toEqual('------------------------------------------------------------------------------------------------------------------------------------------------MNGTEGPNFYVPFSNKTGVVRSPFEA---PQYYLAEPWQFSM--LAAYMFLLIMLGFPINFLTLYVTVQHKKLRTPLNYILLNLAVADLFMVFGGFTTTLYTSLH---GYFVFGPTGCNLEGFFATLGGEIALWSLVVLAIERYVVVCKPMS-NFRFGENHAIMGVAFTWVMA-LACAAPPLVGWSRYI-PEGMQC----SCGIDYYTPHEETNNESFVIYMFVVHFIIPLIVIFFCYGQLV----------------FTVKEAAAQQQESATTQ----------KAEKEVTRMVIIMVIAFLICWLPYAGVAFYIFTHQGSDFGPIFMTIPAFFAKTSAVYNPVIYIMMNKQFRNCMVTTLCCGKNPLGDDEASTTVSKTETSQVAPA');
expect(aliB).toEqual('DYKDDDDAENLYFQGNIFEMLRIDEGLRLKIYKDTEGYYTIGIGHLLTKSPSLNAAKSELDKAIGRNTNGVITKDEAEKLFNQDVDAAVRGILRNAKLKPVYDSLDAVRRAALINMVFQMGETGVAGFTNSLRMLQQKRWDEAAVNLAKS-RWYNQTPNRAKRVITTFRTGTWDAYAADEVWVVGMGIVMSLIVLAIVFG---NVLVITAIAKFERLQTVTNYFITSLACADLVM---GLAVVPFGAAHILTKTWTFGNFWCEFWTSIDVLCVTASIETLCVIAVDRYFAITSPFKYQSLLTKNKARVIILMVWIVSGLTSFLPIQMHWYRATHQEAINCYAEETC-CDFFT------NQAYAIASSIVSFYVPLVIMVFVYSRVFQEAKRQLQKIDKSEGRFHVQNLSQVEQDGRTGHGLRRSSKFCLKEHKALKTLGIIMG-TFTLCWLPFF-IVNIVHVIQDNLIRKEVYILLNWIGYVNSGFNPLIY-CRSPDFRIAFQELLCLRRSSL--KAYGNGYSSNGNTGEQSG');
expect(score).toEqual(118);
});
});

View File

@@ -0,0 +1,196 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { SubstitutionMatrix, SubstitutionMatrices, SubstitutionMatrixData } from './substitution-matrix';
const DefaultAlignmentOptions = {
gapPenalty: -11,
gapExtensionPenalty: -1,
substMatrix: 'default' as SubstitutionMatrix | 'default'
};
export type AlignmentOptions = typeof DefaultAlignmentOptions;
export function align(seqA: ArrayLike<string>, seqB: ArrayLike<string>, options: Partial<AlignmentOptions> = {}) {
const o = { ...DefaultAlignmentOptions, ...options };
const alignment = new Alignment(seqA, seqB, o);
alignment.calculate();
return alignment.trace();
}
class Alignment {
readonly gapPenalty: number; readonly gapExtensionPenalty: number
readonly substMatrix: SubstitutionMatrixData | undefined
readonly n: number; readonly m: number
readonly S: number[][] = []; readonly V: number[][] = []; readonly H: number[][] = []
constructor (readonly seqA: ArrayLike<string>, readonly seqB: ArrayLike<string>, options: AlignmentOptions) {
this.gapPenalty = options.gapPenalty;
this.gapExtensionPenalty = options.gapExtensionPenalty;
this.substMatrix = options.substMatrix === 'default' ? undefined : SubstitutionMatrices[options.substMatrix];
this.n = this.seqA.length;
this.m = this.seqB.length;
}
private initMatrices () {
const { n, m, gapPenalty, S, V, H } = this;
for (let i = 0; i <= n; ++i) {
S[i] = [], V[i] = [], H[i] = [];
for (let j = 0; j <= m; ++j) {
S[i][j] = 0, V[i][j] = 0, H[i][j] = 0;
}
}
for (let i = 0; i <= n; ++i) {
S[i][0] = gapPenalty;
H[i][0] = -Infinity;
}
for (let j = 0; j <= m; ++j) {
S[0][j] = gapPenalty;
V[0][j] = -Infinity;
}
S[0][0] = 0;
}
private makeScoreFn () {
const { seqA, seqB, substMatrix } = this;
if (substMatrix) {
return function score (i: number, j: number) {
const cA = seqA[i];
const cB = seqB[j];
return substMatrix[cA]?.[cB] ?? -4;
};
} else {
return function scoreNoSubstMat (i: number, j: number) {
const cA = seqA[i];
const cB = seqB[j];
return cA === cB ? 5 : -3;
};
}
}
calculate () {
this.initMatrices();
const scoreFn = this.makeScoreFn();
const { V, H, S, n, m, gapExtensionPenalty, gapPenalty } = this;
let Vi1, Si1, Vi, Hi, Si;
for (let i = 1; i <= n; ++i) {
Si1 = S[i - 1], Vi1 = V[i - 1];
Vi = V[i], Hi = H[i], Si = S[i];
for (let j = 1; j <= m; ++j) {
Vi[j] = Math.max(
Si1[j] + gapPenalty,
Vi1[j] + gapExtensionPenalty
);
Hi[j] = Math.max(
Si[j - 1] + gapPenalty,
Hi[j - 1] + gapExtensionPenalty
);
Si[j] = Math.max(
Si1[j - 1] + scoreFn(i - 1, j - 1), // match
Vi[j], // del
Hi[j] // ins
);
}
}
}
trace (): { aliA: ArrayLike<string>, aliB: ArrayLike<string>, score: number } {
const scoreFn = this.makeScoreFn();
const { V, H, S, seqA, seqB, gapExtensionPenalty, gapPenalty } = this;
let i = this.n;
let j = this.m;
let mat: 'S' | 'V' | 'H';
let score: number;
let aliA = '';
let aliB = '';
if (S[i][j] >= V[i][j]) {
mat = 'S';
score = S[i][j];
} else if (V[i][j] >= H[i][j]) {
mat = 'V';
score = V[i][j];
} else {
mat = 'H';
score = H[i][j];
}
while (i > 0 && j > 0) {
if (mat === 'S') {
if (S[i][j] === S[i - 1][j - 1] + scoreFn(i - 1, j - 1)) {
aliA = seqA[i - 1] + aliA;
aliB = seqB[j - 1] + aliB;
--i;
--j;
mat = 'S';
} else if (S[i][j] === V[i][j]) {
mat = 'V';
} else if (S[i][j] === H[i][j]) {
mat = 'H';
} else {
--i;
--j;
}
} else if (mat === 'V') {
if (V[i][j] === V[i - 1][j] + gapExtensionPenalty) {
aliA = seqA[i - 1] + aliA;
aliB = '-' + aliB;
--i;
mat = 'V';
} else if (V[i][j] === S[i - 1][j] + gapPenalty) {
aliA = seqA[i - 1] + aliA;
aliB = '-' + aliB;
--i;
mat = 'S';
} else {
--i;
}
} else if (mat === 'H') {
if (H[i][j] === H[i][j - 1] + gapExtensionPenalty) {
aliA = '-' + aliA;
aliB = seqB[j - 1] + aliB;
--j;
mat = 'H';
} else if (H[i][j] === S[i][j - 1] + gapPenalty) {
aliA = '-' + aliA;
aliB = seqB[j - 1] + aliB;
--j;
mat = 'S';
} else {
--j;
}
}
}
while (i > 0) {
aliA = seqA[i - 1] + aliA;
aliB = '-' + aliB;
--i;
}
while (j > 0) {
aliA = '-' + aliA;
aliB = seqB[j - 1] + aliB;
--j;
}
return { aliA, aliB, score };
}
}

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { StructureElement, Unit } from '../../structure/structure';
import { AlignmentOptions, align } from './alignment';
import { OrderedSet } from '../../../mol-data/int';
export { AlignSequences };
namespace AlignSequences {
export type Input = {
a: StructureElement.Loci.Element,
b: StructureElement.Loci.Element
}
/** `a` and `b` contain matching pairs, i.e. `a.indices[0]` aligns with `b.indices[0]` */
export type Result = {
a: StructureElement.Loci.Element,
b: StructureElement.Loci.Element,
score: number
}
function createSeqIdIndicesMap(element: StructureElement.Loci.Element) {
const seqIds = new Map<number, StructureElement.UnitIndex[]>();
if (Unit.isAtomic(element.unit)) {
const { label_seq_id } = element.unit.model.atomicHierarchy.residues;
const { residueIndex } = element.unit;
for (let i = 0, il = OrderedSet.size(element.indices); i < il; ++i) {
const uI = OrderedSet.getAt(element.indices, i);
const eI = element.unit.elements[uI];
const seqId = label_seq_id.value(residueIndex[eI]);
if (seqIds.has(seqId)) seqIds.get(seqId)!.push(uI);
else seqIds.set(seqId, [uI]);
}
} else if (Unit.isCoarse(element.unit)) {
const { seq_id_begin } = Unit.isSpheres(element.unit)
? element.unit.model.coarseHierarchy.spheres
: element.unit.model.coarseHierarchy.gaussians;
for (let i = 0, il = OrderedSet.size(element.indices); i < il; ++i) {
const uI = OrderedSet.getAt(element.indices, i);
const eI = element.unit.elements[uI];
const seqId = seq_id_begin.value(eI);
seqIds.set(seqId, [uI]);
}
}
return seqIds;
}
export function compute(input: Input, options: Partial<AlignmentOptions> = {}): Result {
const seqA = getSequence(input.a.unit);
const seqB = getSequence(input.b.unit);
const seqIdIndicesA = createSeqIdIndicesMap(input.a);
const seqIdIndicesB = createSeqIdIndicesMap(input.b);
const indicesA: StructureElement.UnitIndex[] = [];
const indicesB: StructureElement.UnitIndex[] = [];
const { aliA, aliB, score } = align(seqA.code.toArray(), seqB.code.toArray(), options);
let seqIdxA = 0, seqIdxB = 0;
for (let i = 0, il = aliA.length; i < il; ++i) {
if (aliA[i] === '-' || aliB[i] === '-') {
if (aliA[i] !== '-') seqIdxA += 1;
if (aliB[i] !== '-') seqIdxB += 1;
continue;
}
const seqIdA = seqA.seqId.value(seqIdxA);
const seqIdB = seqB.seqId.value(seqIdxB);
if (seqIdIndicesA.has(seqIdA) && seqIdIndicesB.has(seqIdB)) {
const iA = seqIdIndicesA.get(seqIdA)!;
const iB = seqIdIndicesB.get(seqIdB)!;
// use min length to guard against alternate locations
for (let j = 0, jl = Math.min(iA.length, iB.length); j < jl; ++j) {
indicesA.push(iA[j]);
indicesB.push(iB[j]);
}
}
seqIdxA += 1, seqIdxB += 1;
}
const outA = OrderedSet.intersect(OrderedSet.ofSortedArray(indicesA), input.a.indices);
const outB = OrderedSet.intersect(OrderedSet.ofSortedArray(indicesB), input.b.indices);
return {
a: { unit: input.a.unit, indices: outA },
b: { unit: input.b.unit, indices: outB },
score
};
}
}
function entityKey(unit: Unit) {
switch (unit.kind) {
case Unit.Kind.Atomic:
return unit.model.atomicHierarchy.index.getEntityFromChain(unit.chainIndex[unit.elements[0]]);
case Unit.Kind.Spheres:
return unit.model.coarseHierarchy.spheres.entityKey[unit.elements[0]];
case Unit.Kind.Gaussians:
return unit.model.coarseHierarchy.gaussians.entityKey[unit.elements[0]];
}
}
function getSequence(unit: Unit) {
return unit.model.sequence.byEntityKey[entityKey(unit)].sequence;
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mutable } from '../../../mol-util/type-helpers';
const aminoacidsX = 'ACDEFGHIKLMNPQRSTVWY';
const aminoacids = 'ARNDCQEGHILKMFPSTWYVBZX';
const blosum62x = [
[4, 0, -2, -1, -2, 0, -2, -1, -1, -1, -1, -2, -1, -1, -1, 1, 0, 0, -3, -2], // A
[0, 9, -3, -4, -2, -3, -3, -1, -3, -1, -1, -3, -3, -3, -3, -1, -1, -1, -2, -2], // C
[-2, -3, 6, 2, -3, -1, -1, -3, -1, -4, -3, 1, -1, 0, -2, 0, -1, -3, -4, -3], // D
[-1, -4, 2, 5, -3, -2, 0, -3, 1, -3, -2, 0, -1, 2, 0, 0, -1, -2, -3, -2], // E
[-2, -2, -3, -3, 6, -3, -1, 0, -3, 0, 0, -3, -4, -3, -3, -2, -2, -1, 1, 3], // F
[0, -3, -1, -2, -3, 6, -2, -4, -2, -4, -3, 0, -2, -2, -2, 0, -2, -3, -2, -3], // G
[-2, -3, -1, 0, -1, -2, 8, -3, -1, -3, -2, 1, -2, 0, 0, -1, -2, -3, -2, 2], // H
[-1, -1, -3, -3, 0, -4, -3, 4, -3, 2, 1, -3, -3, -3, -3, -2, -1, 3, -3, -1], // I
[-1, -3, -1, 1, -3, -2, -1, -3, 5, -2, -1, 0, -1, 1, 2, 0, -1, -2, -3, -2], // K
[-1, -1, -4, -3, 0, -4, -3, 2, -2, 4, 2, -3, -3, -2, -2, -2, -1, 1, -2, -1], // L
[-1, -1, -3, -2, 0, -3, -2, 1, -1, 2, 5, -2, -2, 0, -1, -1, -1, 1, -1, -1], // M
[-2, -3, 1, 0, -3, 0, 1, -3, 0, -3, -2, 6, -2, 0, 0, 1, 0, -3, -4, -2], // N
[-1, -3, -1, -1, -4, -2, -2, -3, -1, -3, -2, -2, 7, -1, -2, -1, -1, -2, -4, -3], // P
[-1, -3, 0, 2, -3, -2, 0, -3, 1, -2, 0, 0, -1, 5, 1, 0, -1, -2, -2, -1], // Q
[-1, -3, -2, 0, -3, -2, 0, -3, 2, -2, -1, 0, -2, 1, 5, -1, -1, -3, -3, -2], // R
[1, -1, 0, 0, -2, 0, -1, -2, 0, -2, -1, 1, -1, 0, -1, 4, 1, -2, -3, -2], // S
[0, -1, -1, -1, -2, -2, -2, -1, -1, -1, -1, 0, -1, -1, -1, 1, 5, 0, -2, -2], // T
[0, -1, -3, -2, -1, -3, -3, 3, -2, 1, 1, -3, -2, -2, -3, -2, 0, 4, -3, -1], // V
[-3, -2, -4, -3, 1, -2, -2, -3, -3, -2, -1, -4, -4, -2, -3, -3, -2, -3, 11, 2], // W
[-2, -2, -3, -2, 3, -3, 2, -1, -2, -1, -1, -2, -3, -1, -2, -2, -2, -1, 2, 7] // Y
];
const blosum62 = [
// A R N D C Q E G H I L K M F P S T W Y V B Z X
[4, -1, -2, -2, 0, -1, -1, 0, -2, -1, -1, -1, -1, -2, -1, 1, 0, -3, -2, 0, -2, -1, 0], // A
[-1, 5, 0, -2, -3, 1, 0, -2, 0, -3, -2, 2, -1, -3, -2, -1, -1, -3, -2, -3, -1, 0, -1], // R
[-2, 0, 6, 1, -3, 0, 0, 0, 1, -3, -3, 0, -2, -3, -2, 1, 0, -4, -2, -3, 3, 0, -1], // N
[-2, -2, 1, 6, -3, 0, 2, -1, -1, -3, -4, -1, -3, -3, -1, 0, -1, -4, -3, -3, 4, 1, -1], // D
[0, -3, -3, -3, 9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1, -2, -2, -1, -3, -3, -2], // C
[-1, 1, 0, 0, -3, 5, 2, -2, 0, -3, -2, 1, 0, -3, -1, 0, -1, -2, -1, -2, 0, 3, -1], // Q
[-1, 0, 0, 2, -4, 2, 5, -2, 0, -3, -3, 1, -2, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1], // E
[0, -2, 0, -1, -3, -2, -2, 6, -2, -4, -4, -2, -3, -3, -2, 0, -2, -2, -3, -3, -1, -2, -1], // G
[-2, 0, 1, -1, -3, 0, 0, -2, 8, -3, -3, -1, -2, -1, -2, -1, -2, -2, 2, -3, 0, 0, -1], // H
[-1, -3, -3, -3, -1, -3, -3, -4, -3, 4, 2, -3, 1, 0, -3, -2, -1, -3, -1, 3, -3, -3, -1], // I
[-1, -2, -3, -4, -1, -2, -3, -4, -3, 2, 4, -2, 2, 0, -3, -2, -1, -2, -1, 1, -4, -3, -1], // L
[-1, 2, 0, -1, -3, 1, 1, -2, -1, -3, -2, 5, -1, -3, -1, 0, -1, -3, -2, -2, 0, 1, -1], // K
[-1, -1, -2, -3, -1, 0, -2, -3, -2, 1, 2, -1, 5, 0, -2, -1, -1, -1, -1, 1, -3, -1, -1], // M
[-2, -3, -3, -3, -2, -3, -3, -3, -1, 0, 0, -3, 0, 6, -4, -2, -2, 1, 3, -1, -3, -3, -1], // F
[-1, -2, -2, -1, -3, -1, -1, -2, -2, -3, -3, -1, -2, -4, 7, -1, -1, -4, -3, -2, -2, -1, -2], // P
[1, -1, 1, 0, -1, 0, 0, 0, -1, -2, -2, 0, -1, -2, -1, 4, 1, -3, -2, -2, 0, 0, 0], // S
[0, -1, 0, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -2, -1, 1, 5, -2, -2, 0, -1, -1, 0], // T
[-3, -3, -4, -4, -2, -2, -3, -2, -2, -3, -2, -3, -1, 1, -4, -3, -2, 11, 2, -3, -4, -3, -2], // W
[-2, -2, -2, -3, -2, -1, -2, -3, 2, -1, -1, -2, -1, 3, -3, -2, -2, 2, 7, -1, -3, -2, -1], // Y
[0, -3, -3, -3, -1, -2, -2, -3, -3, 3, 1, -2, 1, -1, -2, -2, 0, -3, -1, 4, -3, -2, -1], // V
[-2, -1, 3, 4, -3, 0, 1, -1, 0, -3, -4, 0, -3, -3, -2, 0, -1, -4, -3, -3, 4, 1, -1], // B
[-1, 0, 0, 1, -3, 3, 4, -2, 0, -3, -3, 1, -1, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1], // Z
[0, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, 0, 0, -2, -1, -1, -1, -1, -1] // X
];
export type SubstitutionMatrixData = Readonly<{ [k: string]: Readonly<{ [k: string]: number }> }>;
function prepareMatrix (cellNames: string, mat: number[][]): SubstitutionMatrixData {
let j: number;
let i = 0;
const matDict: Mutable<SubstitutionMatrixData> = {};
mat.forEach(row => {
j = 0;
const rowDict: { [k: string]: number } = {};
row.forEach(elm => rowDict[cellNames[j++]] = elm);
matDict[cellNames[i++]] = rowDict;
});
return matDict;
}
export const SubstitutionMatrices = (() => ({
blosum62: prepareMatrix(aminoacids, blosum62),
blosum62x: prepareMatrix(aminoacidsX, blosum62x)
}))();
export type SubstitutionMatrix = keyof typeof SubstitutionMatrices;

View File

@@ -9,7 +9,6 @@ import { AminoAlphabet, NuclecicAlphabet, getProteinOneLetterCode, getRnaOneLett
import { Column } from '../../mol-data/db';
// TODO add mapping support to other sequence spaces, e.g. uniprot
// TODO sequence alignment (take NGL code as starting point)
type Sequence = Sequence.Protein | Sequence.DNA | Sequence.RNA | Sequence.Generic

View File

@@ -340,9 +340,6 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
const interBonds = inputStructure.interUnitBonds;
const builder = new StructureUniqueSubsetBuilder(inputStructure);
// Note: each bond is visited twice so that bond.atom-a and bond.atom-b both get the "swapped values"
const visitedSourceUnits = new Set<number>();
const atomicBond = ctx.atomicBond;
// Process intra unit bonds
@@ -394,7 +391,6 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
// Process inter unit bonds
for (const bondedUnit of interBonds.getConnectedUnits(inputUnitA)) {
if (visitedSourceUnits.has(bondedUnit.unitB.id)) continue;
const currentUnitB = structure.unitMap.get(bondedUnit.unitB.id);
for (const aI of bondedUnit.connectedIndices) {
@@ -422,8 +418,6 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
}
}
}
visitedSourceUnits.add(inputUnitA.id);
}
return builder.getStructure();

View File

@@ -956,6 +956,9 @@ namespace Structure {
this.current.unit = this.structure.units[this.unitIndex];
this.elements = this.current.unit.elements;
this.maxIdx = this.elements.length - 1;
if (this.maxIdx === 0) {
this.hasNext = this.unitIndex + 1 < this.structure.units.length;
}
}
constructor(private structure: Structure) {
@@ -1057,14 +1060,19 @@ namespace Structure {
//
export const DefaultSizeThresholds = {
/** Must be lower to be small */
smallResidueCount: 10,
/** Must be lower to be medium */
mediumResidueCount: 3000,
/** large ribosomes like 4UG0 should still be `large` */
/** Must be lower to be large (big ribosomes like 4UG0 should still be `large`) */
largeResidueCount: 20000,
/**
* Structures above `largeResidueCount` are consider huge when they have
* a `highSymmetryUnitCount` or gigantic when not
*/
highSymmetryUnitCount: 10,
/** Fiber-like structure are consider small when below this */
fiberResidueCount: 15,
residueCountFactor: 1
};
export type SizeThresholds = typeof DefaultSizeThresholds
@@ -1094,9 +1102,13 @@ namespace Structure {
export enum Size { Small, Medium, Large, Huge, Gigantic }
export function getSize(structure: Structure, thresholds: Partial<SizeThresholds> = {}): Size {
/**
* @param residueCountFactor - modifies the threshold counts, useful when estimating
* the size of a structure comprised of multiple models
*/
export function getSize(structure: Structure, thresholds: Partial<SizeThresholds> = {}, residueCountFactor = 1): Size {
const t = { ...DefaultSizeThresholds, ...thresholds };
if (structure.polymerResidueCount >= t.largeResidueCount * t.residueCountFactor) {
if (structure.polymerResidueCount >= t.largeResidueCount * residueCountFactor) {
if (hasHighSymmetry(structure, t)) {
return Size.Huge;
} else {
@@ -1104,9 +1116,9 @@ namespace Structure {
}
} else if (isFiberLike(structure, t)) {
return Size.Small;
} else if (structure.polymerResidueCount < t.smallResidueCount * t.residueCountFactor) {
} else if (structure.polymerResidueCount < t.smallResidueCount * residueCountFactor) {
return Size.Small;
} else if (structure.polymerResidueCount < t.mediumResidueCount * t.residueCountFactor) {
} else if (structure.polymerResidueCount < t.mediumResidueCount * residueCountFactor) {
return Size.Medium;
} else {
return Size.Large;

View File

@@ -102,6 +102,10 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
if (added) continue;
}
// ignore atoms with zero occupancy (assuming they are not actually atoms)
const occA = occupancyA.value(aI);
if (hasOccupancy && occA === 0) continue;
const { indices, count, squaredDistances } = lookup3d.find(imageA[0], imageA[1], imageA[2], MAX_RADIUS);
if (count === 0) continue;
@@ -112,7 +116,6 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
const metalA = MetalsSet.has(aeI);
const atomIdA = label_atom_idA.value(aI);
const compIdA = label_comp_idA.value(residueIndexA[aI]);
const occA = occupancyA.value(aI);
for (let ni = 0; ni < count; ni++) {
const _bI = indices[ni] as StructureElement.UnitIndex;

View File

@@ -39,6 +39,7 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
const atomCount = unit.elements.length;
const { elements: atoms, residueIndex, chainIndex } = unit;
const { type_symbol, label_atom_id, label_alt_id } = unit.model.atomicHierarchy.atoms;
const { occupancy } = unit.model.atomicConformation;
const { label_comp_id, label_seq_id } = unit.model.atomicHierarchy.residues;
const { index } = unit.model.atomicHierarchy;
const { byEntityKey } = unit.model.sequence;
@@ -116,6 +117,9 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
}
lastResidue = raI;
// ignore atoms with zero occupancy (assuming they are not actually atoms)
if (occupancy.isDefined && occupancy.value(aI) === 0) continue;
const aeI = getElementIdx(type_symbol.value(aI));
const atomIdA = label_atom_id.value(aI);
const componentPairs = componentMap ? componentMap.get(atomIdA) : void 0;

View File

@@ -1,19 +1,26 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 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 { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
import StructureElement from '../element';
import { OrderedSet } from '../../../../mol-data/int';
import { AlignSequences } from '../../../sequence/alignment/sequence';
import StructureProperties from '../properties';
export function superposeStructures(xs: StructureElement.Loci[]): MinimizeRmsd.Result[] {
export function superpose(xs: StructureElement.Loci[]): MinimizeRmsd.Result[] {
const ret: MinimizeRmsd.Result[] = [];
if (xs.length <= 0) return ret;
const n = getMinSize(xs);
const input: MinimizeRmsd.Input = { a: getPositionTable(xs[0], n), b: getPositionTable(xs[1], n) };
const input: MinimizeRmsd.Input = {
a: getPositionTable(xs[0], n),
b: getPositionTable(xs[1], n)
};
ret[0] = MinimizeRmsd.compute(input);
for (let i = 2; i < xs.length; i++) {
input.b = getPositionTable(xs[i], n);
@@ -24,6 +31,39 @@ export function superposeStructures(xs: StructureElement.Loci[]): MinimizeRmsd.R
return ret;
}
type AlignAndSuperposeResult = MinimizeRmsd.Result & { alignmentScore: number };
const reProtein = /(polypeptide|cyclic-pseudo-peptide)/i;
export function alignAndSuperpose(xs: StructureElement.Loci[]): AlignAndSuperposeResult[] {
const ret: AlignAndSuperposeResult[] = [];
if (xs.length <= 0) return ret;
const l = StructureElement.Loci.getFirstLocation(xs[0])!;
const subtype = StructureProperties.entity.subtype(l);
const substMatrix = subtype.match(reProtein) ? 'blosum62' : 'default';
for (let i = 1; i < xs.length; i++) {
const { a, b, score } = AlignSequences.compute({
a: xs[0].elements[0],
b: xs[i].elements[0],
}, { substMatrix });
const lociA = StructureElement.Loci(xs[0].structure, [a]);
const lociB = StructureElement.Loci(xs[i].structure, [b]);
const n = OrderedSet.size(a.indices);
ret.push({
...MinimizeRmsd.compute({
a: getPositionTable(lociA, n),
b: getPositionTable(lociB, n)
}),
alignmentScore: score
});
}
return ret;
}
function getPositionTable(xs: StructureElement.Loci, n: number): MinimizeRmsd.Positions {
const ret = MinimizeRmsd.Positions.empty(n);
let o = 0;

View File

@@ -32,7 +32,7 @@ export const PdbDownloadProvider = {
encoding: PD.Select('bcif', [['cif', 'cif'], ['bcif', 'bcif']] as ['cif' | 'bcif', string][]),
}, { label: 'RCSB PDB', isFlat: true }),
'pdbe': PD.Group({
variant: PD.Select('updated-bcif', [['updated-bcif', 'Updated (bcif)'], ['updated', 'Updated'], ['archival', 'Archival']] as ['updated' | 'archival', string][]),
variant: PD.Select('updated-bcif', [['updated-bcif', 'Updated (bcif)'], ['updated', 'Updated'], ['archival', 'Archival']] as ['updated' | 'updtaed-bcif' | 'archival', string][]),
}, { label: 'PDBe', isFlat: true }),
};
export type PdbDownloadProvider = keyof typeof PdbDownloadProvider;
@@ -55,7 +55,10 @@ const DownloadStructure = StateAction.build({
options
}, { isFlat: true, label: 'PDB' }),
'pdb-dev': PD.Group({
id: PD.Text('PDBDEV_00000001', { label: 'PDBDev Id(s)', description: 'One or more comma/space separated ids.' }),
provider: PD.Group({
id: PD.Text('PDBDEV_00000001', { label: 'PDBDev Id(s)', description: 'One or more comma/space separated ids.' }),
encoding: PD.Select('bcif', [['cif', 'cif'], ['bcif', 'bcif']] as ['cif' | 'bcif', string][]),
}, { pivot: 'id' }),
options
}, { isFlat: true, label: 'PDBDEV' }),
'bcif-static': PD.Group({
@@ -104,13 +107,15 @@ const DownloadStructure = StateAction.build({
asTrajectory = !!src.params.options.asTrajectory;
break;
case 'pdb-dev':
downloadParams = getDownloadParams(src.params.id,
downloadParams = getDownloadParams(src.params.provider.id,
id => {
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
return `https://pdb-dev.wwpdb.org/cif/${nId.toUpperCase()}.cif`;
return src.params.provider.encoding === 'bcif'
? `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`
: `https://pdb-dev.wwpdb.org/cif/${nId.toUpperCase()}.cif`;
},
id => id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`,
false
src.params.provider.encoding === 'bcif'
);
asTrajectory = !!src.params.options.asTrajectory;
break;

View File

@@ -23,6 +23,15 @@ export const AnimateModelIndex = PluginStateAnimation.create({
}, { options: [['palindrome', 'Palindrome'], ['loop', 'Loop'], ['once', 'Once']] }),
maxFPS: PD.Numeric(15, { min: 1, max: 30, step: 1 })
}),
canApply(ctx) {
const state = ctx.state.data;
const models = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
for (const m of models) {
const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
if (parent && parent.obj && parent.obj.data.length > 1) return { canApply: true };
}
return { canApply: false, reason: 'No trajectory to animate' };
},
initialState: () => ({} as { palindromeDirections?: { [id: string]: -1 | 1 | undefined } }),
async apply(animState, t, ctx) {
// limit fps
@@ -48,6 +57,8 @@ export const AnimateModelIndex = PluginStateAnimation.create({
const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
if (!parent || !parent.obj) continue;
const traj = parent.obj;
if (traj.data.length <= 1) continue;
update.to(m).update(old => {
const len = traj.data.length;
if (len !== 1) {
@@ -79,7 +90,9 @@ export const AnimateModelIndex = PluginStateAnimation.create({
});
}
await PluginCommands.State.Update(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
if (!allSingles) {
await PluginCommands.State.Update(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
}
if (allSingles || (params.mode.name === 'once' && isEnd)) return { kind: 'finished' };
if (params.mode.name === 'palindrome') return { kind: 'next', state: { palindromeDirections } };
@@ -104,6 +117,12 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
target: PD.Select(targets[0][0], targets)
};
},
canApply(plugin) {
const state = plugin.state.data;
const root = StateTransform.RootRef;
const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3D, root));
return { canApply: reprs.length > 0 };
},
initialState: () => ({ t: 0 }),
setup(params, plugin) {
const state = plugin.state.data;
@@ -231,6 +250,9 @@ export const AnimateStateInterpolation = PluginStateAnimation.create({
params: () => ({
transtionDurationInMs: PD.Numeric(2000, { min: 100, max: 30000, step: 10 })
}),
canApply(plugin) {
return { canApply: plugin.managers.snapshot.state.entries.size > 1 };
},
initialState: () => ({ }),
async apply(animState, t, ctx) {

View File

@@ -15,7 +15,9 @@ export { PluginStateAnimation };
interface PluginStateAnimation<P = any, S = any> {
name: string,
readonly display: { readonly name: string, readonly description?: string },
params(ctx: PluginContext): PD.For<P>,
canApply?(ctx: PluginContext): { canApply: true } | { canApply: false, reason?: string },
initialState(params: P, ctx: PluginContext): S,
// TODO: support state in setup/teardown?

View File

@@ -15,6 +15,7 @@ import { PluginContext } from '../../../mol-plugin/context';
import { StateObjectRef, StateObjectSelector } from '../../../mol-state';
import { StaticStructureComponentType } from '../../helpers/structure-component';
import { StructureSelectionQueries as Q } from '../../helpers/structure-selection-query';
import { PluginConfig } from '../../../mol-plugin/config';
export interface StructureRepresentationPresetProvider<P = any, S extends _Result = _Result> extends PresetProvider<PluginStateObject.Molecule.Structure, P, S> { }
export function StructureRepresentationPresetProvider<P, S extends _Result>(repr: StructureRepresentationPresetProvider<P, S>) { return repr; }
@@ -64,7 +65,9 @@ const auto = StructureRepresentationPresetProvider({
apply(ref, params, plugin) {
const structure = StateObjectRef.resolveAndCheck(plugin.state.data, ref)?.obj?.data;
if (!structure) return { };
const size = Structure.getSize(structure);
const thresholds = plugin.config.get(PluginConfig.Structure.SizeThresholds) || Structure.DefaultSizeThresholds;
const size = Structure.getSize(structure, thresholds);
switch (size) {
case Structure.Size.Gigantic:

View File

@@ -74,7 +74,7 @@ function StructureSelectionQuery(label: string, expression: Expression, props: S
ensureCustomProperties: props.ensureCustomProperties,
async getSelection(plugin, runtime, structure) {
const current = plugin.managers.structure.selection.getStructure(structure);
const currentSelection = current ? StructureSelection.Singletons(structure, current) : StructureSelection.Empty(structure);
const currentSelection = current ? StructureSelection.Sequence(structure, [current]) : StructureSelection.Empty(structure);
if (props.ensureCustomProperties) {
await props.ensureCustomProperties({ runtime, assetManager: plugin.managers.asset }, structure);
}
@@ -119,71 +119,132 @@ const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([
])
]), { category: StructureSelectionCategory.Structure });
// TODO maybe pre-calculate atom properties like backbone/sidechain
const _proteinEntityTest = MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(polypeptide|cyclic-pseudo-peptide)', 'i'),
MS.ammp('entitySubtype')
])
]);
const _nucleiEntityTest = MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(nucleotide|peptide nucleic acid)', 'i'),
MS.ammp('entitySubtype')
])
]);
/**
* this is to get non-polymer and peptide terminus components in polymer entities,
* - non-polymer, e.g. PXZ in 4HIV or generally ACE
* - carboxy terminus, e.g. FC0 in 4BP9, or ETA in 6DDE
* - amino terminus, e.g. ARF in 3K4V, or 4MM in 3EGV
*/
const _nonPolymerResidueTest = MS.core.str.match([
MS.re('non-polymer|(amino|carboxy) terminus|peptide-like', 'i'),
MS.ammp('chemCompType')
]);
// TODO maybe pre-calculate backbone atom properties
const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
MS.struct.combinator.merge([
MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(polypeptide|cyclic-pseudo-peptide)', 'i'),
MS.ammp('entitySubtype')
])
]),
'entity-test': _proteinEntityTest,
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
'residue-test': MS.core.logic.not([_nonPolymerResidueTest]),
'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(ProteinBackboneAtoms)), MS.ammp('label_atom_id')])
})
]),
MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(nucleotide|peptide nucleic acid)', 'i'),
MS.ammp('entitySubtype')
])
]),
'entity-test': _nucleiEntityTest,
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
'residue-test': MS.core.logic.not([_nonPolymerResidueTest]),
'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(NucleicBackboneAtoms)), MS.ammp('label_atom_id')])
})
])
])
]), { category: StructureSelectionCategory.Structure });
const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(polypeptide|cyclic-pseudo-peptide)', 'i'),
MS.ammp('entitySubtype')
])
// TODO maybe pre-calculate sidechain atom property
const sidechain = StructureSelectionQuery('Sidechain', MS.struct.modifier.union([
MS.struct.combinator.merge([
MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': _proteinEntityTest,
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
'residue-test': MS.core.logic.not([_nonPolymerResidueTest]),
'atom-test': MS.core.logic.or([
MS.core.logic.not([
MS.core.set.has([MS.set(...SetUtils.toArray(ProteinBackboneAtoms)), MS.ammp('label_atom_id')])
])
])
})
]),
MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': _nucleiEntityTest,
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
'residue-test': MS.core.logic.not([_nonPolymerResidueTest]),
'atom-test': MS.core.logic.or([
MS.core.logic.not([
MS.core.set.has([MS.set(...SetUtils.toArray(NucleicBackboneAtoms)), MS.ammp('label_atom_id')])
])
])
})
])
})
])
]), { category: StructureSelectionCategory.Structure });
// TODO maybe pre-calculate sidechain atom property
const sidechainWithTrace = StructureSelectionQuery('Sidechain with Trace', MS.struct.modifier.union([
MS.struct.combinator.merge([
MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': _proteinEntityTest,
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
'residue-test': MS.core.logic.not([_nonPolymerResidueTest]),
'atom-test': MS.core.logic.or([
MS.core.logic.not([
MS.core.set.has([MS.set(...SetUtils.toArray(ProteinBackboneAtoms)), MS.ammp('label_atom_id')])
]),
MS.core.rel.eq([MS.ammp('label_atom_id'), 'CA']),
MS.core.logic.and([
MS.core.rel.eq([MS.ammp('auth_comp_id'), 'PRO']),
MS.core.rel.eq([MS.ammp('label_atom_id'), 'N'])
])
])
})
]),
MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': _nucleiEntityTest,
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
'residue-test': MS.core.logic.not([_nonPolymerResidueTest]),
'atom-test': MS.core.logic.or([
MS.core.logic.not([
MS.core.set.has([MS.set(...SetUtils.toArray(NucleicBackboneAtoms)), MS.ammp('label_atom_id')])
]),
MS.core.rel.eq([MS.ammp('label_atom_id'), 'P'])
])
})
])
])
]), { category: StructureSelectionCategory.Structure });
const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([
MS.struct.generator.atomGroups({ 'entity-test': _proteinEntityTest })
]), { category: StructureSelectionCategory.Type });
const nucleic = StructureSelectionQuery('Nucleic', MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(nucleotide|peptide nucleic acid)', 'i'),
MS.ammp('entitySubtype')
])
])
})
MS.struct.generator.atomGroups({ 'entity-test': _nucleiEntityTest })
]), { category: StructureSelectionCategory.Type });
const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(polypeptide|cyclic-pseudo-peptide)', 'i'),
MS.ammp('entitySubtype')
])
]),
'entity-test': _proteinEntityTest,
'residue-test': MS.core.flags.hasAny([
MS.ammp('secondaryStructureFlags'),
MS.core.type.bitflags([SecondaryStructureType.Flag.Helix])
@@ -193,13 +254,7 @@ const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([
const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.logic.and([
MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
MS.core.str.match([
MS.re('(polypeptide|cyclic-pseudo-peptide)', 'i'),
MS.ammp('entitySubtype')
])
]),
'entity-test': _proteinEntityTest,
'residue-test': MS.core.flags.hasAny([
MS.ammp('secondaryStructureFlags'),
MS.core.type.bitflags([SecondaryStructureType.Flag.Beta])
@@ -258,18 +313,11 @@ const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([
])
})
]),
// this is to get non-polymer and peptide terminus components in polymer entities,
// - non-polymer, e.g. PXZ in 4HIV or generally ACE
// - carboxy terminus, e.g. FC0 in 4BP9, or ETA in 6DDE
// - amino terminus, e.g. ARF in 3K4V, or 4MM in 3EGV
MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
'residue-test': MS.core.str.match([
MS.re('non-polymer|(amino|carboxy) terminus|peptide-like', 'i'),
MS.ammp('chemCompType')
])
'residue-test': _nonPolymerResidueTest
})
])
]),
@@ -373,7 +421,7 @@ const complement = StructureSelectionQuery('Inverse / Complement of Selection',
referencesCurrent: true
});
const bonded = StructureSelectionQuery('Residues Bonded to Selection', MS.struct.modifier.union([
const covalentlyBonded = StructureSelectionQuery('Residues Covalently Bonded to Selection', MS.struct.modifier.union([
MS.struct.modifier.includeConnected({
0: MS.internal.generator.current(), 'layer-count': 1, 'as-whole-residues': true
})
@@ -383,6 +431,24 @@ const bonded = StructureSelectionQuery('Residues Bonded to Selection', MS.struct
referencesCurrent: true
});
const covalentlyOrMetallicBonded = StructureSelectionQuery('Residues with Cov. or Metallic Bond to Selection', MS.struct.modifier.union([
MS.struct.modifier.includeConnected({
0: MS.internal.generator.current(),
'layer-count': 1,
'as-whole-residues': true,
'bond-test': MS.core.flags.hasAny([
MS.struct.bondProperty.flags(),
MS.core.type.bitflags([
BondType.Flag.Covalent | BondType.Flag.MetallicCoordination
])
])
})
]), {
description: 'Select residues with covalent or metallic bond to current selection.',
category: StructureSelectionCategory.Manipulate,
referencesCurrent: true
});
const wholeResidues = StructureSelectionQuery('Whole Residues of Selection', MS.struct.modifier.union([
MS.struct.modifier.wholeResidues({
0: MS.internal.generator.current()
@@ -532,6 +598,8 @@ export const StructureSelectionQueries = {
polymer,
trace,
backbone,
sidechain,
sidechainWithTrace,
protein,
nucleic,
helix,
@@ -551,7 +619,8 @@ export const StructureSelectionQueries = {
aromaticRing,
surroundings,
complement,
bonded,
covalentlyBonded,
covalentlyOrMetallicBonded,
wholeResidues,
};

View File

@@ -204,9 +204,10 @@ namespace InteractivityManager {
}
selectOnly(current: Representation.Loci, applyGranularity = true) {
this.deselectAll();
const normalized = this.normalizedLoci(current, applyGranularity);
if (StructureElement.Loci.is(normalized.loci)) {
// only deselect for the structure of the given loci
this.deselect({ loci: Structure.toStructureElementLoci(normalized.loci.structure), repr: normalized.repr }, false);
this.sel.modify('set', normalized.loci);
}
this.mark(normalized, MarkerAction.Select);

View File

@@ -231,6 +231,8 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
if (PluginStateSnapshotManager.isStateSnapshot(snapshot)) {
return this.setStateSnapshot(snapshot);
} else if (PluginStateSnapshotManager.isStateSnapshot(snapshot.data)) {
return this.setStateSnapshot(snapshot.data);
} else {
this.plugin.state.setSnapshot(snapshot);
}
@@ -330,7 +332,7 @@ namespace PluginStateSnapshotManager {
snapshot: PluginState.Snapshot
}
export function Entry(snapshot: PluginState.Snapshot, params: {name?: string, description?: string }): Entry {
export function Entry(snapshot: PluginState.Snapshot, params: { name?: string, description?: string }): Entry {
return { timestamp: +new Date(), snapshot, ...params };
}

View File

@@ -13,13 +13,13 @@ import { EmptyLoci, Loci } from '../../../mol-model/loci';
import { Structure, StructureElement, StructureSelection } from '../../../mol-model/structure';
import { Boundary } from '../../../mol-model/structure/structure/util/boundary';
import { PluginContext } from '../../../mol-plugin/context';
import { StateObject, StateObjectRef } from '../../../mol-state';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { structureElementStatsLabel } from '../../../mol-theme/label';
import { arrayRemoveAtInPlace } from '../../../mol-util/array';
import { StatefulPluginComponent } from '../../component';
import { StructureSelectionQuery } from '../../helpers/structure-selection-query';
import { PluginStateObject } from '../../objects';
import { PluginStateObject as PSO } from '../../objects';
import { UUID } from '../../../mol-util';
import { StructureRef } from './hierarchy-state';
@@ -30,17 +30,23 @@ interface StructureSelectionManagerState {
}
const boundaryHelper = new BoundaryHelper('98');
const HISTORY_CAPACITY = 8;
const HISTORY_CAPACITY = 24;
export type StructureSelectionModifier = 'add' | 'remove' | 'intersect' | 'set'
export class StructureSelectionManager extends StatefulPluginComponent<StructureSelectionManagerState> {
readonly events = {
changed: this.ev<undefined>(),
additionsHistoryUpdated: this.ev<undefined>()
additionsHistoryUpdated: this.ev<undefined>(),
loci: {
add: this.ev<StructureElement.Loci>(),
remove: this.ev<StructureElement.Loci>(),
clear: this.ev<undefined>()
}
}
private referenceLoci: Loci | undefined
private referenceLoci: StructureElement.Loci | undefined
get entries() { return this.state.entries; }
get additionsHistory() { return this.state.additionsHistory; }
@@ -51,7 +57,8 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
}
private getEntry(s: Structure) {
const cell = this.plugin.helpers.substructureParent.get(s);
// ignore decorators to get stable ref
const cell = this.plugin.helpers.substructureParent.get(s, true);
if (!cell) return;
const ref = cell.transform.ref;
if (!this.entries.has(ref)) {
@@ -94,6 +101,7 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
entry.selection = StructureElement.Loci.union(entry.selection, loci);
this.tryAddHistory(loci);
this.referenceLoci = loci;
this.events.loci.add.next(loci);
return !StructureElement.Loci.areEqual(sel, entry.selection);
}
@@ -107,6 +115,7 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
// this.addHistory(loci);
this.referenceLoci = loci;
this.events.loci.remove.next(loci);
return !StructureElement.Loci.areEqual(sel, entry.selection);
}
@@ -136,26 +145,34 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
return !StructureElement.Loci.areEqual(sel, entry.selection);
}
modifyHistory(entry: StructureSelectionHistoryEntry, action: 'remove' | 'up' | 'down', modulus?: number) {
const idx = this.additionsHistory.indexOf(entry);
modifyHistory(entry: StructureSelectionHistoryEntry, action: 'remove' | 'up' | 'down', modulus?: number, groupByStructure = false) {
const history = this.additionsHistory;
const idx = history.indexOf(entry);
if (idx < 0) return;
let swapWith: number | undefined = void 0;
switch (action) {
case 'remove': arrayRemoveAtInPlace(this.additionsHistory, idx); break;
case 'remove': arrayRemoveAtInPlace(history, idx); break;
case 'up': swapWith = idx - 1; break;
case 'down': swapWith = idx + 1; break;
}
if (swapWith !== void 0) {
const mod = modulus ? Math.min(this.additionsHistory.length, modulus) : this.additionsHistory.length;
swapWith = swapWith % mod;
if (swapWith < 0) swapWith += mod;
const mod = modulus ? Math.min(history.length, modulus) : history.length;
while (true) {
swapWith = swapWith % mod;
if (swapWith < 0) swapWith += mod;
const t = this.additionsHistory[idx];
this.additionsHistory[idx] = this.additionsHistory[swapWith];
this.additionsHistory[swapWith] = t;
if (!groupByStructure || history[idx].loci.structure === history[swapWith].loci.structure) {
const t = history[idx];
history[idx] = history[swapWith];
history[swapWith] = t;
break;
} else {
swapWith += action === 'up' ? -1 : +1;
}
}
}
this.events.additionsHistoryUpdated.next();
@@ -190,33 +207,88 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
this.events.additionsHistoryUpdated.next();
}
private onRemove(ref: string) {
if (this.entries.has(ref)) {
this.entries.delete(ref);
// TODO: property update the latest loci
private clearHistory() {
if (this.state.additionsHistory.length !== 0) {
this.state.additionsHistory = [];
this.referenceLoci = undefined;
this.events.additionsHistoryUpdated.next();
}
}
private onUpdate(ref: string, oldObj: StateObject | undefined, obj: StateObject) {
if (!PluginStateObject.Molecule.Structure.is(obj)) return;
private clearHistoryForStructure(structure: Structure) {
const historyEntryToRemove: StructureSelectionHistoryEntry[] = [];
for (const e of this.state.additionsHistory) {
if (e.loci.structure.root === structure.root) {
historyEntryToRemove.push(e);
}
}
for (const e of historyEntryToRemove) {
this.modifyHistory(e, 'remove');
}
if (historyEntryToRemove.length !== 0) {
this.events.additionsHistoryUpdated.next();
}
}
private onRemove(ref: string, obj: PSO.Molecule.Structure | undefined) {
if (this.entries.has(ref)) {
if (!PluginStateObject.Molecule.Structure.is(oldObj) || oldObj === obj || oldObj.data === obj.data) return;
this.entries.delete(ref);
if (obj?.data) {
this.clearHistoryForStructure(obj.data);
}
if (this.referenceLoci?.structure === obj?.data) {
this.referenceLoci = undefined;
}
this.state.stats = void 0;
this.events.changed.next();
}
}
// TODO: property update the latest loci & reference loci
this.state.additionsHistory = [];
this.referenceLoci = undefined;
private onUpdate(ref: string, oldObj: PSO.Molecule.Structure | undefined, obj: PSO.Molecule.Structure) {
// remap the old selection to be related to the new object if possible.
if (Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
this.entries.set(ref, remapSelectionEntry(this.entries.get(ref)!, obj.data));
return;
// no change to structure
if (oldObj === obj || oldObj?.data === obj.data) return;
// ignore decorators to get stable ref
const cell = this.plugin.helpers.substructureParent.get(obj.data, true);
if (!cell) return;
ref = cell.transform.ref;
if (!this.entries.has(ref)) return;
// use structure from last decorator as reference
const structure = this.plugin.helpers.substructureParent.get(obj.data)?.obj?.data;
if (!structure) return;
// oldObj is not defined for inserts (e.g. TransformStructureConformation)
if (!oldObj || Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
this.entries.set(ref, remapSelectionEntry(this.entries.get(ref)!, structure));
// remap referenceLoci & prevHighlight if needed and possible
if (this.referenceLoci?.structure.root === structure.root) {
this.referenceLoci = StructureElement.Loci.remap(this.referenceLoci, structure);
}
// clear the selection
this.entries.set(ref, new SelectionEntry(StructureElement.Loci(obj.data, [])));
// remap history locis if needed and possible
let changedHistory = false;
for (const e of this.state.additionsHistory) {
if (e.loci.structure.root === structure.root) {
e.loci = StructureElement.Loci.remap(e.loci, structure);
changedHistory = true;
}
}
if (changedHistory) this.events.additionsHistoryUpdated.next();
} else {
// clear the selection for ref
this.entries.set(ref, new SelectionEntry(StructureElement.Loci(structure, [])));
if (this.referenceLoci?.structure.root === structure.root) {
this.referenceLoci = undefined;
}
this.clearHistoryForStructure(structure);
this.state.stats = void 0;
this.events.changed.next();
}
}
@@ -234,6 +306,8 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
this.referenceLoci = undefined;
this.state.stats = void 0;
this.events.changed.next();
this.events.loci.clear.next();
this.clearHistory();
return selections;
}
@@ -276,7 +350,7 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
if (!xs) return;
const ref = this.referenceLoci;
if (!ref || !StructureElement.Loci.is(ref) || ref.structure.root !== loci.structure.root) return;
if (!ref || !StructureElement.Loci.is(ref) || ref.structure !== loci.structure) return;
let e: StructureElement.Loci['elements'][0] | undefined;
for (const _e of ref.elements) {
@@ -289,26 +363,7 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
if (xs.unit !== e.unit) return;
return getElementRange(loci.structure.root, e, xs);
}
private prevHighlight: StructureElement.Loci | undefined = void 0;
accumulateInteractiveHighlight(loci: Loci) {
if (StructureElement.Loci.is(loci)) {
if (this.prevHighlight) {
this.prevHighlight = StructureElement.Loci.union(this.prevHighlight, loci);
} else {
this.prevHighlight = loci;
}
}
return this.prevHighlight;
}
clearInteractiveHighlight() {
const ret = this.prevHighlight;
this.prevHighlight = void 0;
return ret || EmptyLoci;
return getElementRange(loci.structure, e, xs);
}
/** Count of all selected elements */
@@ -411,11 +466,11 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
}));
}
fromSelections(ref: StateObjectRef<PluginStateObject.Molecule.Structure.Selections>) {
fromSelections(ref: StateObjectRef<PSO.Molecule.Structure.Selections>) {
const cell = StateObjectRef.resolveAndCheck(this.plugin.state.data, ref);
if (!cell || !cell.obj) return;
if (!PluginStateObject.Molecule.Structure.Selections.is(cell.obj)) {
if (!PSO.Molecule.Structure.Selections.is(cell.obj)) {
console.warn('fromSelections applied to wrong object type.', cell.obj);
return;
}
@@ -429,8 +484,9 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
constructor(private plugin: PluginContext) {
super({ entries: new Map(), additionsHistory: [], stats: SelectionStats() });
plugin.state.data.events.object.removed.subscribe(e => this.onRemove(e.ref));
plugin.state.data.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
// listen to events from substructureParent helper to ensure it is updated
plugin.helpers.substructureParent.events.removed.subscribe(e => this.onRemove(e.ref, e.obj));
plugin.helpers.substructureParent.events.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
}
}

View File

@@ -42,7 +42,7 @@ const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
apply({ a, params }) {
return Task.create('Create volume from CCP4/MRC/MAP', async ctx => {
const volume = await volumeFromCcp4(a.data, { ...params, label: a.data.name || a.label }).runInContext(ctx);
const props = { label: volume.label || 'Volume', description: 'Volume' };
const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.NX}\u00D7${a.data.header.NX}\u00D7${a.data.header.NX}` };
return new SO.Volume.Data(volume, props);
});
}
@@ -63,7 +63,7 @@ const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
apply({ a, params }) {
return Task.create('Create volume from DSN6/BRIX', async ctx => {
const volume = await volumeFromDsn6(a.data, { ...params, label: a.data.name || a.label }).runInContext(ctx);
const props = { label: volume.label || 'Volume', description: 'Volume' };
const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.xExtent}\u00D7${a.data.header.yExtent}\u00D7${a.data.header.zExtent}` };
return new SO.Volume.Data(volume, props);
});
}
@@ -85,7 +85,7 @@ const VolumeFromCube = PluginStateTransform.BuiltIn({
apply({ a, params }) {
return Task.create('Create volume from Cube', async ctx => {
const volume = await volumeFromCube(a.data, { ...params, label: a.data.name || a.label }).runInContext(ctx);
const props = { label: volume.label || 'Volume', description: 'Volume' };
const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.dim[0]}\u00D7${a.data.header.dim[1]}\u00D7${a.data.header.dim[2]}` };
return new SO.Volume.Data(volume, props);
});
}
@@ -102,8 +102,7 @@ const VolumeFromDx = PluginStateTransform.BuiltIn({
return Task.create('Parse DX', async ctx => {
console.log(a);
const volume = await volumeFromDx(a.data, { label: a.data.name || a.label }).runInContext(ctx);
console.log(volume);
const props = { label: volume.label || 'Volume', description: 'Volume' };
const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.dim[0]}\u00D7${a.data.header.dim[1]}\u00D7${a.data.header.dim[2]}` };
return new SO.Volume.Data(volume, props);
});
}
@@ -135,7 +134,8 @@ const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
if (!block) throw new Error(`Data block '${[header]}' not found.`);
const densityServerCif = CIF.schema.densityServer(block);
const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx);
const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` };
const [x, y, z] = volume.grid.cells.space.dimensions;
const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `Volume ${x}\u00D7${y}\u00D7${z}` };
return new SO.Volume.Data(volume, props);
});
}

View File

@@ -30,6 +30,7 @@ import { StructureSelectionActionsControls } from './structure/selection';
import { StructureSourceControls } from './structure/source';
import { VolumeStreamingControls, VolumeSourceControls } from './structure/volume';
import { PluginConfig } from '../mol-plugin/config';
import { StructureSuperpositionControls } from './structure/superposition';
export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
state = { show: false, label: '' }
@@ -296,6 +297,7 @@ export class DefaultStructureTools extends PluginUIComponent {
<StructureSourceControls />
<StructureMeasurementsControls />
<StructureSuperpositionControls />
<StructureComponentControls />
<VolumeStreamingControls />
<VolumeSourceControls />

View File

@@ -34,10 +34,22 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
}
}
onChangeText = (value: Color) => {
if (value !== this.props.value) {
this.update(value);
}
onR = (v: number) => {
const [, g, b] = Color.toRgb(this.props.value);
const value = Color.fromRgb(v, g, b);
if (value !== this.props.value) this.update(value);
}
onG = (v: number) => {
const [r, , b] = Color.toRgb(this.props.value);
const value = Color.fromRgb(r, v, b);
if (value !== this.props.value) this.update(value);
}
onB = (v: number) => {
const [r, g, ] = Color.toRgb(this.props.value);
const value = Color.fromRgb(r, g, v);
if (value !== this.props.value) this.update(value);
}
swatch() {
@@ -49,44 +61,23 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
render() {
const label = this.props.param.label || camelCaseToWords(this.props.name);
const [r, g, b] = Color.toRgb(this.props.value);
return <>
<ControlRow title={this.props.param.description}
label={label}
control={<Button onClick={this.toggleExpanded} inline className='msp-combined-color-button' style={{ background: Color.toStyle(this.props.value) }} />} />
{this.state.isExpanded && <div className='msp-control-offset'>
{this.swatch()}
<ControlRow label='RGB'
control={<TextInput onChange={this.onChangeText} value={this.props.value}
fromValue={formatColorRGB} toValue={getColorFromString} isValid={isValidColorString}
className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true}
placeholder='e.g. 127 127 127' delayMs={250} />} />
<ControlRow label='RGB' control={<div style={{ display: 'flex', textAlignLast: 'center' }}>
<TextInput onChange={this.onR} numeric value={r} delayMs={250} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} />
<TextInput onChange={this.onG} numeric value={g} delayMs={250} style={{ order: 2, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} />
<TextInput onChange={this.onB} numeric value={b} delayMs={250} style={{ order: 3, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} />
</div>}/>
</div>}
</>;
}
}
function formatColorRGB(c: Color) {
const [r, g, b] = Color.toRgb(c);
return `${r} ${g} ${b}`;
}
function getColorFromString(s: string) {
const cs = s.split(/\s+/g);
return Color.fromRgb(+cs[0], +cs[1], +cs[2]);
}
function isValidColorString(s: string) {
const cs = s.split(/\s+/g);
if (cs.length !== 3 && !(cs.length === 4 && cs[3] === '')) return false;
for (const c of cs) {
if (c === '') continue;
const n = +c;
if ('' + n !== c) return false;
if (n < 0 || n > 255) return false;
}
return true;
}
let _colors: React.ReactFragment | undefined = void 0;
export function ColorOptions() {
if (_colors) return _colors;

View File

@@ -24,7 +24,8 @@ export class ControlGroup extends React.Component<{
headerLeftMargin?: string,
onHeaderClick?: () => void,
noTopMargin?: boolean,
childrenClassName?: string
childrenClassName?: string,
maxHeight?: string
}, { isExpanded: boolean }> {
state = { isExpanded: !!this.props.initialExpanded }
@@ -49,7 +50,7 @@ export class ControlGroup extends React.Component<{
<b>{this.props.header}</b>
</Button>
</div>
{this.state.isExpanded && <div className={groupClassName} style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
{this.state.isExpanded && <div className={groupClassName} style={{ display: this.state.isExpanded ? 'block' : 'none', maxHeight: this.props.maxHeight, overflow: 'hidden', overflowY: 'auto' }}>
{this.props.children}
</div>}
</div>;

View File

@@ -21,13 +21,14 @@ export function Icon(props: {
//
export function Union() { return _union; }
export function Subtract() { return _subtract; }
export function Intersect() { return _intersect; }
export function UnionSvg() { return _union; }
export function SubtractSvg() { return _subtract; }
export function IntersectSvg() { return _intersect; }
export function SetSvg() { return _set; }
export function MoleculeSvg() { return _molecule; }
export function RulerSvg() { return _ruler; }
export function CubeSvg() { return _cube; }
export function CursorSvg() { return _cursor; }
const circleLeft = <circle r="6px" id="circle-left" cy="12px" cx="8px" strokeWidth="1" />;
const circleRight = <circle r="6px" id="circle-right" cy="12px" cx="16px" strokeWidth="1" />;
@@ -87,64 +88,25 @@ const _set = <svg width="24px" height="24px" viewBox="0 0 24 24">
const _molecule = <svg width="17px" height="17px" viewBox="0 0 299.463 299.463">
<g>
<path d="M256.851,173.832v-48.201c22.916-4.918,34.151-30.668,22.556-50.771c-11.547-20.004-39.486-23.251-55.242-5.844
l-41.746-24.106C189.618,22.603,172.861,0,149.734,0c-23.132,0-39.881,22.609-32.685,44.911L75.305,69.016
C59.522,51.586,31.597,54.88,20.061,74.863c-11.63,20.163-0.298,45.862,22.557,50.769v48.2
c-22.821,4.898-34.195,30.591-22.556,50.771c11.529,19.972,39.454,23.285,55.242,5.845l41.746,24.106
c-7.199,22.308,9.559,44.911,32.685,44.911c23.132,0,39.88-22.609,32.685-44.911l41.745-24.106
c15.817,17.469,43.73,14.099,55.242-5.844c0,0,0-0.001,0.001-0.002c4.587-7.953,5.805-17.213,3.431-26.076
C279.392,185.657,269.129,176.461,256.851,173.832z M249.62,72.088c20.568,0,27.428,27.191,10.008,37.239
c-0.003,0.002-0.006,0.003-0.009,0.005c-10.04,5.81-22.85,1.762-27.877-8.475C225.206,87.548,234.938,72.088,249.62,72.088z
M149.734,14.4c11.005,0,19.958,8.954,19.958,19.959c0,11.127-9.077,19.958-19.958,19.958c-10.95,0-19.958-8.9-19.958-19.958
C129.776,23.354,138.729,14.4,149.734,14.4z M39.84,109.328c-17.451-10.067-10.534-37.24,10.01-37.24
c15.311,0,24.922,16.653,17.251,29.942C61.681,111.397,49.517,114.925,39.84,109.328z M59.802,224.702
c-9.535,5.503-21.768,2.229-27.268-7.298c-7.639-13.242,1.887-29.945,17.236-29.945c0.013,0,0.027,0,0.04,0
C70.07,187.48,77.49,214.469,59.802,224.702z M149.734,285.062c-11.005,0-19.958-8.954-19.958-19.958
c0-11.127,9.077-19.958,19.958-19.958c10.954,0,19.958,8.903,19.958,19.958C169.693,276.109,160.74,285.062,149.734,285.062z
M216.953,217.982l-41.727,24.095c-13.778-15.22-37.459-14.94-50.983,0l-41.728-24.096c6.196-19.289-5.541-39.835-25.498-44.149
V125.63c19.752-4.268,31.762-24.65,25.498-44.149l41.727-24.095c13.629,15.055,37.32,15.093,50.983,0l41.728,24.096
c-6.196,19.29,5.534,39.835,25.498,44.149v48.202C222.61,178.123,210.721,198.581,216.953,217.982z M266.935,217.404
c-5.501,9.528-17.732,12.802-27.261,7.302c-17.682-10.23-10.301-37.247,10.032-37.247
C264.984,187.459,274.602,204.112,266.935,217.404z"/>
<path d="M256.851,173.832v-48.201c22.916-4.918,34.151-30.668,22.556-50.771c-11.547-20.004-39.486-23.251-55.242-5.844 l-41.746-24.106C189.618,22.603,172.861,0,149.734,0c-23.132,0-39.881,22.609-32.685,44.911L75.305,69.016 C59.522,51.586,31.597,54.88,20.061,74.863c-11.63,20.163-0.298,45.862,22.557,50.769v48.2 c-22.821,4.898-34.195,30.591-22.556,50.771c11.529,19.972,39.454,23.285,55.242,5.845l41.746,24.106 c-7.199,22.308,9.559,44.911,32.685,44.911c23.132,0,39.88-22.609,32.685-44.911l41.745-24.106 c15.817,17.469,43.73,14.099,55.242-5.844c0,0,0-0.001,0.001-0.002c4.587-7.953,5.805-17.213,3.431-26.076 C279.392,185.657,269.129,176.461,256.851,173.832z M249.62,72.088c20.568,0,27.428,27.191,10.008,37.239 c-0.003,0.002-0.006,0.003-0.009,0.005c-10.04,5.81-22.85,1.762-27.877-8.475C225.206,87.548,234.938,72.088,249.62,72.088z M149.734,14.4c11.005,0,19.958,8.954,19.958,19.959c0,11.127-9.077,19.958-19.958,19.958c-10.95,0-19.958-8.9-19.958-19.958 C129.776,23.354,138.729,14.4,149.734,14.4z M39.84,109.328c-17.451-10.067-10.534-37.24,10.01-37.24 c15.311,0,24.922,16.653,17.251,29.942C61.681,111.397,49.517,114.925,39.84,109.328z M59.802,224.702 c-9.535,5.503-21.768,2.229-27.268-7.298c-7.639-13.242,1.887-29.945,17.236-29.945c0.013,0,0.027,0,0.04,0 C70.07,187.48,77.49,214.469,59.802,224.702z M149.734,285.062c-11.005,0-19.958-8.954-19.958-19.958 c0-11.127,9.077-19.958,19.958-19.958c10.954,0,19.958,8.903,19.958,19.958C169.693,276.109,160.74,285.062,149.734,285.062z M216.953,217.982l-41.727,24.095c-13.778-15.22-37.459-14.94-50.983,0l-41.728-24.096c6.196-19.289-5.541-39.835-25.498-44.149 V125.63c19.752-4.268,31.762-24.65,25.498-44.149l41.727-24.095c13.629,15.055,37.32,15.093,50.983,0l41.728,24.096 c-6.196,19.29,5.534,39.835,25.498,44.149v48.202C222.61,178.123,210.721,198.581,216.953,217.982z M266.935,217.404 c-5.501,9.528-17.732,12.802-27.261,7.302c-17.682-10.23-10.301-37.247,10.032-37.247 C264.984,187.459,274.602,204.112,266.935,217.404z"/>
</g>
</svg>;
const _ruler = <svg viewBox="0 0 508.073 508.073" width="17px" height="17px">
<g>
<path d="M470.459,378.925c-0.7-2.1-1.9-4-3.4-5.5l-113.9-113.9l149.8-149.8c10-10,2.6-17.3,0-19.9l-85.7-85.7
c-5.5-5.5-14.4-5.5-19.9,0l-149.8,149.8l-134.7-134.7c-25.5-25.5-67.8-25.6-93.4,0c-25.8,25.8-25.8,67.7,0,93.5l134.6,134.7
l-149.9,149.9c-5.5,5.5-5.5,14.4,0,19.9l85.6,85.7c2.6,2.6,10,10,19.9,0l150-149.9l113.9,113.9c1.5,1.5,3.4,2.7,5.5,3.4
l110.4,36.9c6,2,10.7,0.3,14.4-3.4c3.8-3.8,5.1-9.4,3.4-14.4L470.459,378.925z M276.159,165.225l29.9,29.9
c5.5,5.5,14.4,5.5,19.9,0c5-5,5.5-14.4,0-19.9l-29.9-29.9l23.7-23.7l13,13c5.5,5.5,14.4,5.5,19.9,0c5.2-5.2,6.2-13.7,0-19.9
l-13-13l23.7-23.7l29.4,29.4c5.5,5.5,14.4,5.5,19.9,0c5.6-5.6,5.5-14.4,0-19.9l-29.4-29.5l24-24l65.8,65.7l-139.8,139.9
l-65.8-65.8L276.159,165.225z M39.359,92.825c-14.8-14.8-14.8-38.8,0-53.6c14.1-14.1,38.8-14.7,53.6,0l15.5,15.5l-53.6,53.6
L39.359,92.825z M99.759,473.025l-65.7-65.7l24-24l13.2,13.2c5.5,5.5,14.4,5.6,19.9,0c5.5-5.5,5.5-14.4,0-19.9l-13.1-13.3
l23.7-23.7l29.6,29.6c6.2,6.2,14.7,5.2,19.9,0c5.5-5.5,5.5-14.4,0-19.9l-29.6-29.6l23.7-23.7l13.2,13.2c5.5,5.5,14.9,5,19.9,0
c5.5-5.5,5.5-14.4,0-19.9l-13.2-13.2l8.8-8.8l65.8,65.7L99.759,473.025z M74.759,128.225l53.6-53.6l308.7,308.7l-53.6,53.6
L74.759,128.225z M409.659,450.725l41.3-41.3l20.7,61.9L409.659,450.725z"/>
<path d="M470.459,378.925c-0.7-2.1-1.9-4-3.4-5.5l-113.9-113.9l149.8-149.8c10-10,2.6-17.3,0-19.9l-85.7-85.7 c-5.5-5.5-14.4-5.5-19.9,0l-149.8,149.8l-134.7-134.7c-25.5-25.5-67.8-25.6-93.4,0c-25.8,25.8-25.8,67.7,0,93.5l134.6,134.7 l-149.9,149.9c-5.5,5.5-5.5,14.4,0,19.9l85.6,85.7c2.6,2.6,10,10,19.9,0l150-149.9l113.9,113.9c1.5,1.5,3.4,2.7,5.5,3.4 l110.4,36.9c6,2,10.7,0.3,14.4-3.4c3.8-3.8,5.1-9.4,3.4-14.4L470.459,378.925z M276.159,165.225l29.9,29.9 c5.5,5.5,14.4,5.5,19.9,0c5-5,5.5-14.4,0-19.9l-29.9-29.9l23.7-23.7l13,13c5.5,5.5,14.4,5.5,19.9,0c5.2-5.2,6.2-13.7,0-19.9 l-13-13l23.7-23.7l29.4,29.4c5.5,5.5,14.4,5.5,19.9,0c5.6-5.6,5.5-14.4,0-19.9l-29.4-29.5l24-24l65.8,65.7l-139.8,139.9 l-65.8-65.8L276.159,165.225z M39.359,92.825c-14.8-14.8-14.8-38.8,0-53.6c14.1-14.1,38.8-14.7,53.6,0l15.5,15.5l-53.6,53.6 L39.359,92.825z M99.759,473.025l-65.7-65.7l24-24l13.2,13.2c5.5,5.5,14.4,5.6,19.9,0c5.5-5.5,5.5-14.4,0-19.9l-13.1-13.3 l23.7-23.7l29.6,29.6c6.2,6.2,14.7,5.2,19.9,0c5.5-5.5,5.5-14.4,0-19.9l-29.6-29.6l23.7-23.7l13.2,13.2c5.5,5.5,14.9,5,19.9,0 c5.5-5.5,5.5-14.4,0-19.9l-13.2-13.2l8.8-8.8l65.8,65.7L99.759,473.025z M74.759,128.225l53.6-53.6l308.7,308.7l-53.6,53.6 L74.759,128.225z M409.659,450.725l41.3-41.3l20.7,61.9L409.659,450.725z"/>
</g>
</svg>;
const _cube = <svg viewBox="0 0 270.06 270.06" width="17px" height="17px">
<g>
<path d="M264.898,0.007
c-0.181,0.006-0.362,0.023-0.541,0.049H84.996c-1.326,0-2.598,0.527-3.535,1.465l-80,80C0.525,82.459-0.001,83.73,0,85.056v180
c0,2.761,2.239,5,5,5h180c1.326,0,2.598-0.527,3.535-1.465l80-80c0.938-0.938,1.465-2.209,1.465-3.535V5.819
c0.14-0.893,0.035-1.807-0.303-2.645c-0.016-0.045-0.033-0.09-0.051-0.135c-0.808-1.89-2.69-3.093-4.744-3.033L264.898,0.007z
M87.066,10.056h165.859l-70,70H17.066L87.066,10.056z M259.996,17.126v165.859l-70,70V87.126L259.996,17.126L259.996,17.126z
M84.92,19.985c-2.759,0.042-4.963,2.311-4.924,5.07v40c-0.039,2.761,2.168,5.032,4.929,5.071s5.032-2.168,5.071-4.929
c0.001-0.047,0.001-0.094,0-0.141v-40c0.039-2.761-2.168-5.031-4.93-5.07C85.018,19.984,84.969,19.984,84.92,19.985z M9.996,90.056
h170v170h-170V90.056L9.996,90.056z M84.92,99.985c-2.759,0.042-4.963,2.311-4.924,5.07v30c-0.039,2.761,2.168,5.032,4.929,5.071
s5.032-2.168,5.071-4.929c0.001-0.047,0.001-0.094,0-0.141v-30c0.039-2.761-2.168-5.031-4.93-5.07
C85.018,99.984,84.969,99.984,84.92,99.985z M84.92,159.985c-2.759,0.042-4.963,2.311-4.924,5.07v17.93L61.461,201.52
c-1.992,1.913-2.057,5.078-0.144,7.07c1.913,1.992,5.078,2.057,7.07,0.144c0.049-0.047,0.097-0.095,0.144-0.144l18.535-18.535h17.93
c2.761,0.039,5.032-2.168,5.071-4.929s-2.168-5.032-4.929-5.071c-0.047-0.001-0.094-0.001-0.141,0h-15v-15
c0.039-2.761-2.168-5.031-4.93-5.07C85.018,159.984,84.969,159.984,84.92,159.985z M134.996,180.055
c-2.761-0.039-5.032,2.168-5.071,4.929c-0.039,2.761,2.168,5.032,4.929,5.071c0.047,0.001,0.094,0.001,0.141,0h30
c2.761,0.039,5.032-2.168,5.071-4.929c0.039-2.761-2.168-5.032-4.929-5.071c-0.047-0.001-0.094-0.001-0.141,0H134.996z
M204.996,180.055c-2.761-0.039-5.032,2.168-5.071,4.929c-0.039,2.761,2.168,5.032,4.929,5.071c0.047,0.001,0.094,0.001,0.141,0h40
c2.761,0.039,5.032-2.168,5.071-4.929s-2.168-5.032-4.929-5.071c-0.047-0.001-0.094-0.001-0.141,0H204.996z M44.898,220.007
c-1.299,0.039-2.532,0.582-3.438,1.514l-20,20c-1.992,1.913-2.057,5.078-0.144,7.07c1.913,1.992,5.078,2.057,7.07,0.144
c0.049-0.047,0.097-0.095,0.144-0.144l20-20c1.98-1.925,2.025-5.091,0.1-7.071C47.654,220.514,46.3,219.965,44.898,220.007z"/>
<path d="M264.898,0.007 c-0.181,0.006-0.362,0.023-0.541,0.049H84.996c-1.326,0-2.598,0.527-3.535,1.465l-80,80C0.525,82.459-0.001,83.73,0,85.056v180 c0,2.761,2.239,5,5,5h180c1.326,0,2.598-0.527,3.535-1.465l80-80c0.938-0.938,1.465-2.209,1.465-3.535V5.819 c0.14-0.893,0.035-1.807-0.303-2.645c-0.016-0.045-0.033-0.09-0.051-0.135c-0.808-1.89-2.69-3.093-4.744-3.033L264.898,0.007z M87.066,10.056h165.859l-70,70H17.066L87.066,10.056z M259.996,17.126v165.859l-70,70V87.126L259.996,17.126L259.996,17.126z M84.92,19.985c-2.759,0.042-4.963,2.311-4.924,5.07v40c-0.039,2.761,2.168,5.032,4.929,5.071s5.032-2.168,5.071-4.929 c0.001-0.047,0.001-0.094,0-0.141v-40c0.039-2.761-2.168-5.031-4.93-5.07C85.018,19.984,84.969,19.984,84.92,19.985z M9.996,90.056 h170v170h-170V90.056L9.996,90.056z M84.92,99.985c-2.759,0.042-4.963,2.311-4.924,5.07v30c-0.039,2.761,2.168,5.032,4.929,5.071 s5.032-2.168,5.071-4.929c0.001-0.047,0.001-0.094,0-0.141v-30c0.039-2.761-2.168-5.031-4.93-5.07 C85.018,99.984,84.969,99.984,84.92,99.985z M84.92,159.985c-2.759,0.042-4.963,2.311-4.924,5.07v17.93L61.461,201.52 c-1.992,1.913-2.057,5.078-0.144,7.07c1.913,1.992,5.078,2.057,7.07,0.144c0.049-0.047,0.097-0.095,0.144-0.144l18.535-18.535h17.93 c2.761,0.039,5.032-2.168,5.071-4.929s-2.168-5.032-4.929-5.071c-0.047-0.001-0.094-0.001-0.141,0h-15v-15 c0.039-2.761-2.168-5.031-4.93-5.07C85.018,159.984,84.969,159.984,84.92,159.985z M134.996,180.055 c-2.761-0.039-5.032,2.168-5.071,4.929c-0.039,2.761,2.168,5.032,4.929,5.071c0.047,0.001,0.094,0.001,0.141,0h30 c2.761,0.039,5.032-2.168,5.071-4.929c0.039-2.761-2.168-5.032-4.929-5.071c-0.047-0.001-0.094-0.001-0.141,0H134.996z M204.996,180.055c-2.761-0.039-5.032,2.168-5.071,4.929c-0.039,2.761,2.168,5.032,4.929,5.071c0.047,0.001,0.094,0.001,0.141,0h40 c2.761,0.039,5.032-2.168,5.071-4.929s-2.168-5.032-4.929-5.071c-0.047-0.001-0.094-0.001-0.141,0H204.996z M44.898,220.007 c-1.299,0.039-2.532,0.582-3.438,1.514l-20,20c-1.992,1.913-2.057,5.078-0.144,7.07c1.913,1.992,5.078,2.057,7.07,0.144 c0.049-0.047,0.097-0.095,0.144-0.144l20-20c1.98-1.925,2.025-5.091,0.1-7.071C47.654,220.514,46.3,219.965,44.898,220.007z"/>
</g>
</svg>;
// Icons below are adapted from https://materialdesignicons.com/ and
// licensed with https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
const _cursor = <svg width="24px" height="24px" viewBox="0 0 24 24">
<path d="M10.07,14.27C10.57,14.03 11.16,14.25 11.4,14.75L13.7,19.74L15.5,18.89L13.19,13.91C12.95,13.41 13.17,12.81 13.67,12.58L13.95,12.5L16.25,12.05L8,5.12V15.9L9.82,14.43L10.07,14.27M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" />
</svg>;

View File

@@ -1073,7 +1073,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
type ObjectListEditorProps = { params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }
class ObjectListEditor extends React.PureComponent<ObjectListEditorProps, { current: object }> {
state = { current: void 0 as any };
state = { current: this.props.value };
onChangeParam: ParamOnChange = e => {
this.setState({ current: { ...this.state.current, [e.name]: e.value } });

View File

@@ -28,6 +28,13 @@ const ChannelParams = {
};
type ChannelParams = PD.Values<typeof ChannelParams>
const Bounds = new Map<VolumeStreaming.ChannelType, [number, number]>([
['em', [-5, 5]],
['2fo-fc', [0, 3]],
['fo-fc(+ve)', [1, 5]],
['fo-fc(-ve)', [-5, -1]],
]);
class Channel extends PluginUIComponent<{
label: string,
name: VolumeStreaming.ChannelType,
@@ -38,7 +45,8 @@ class Channel extends PluginUIComponent<{
changeIso: (name: string, value: number, isRelative: boolean) => void,
changeParams: (name: string, param: string, value: any) => void,
bCell: StateObjectCell,
isDisabled?: boolean
isDisabled?: boolean,
isUnbounded?: boolean
}> {
private ref = StateSelection.findTagInSubtree(this.plugin.state.data.tree, this.props.bCell!.transform.ref, this.props.name);
@@ -73,15 +81,31 @@ class Channel extends PluginUIComponent<{
const { min, max, mean, sigma } = stats;
const value = Math.round(100 * (channel.isoValue.kind === 'relative' ? channel.isoValue.relativeValue : channel.isoValue.absoluteValue)) / 100;
const relMin = (min - mean) / sigma;
const relMax = (max - mean) / sigma;
const step = toPrecision(isRelative ? Math.round(((max - min) / sigma)) / 100 : sigma / 100, 2);
let relMin = (min - mean) / sigma;
let relMax = (max - mean) / sigma;
if (!this.props.isUnbounded) {
const bounds = Bounds.get(this.props.name)!;
if (this.props.name === 'em') {
relMin = Math.max(bounds[0], relMin);
relMax = Math.min(bounds[1], relMax);
} else {
relMin = bounds[0];
relMax = bounds[1];
}
}
const vMin = mean + sigma * relMin, vMax = mean + sigma * relMax;
const step = toPrecision(isRelative ? Math.round(((vMax - vMin) / sigma)) / 100 : sigma / 100, 2);
const ctrlMin = isRelative ? relMin : vMin;
const ctrlMax = isRelative ? relMax : vMax;
return <ExpandableControlRow
label={props.label + (props.isRelative ? ' \u03C3' : '')}
colorStripe={channel.color}
pivot={<div className='msp-volume-channel-inline-controls'>
<Slider value={value} min={isRelative ? relMin : min} max={isRelative ? relMax : max} step={step}
<Slider value={value} min={ctrlMin} max={ctrlMax} step={step}
onChange={v => props.changeIso(props.name, v, isRelative)} disabled={props.params.isDisabled} onEnter={props.params.events.onEnter} />
<IconButton svg={this.getVisible() ? VisibilityOutlined : VisibilityOffOutlined} onClick={this.toggleVisible} toggleState={false} disabled={props.params.isDisabled} />
</div>}
@@ -163,6 +187,7 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
const isEM = b.info.kind === 'em';
const isRelative = value.params.isRelative;
const sampling = b.info.header.sampling[0];
const oldChannels = old.entry.params.channels as any;
@@ -180,6 +205,7 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
viewParams.bottomLeft = value.params.bottomLeft;
viewParams.topRight = value.params.topRight;
}
viewParams.isUnbounded = !!value.params.isUnbounded;
this.newParams({
...old,
@@ -221,28 +247,35 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
const isRelativeParam = PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' });
const isUnbounded = !!(params.entry.params.view.params as any).isUnbounded;
const isUnboundedParam = PD.Boolean(isUnbounded, { description: 'Show full/limited range of iso-values for more fine-grained control.', label: 'Unbounded' });
const isOff = params.entry.params.view.name === 'off';
// TODO: factor common things out, cache
const OptionsParams = {
entry: PD.Select(params.entry.name, b.data.entries.map(info => [info.dataId, info.dataId] as [string, string]), { isHidden: isOff, description: 'Which entry with volume data to display.' }),
view: PD.MappedStatic(params.entry.params.view.name, {
'off': PD.Group({
isRelative: PD.Boolean(isRelative, { isHidden: true })
isRelative: PD.Boolean(isRelative, { isHidden: true }),
isUnbounded: PD.Boolean(isUnbounded, { isHidden: true }),
}, { description: 'Display off.' }),
'box': PD.Group({
bottomLeft: PD.Vec3(Vec3.zero()),
topRight: PD.Vec3(Vec3.zero()),
detailLevel,
isRelative: isRelativeParam
isRelative: isRelativeParam,
isUnbounded: isUnboundedParam,
}, { description: 'Static box defined by cartesian coords.' }),
'selection-box': PD.Group({
radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
detailLevel,
isRelative: isRelativeParam
isRelative: isRelativeParam,
isUnbounded: isUnboundedParam,
}, { description: 'Box around focused element.' }),
'cell': PD.Group({
detailLevel,
isRelative: isRelativeParam
isRelative: isRelativeParam,
isUnbounded: isUnboundedParam,
}, { description: 'Box around the structure\'s bounding box.' }),
// 'auto': PD.Group({ }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
}, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Whole Structure" shows the volume for the whole structure.' })
@@ -256,7 +289,8 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
radius: (params.entry.params.view.params as any).radius,
bottomLeft: (params.entry.params.view.params as any).bottomLeft,
topRight: (params.entry.params.view.params as any).topRight,
isRelative
isRelative,
isUnbounded
}
}
};
@@ -266,10 +300,10 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
}
return <>
{!isEM && <Channel label='2Fo-Fc' name='2fo-fc' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
{!isEM && <Channel label='Fo-Fc(+ve)' name='fo-fc(+ve)' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
{!isEM && <Channel label='Fo-Fc(-ve)' name='fo-fc(-ve)' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
{isEM && <Channel label='EM' name='em' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
{!isEM && <Channel label='2Fo-Fc' name='2fo-fc' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} isUnbounded={isUnbounded} />}
{!isEM && <Channel label='Fo-Fc(+ve)' name='fo-fc(+ve)' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} isUnbounded={isUnbounded} />}
{!isEM && <Channel label='Fo-Fc(-ve)' name='fo-fc(-ve)' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} isUnbounded={isUnbounded} />}
{isEM && <Channel label='EM' name='em' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} isUnbounded={isUnbounded} />}
<ParameterControls onChange={this.changeOption} params={OptionsParams} values={options} onEnter={this.props.events.onEnter} isDisabled={this.props.isDisabled} />
</>;

View File

@@ -239,7 +239,7 @@
// TODO : get rid of the important
.msp-control-group-header {
background: $default-background;
> button {
> button, div {
padding-left: 4px; // $control-spacing / 2 !important;
text-align: left;
height: 24px !important; // 2 * $row-height / 3 !important;

View File

@@ -427,8 +427,7 @@
position: relative;
// display: inline-block;
margin: $control-spacing auto 0 auto;
width: 400px;
line-height: $row-height;
width: 430px;
&-actions {
position: absolute;

View File

@@ -100,15 +100,16 @@
}
&-disabled {
background-color: #e9e9e9;
.msp-slider-base-track {
background-color: $slider-disabledColor;
}
background: $default-background;
opacity: 0.35;
// .msp-slider-base-track {
// background-color: $slider-disabledColor;
// }
.msp-slider-base-handle, .msp-slider-base-dot {
border-color: $slider-disabledColor;
background-color: #fff;
// border-color: $slider-disabledColor;
// background-color: color-lower-contrast($font-color, 10%);
cursor: not-allowed;
}

View File

@@ -37,14 +37,15 @@ export class AnimationControls extends PluginUIComponent<{ onStart?: () => void
if (anim.isEmpty) return null;
const isDisabled = anim.state.animationState === 'playing';
const canApply = anim.current.anim.canApply?.(this.plugin);
return <>
<ParameterControls params={anim.getParams()} values={anim.state.params} onChange={this.updateParams} isDisabled={isDisabled} />
<ParameterControls params={anim.current.params} values={anim.current.paramValues} onChange={this.updateCurrentParams} isDisabled={isDisabled} />
<div className='msp-flex-row'>
<Button icon={anim.state.animationState !== 'playing' ? void 0 : PlayArrow} onClick={this.startOrStop}>
{anim.state.animationState === 'playing' ? 'Stop' : 'Start'}
<Button icon={anim.state.animationState !== 'playing' ? void 0 : PlayArrow} onClick={this.startOrStop} disabled={canApply !== void 0 && canApply.canApply === false}>
{anim.state.animationState === 'playing' ? 'Stop' : canApply === void 0 || canApply.canApply ? 'Start' : canApply.reason || 'Start'}
</Button>
</div>
</>;

View File

@@ -14,6 +14,7 @@ import OpenInBrowser from '@material-ui/icons/OpenInBrowser';
import SaveOutlined from '@material-ui/icons/SaveOutlined';
import SwapHoriz from '@material-ui/icons/SwapHoriz';
import Refresh from '@material-ui/icons/Refresh';
import Warning from '@material-ui/icons/Warning';
import { OrderedMap } from 'immutable';
import * as React from 'react';
import { PluginCommands } from '../../mol-plugin/commands';
@@ -73,17 +74,22 @@ export class StateExportImportControls extends PluginUIComponent<{ onAction?: ()
}
render() {
return <div className='msp-flex-row'>
<Button icon={GetApp} onClick={this.downloadToFileJson} title='Save the state description. Input data are loaded using the provided sources. Does not work if local files are used as input.'>
State
</Button>
<Button icon={GetApp} onClick={this.downloadToFileZip} title='Save the state including the input data.'>
Session
</Button>
<div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
<Icon svg={OpenInBrowser} inline /> Open <input onChange={this.open} type='file' multiple={false} accept='.molx,.molj' />
return <>
<div className='msp-flex-row'>
<Button icon={GetApp} onClick={this.downloadToFileJson} title='Save the state description. Input data are loaded using the provided sources. Does not work if local files are used as input.'>
State
</Button>
<Button icon={GetApp} onClick={this.downloadToFileZip} title='Save the state including the input data.'>
Session
</Button>
<div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
<Icon svg={OpenInBrowser} inline /> Open <input onChange={this.open} type='file' multiple={false} accept='.molx,.molj' />
</div>
</div>
</div>;
<div className='msp-help-text' style={{ padding: '10px'}}>
<Icon svg={Warning} /> This is an experimental feature and stored states/sessions might not be openable in a future version.
</div>
</>;
}
}
@@ -211,11 +217,18 @@ export class RemoteStateSnapshots extends PluginUIComponent<
}, { isFlat: true })
};
private _mounted = false;
componentDidMount() {
this.refresh();
// TODO: solve this by using "PluginComponent" with behaviors intead
this._mounted = true;
// this.subscribe(UploadedEvent, this.refresh);
}
componentWillUnmount() {
this._mounted = false;
}
serverUrl(q?: string) {
if (!q) return this.state.params.options.serverUrl;
return urlCombine(this.state.params.options.serverUrl, q);
@@ -242,10 +255,10 @@ export class RemoteStateSnapshots extends PluginUIComponent<
});
}
this.setState({ entries: entries.asImmutable(), isBusy: false });
if (this._mounted) this.setState({ entries: entries.asImmutable(), isBusy: false });
} catch (e) {
this.plugin.log.error('Fetching Remote Snapshots: ' + e);
this.setState({ entries: OrderedMap(), isBusy: false });
if (this._mounted) this.setState({ entries: OrderedMap(), isBusy: false });
}
}
@@ -260,11 +273,15 @@ export class RemoteStateSnapshots extends PluginUIComponent<
serverUrl: this.state.params.options.serverUrl
});
this.setState({ isBusy: false });
this.plugin.log.message('Snapshot uploaded.');
this.refresh();
if (this._mounted) {
this.setState({ isBusy: false });
this.refresh();
}
}
fetch = async (e: React.MouseEvent<HTMLElement>) => {
const id = e.currentTarget.getAttribute('data-id');
if (!id) return;
@@ -275,7 +292,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
try {
await PluginCommands.State.Snapshots.Fetch(this.plugin, { url: entry.url });
} finally {
this.setState({ isBusy: false });
if (this._mounted) this.setState({ isBusy: false });
}
}

View File

@@ -24,7 +24,7 @@ import { ParamDefinition } from '../../mol-util/param-definition';
import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { Button, ExpandGroup, IconButton, ToggleButton } from '../controls/common';
import { CubeSvg, Intersect, SetSvg, Subtract, Union } from '../controls/icons';
import { CubeSvg, IntersectSvg, SetSvg, SubtractSvg, UnionSvg } from '../controls/icons';
import { ParameterControls } from '../controls/parameters';
import { UpdateTransformControl } from '../state/update-transform';
import { GenericEntryListControls } from './generic';
@@ -267,9 +267,9 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
if (mng.canBeModified(this.props.group[0])) {
ret.push([
ActionMenu.Header('Modify by Selection'),
ActionMenu.Item('Include', () => mng.modifyByCurrentSelection(this.props.group, 'union'), { icon: Union }),
ActionMenu.Item('Subtract', () => mng.modifyByCurrentSelection(this.props.group, 'subtract'), { icon: Subtract }),
ActionMenu.Item('Intersect', () => mng.modifyByCurrentSelection(this.props.group, 'intersect'), { icon: Intersect })
ActionMenu.Item('Include', () => mng.modifyByCurrentSelection(this.props.group, 'union'), { icon: UnionSvg }),
ActionMenu.Item('Subtract', () => mng.modifyByCurrentSelection(this.props.group, 'subtract'), { icon: SubtractSvg }),
ActionMenu.Item('Intersect', () => mng.modifyByCurrentSelection(this.props.group, 'intersect'), { icon: IntersectSvg })
]);
}

View File

@@ -11,10 +11,9 @@ import ArrowUpward from '@material-ui/icons/ArrowUpward';
import DeleteOutlined from '@material-ui/icons/DeleteOutlined';
import HelpOutline from '@material-ui/icons/HelpOutline';
import MoreHoriz from '@material-ui/icons/MoreHoriz';
import RemoveOutlined from '@material-ui/icons/RemoveOutlined';
import Tune from '@material-ui/icons/Tune';
import VisibilityOutlined from '@material-ui/icons/VisibilityOutlined';
import VisibilityOffOutlined from '@material-ui/icons/VisibilityOffOutlined';
import VisibilityOutlined from '@material-ui/icons/VisibilityOutlined';
import * as React from 'react';
import { Loci } from '../../mol-model/loci';
import { StructureElement } from '../../mol-model/structure';
@@ -27,9 +26,10 @@ import { FiniteArray } from '../../mol-util/type-helpers';
import { CollapsableControls, PurePluginUIComponent } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { Button, ExpandGroup, IconButton, ToggleButton } from '../controls/common';
import { Icon, SetSvg, RulerSvg } from '../controls/icons';
import { Icon, RulerSvg, SetSvg } from '../controls/icons';
import { ParameterControls } from '../controls/parameters';
import { UpdateTransformControl } from '../state/update-transform';
import { ToggleSelectionModeButton } from './selection';
// TODO details, options (e.g. change text for labels)
@@ -172,7 +172,7 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
{entries}
</div>}
{entries.length === 0 && <div className='msp-control-offset msp-help-text'>
<div className='msp-help-description'><Icon svg={HelpOutline} inline />Add one or more selections</div>
<div className='msp-help-description'><Icon svg={HelpOutline} inline />Add one or more selections (toggle <ToggleSelectionModeButton inline /> mode)</div>
</div>}
</>;
}
@@ -298,7 +298,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
<span dangerouslySetInnerHTML={{ __html: this.label }} />
</button>
<IconButton svg={cell.state.isHidden ? VisibilityOffOutlined : VisibilityOutlined} toggleState={false} small className='msp-form-control' onClick={this.toggleVisibility} flex title={cell.state.isHidden ? 'Show' : 'Hide'} />
<IconButton svg={RemoveOutlined} small className='msp-form-control' onClick={this.delete} flex title='Delete' />
<IconButton svg={DeleteOutlined} small className='msp-form-control' onClick={this.delete} flex title='Delete' toggleState={false} />
<IconButton svg={MoreHoriz} className='msp-form-control' onClick={this.toggleUpdate} flex title='Actions' toggleState={this.state.showUpdate} />
</div>
{this.state.showUpdate && cell.parent && <>

View File

@@ -10,6 +10,7 @@ import CancelOutlined from '@material-ui/icons/CancelOutlined';
import Brush from '@material-ui/icons/Brush';
import Restore from '@material-ui/icons/Restore';
import Remove from '@material-ui/icons/Remove';
import HelpOutline from '@material-ui/icons/HelpOutline';
import * as React from 'react';
import { StructureSelectionQueries, StructureSelectionQuery, getNonStandardResidueQueries, getElementQueries, getPolymerAndBranchedEntityQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
@@ -23,9 +24,31 @@ import { PluginUIComponent, PurePluginUIComponent } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { Button, ControlGroup, IconButton, ToggleButton } from '../controls/common';
import { ParameterControls, ParamOnChange, PureSelectControl } from '../controls/parameters';
import { Union, Subtract, Intersect, SetSvg as SetSvg, CubeSvg } from '../controls/icons';
import { UnionSvg, SubtractSvg, IntersectSvg, SetSvg, CubeSvg, Icon, CursorSvg } from '../controls/icons';
import { AddComponentControls } from './components';
import Structure from '../../mol-model/structure/structure/structure';
import { ViewportHelpContent, HelpGroup, HelpText } from '../viewport/help';
export class ToggleSelectionModeButton extends PurePluginUIComponent<{ inline?: boolean }> {
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
this.subscribe(this.plugin.behaviors.interaction.selectionMode, () => this.forceUpdate());
}
_toggleSelMode = () => {
this.plugin.selectionMode = !this.plugin.selectionMode;
}
render() {
const style = this.props.inline
? { background: 'transparent', width: 'auto', height: 'auto', lineHeight: 'unset' }
: { background: 'transparent' };
return <IconButton svg={CursorSvg} onClick={this._toggleSelMode} title={'Toggle Selection Mode'} style={style} toggleState={this.plugin.selectionMode} />;
}
}
const StructureSelectionParams = {
granularity: InteractivityManager.Params.granularity,
@@ -36,7 +59,7 @@ interface StructureSelectionActionsControlsState {
isBusy: boolean,
canUndo: boolean,
action?: StructureSelectionModifier | 'color' | 'add-repr'
action?: StructureSelectionModifier | 'color' | 'add-repr' | 'help'
}
const ActionHeader = new Map<StructureSelectionModifier, string>([
@@ -144,6 +167,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
toggleSet = this.showAction('set')
toggleColor = this.showAction('color')
toggleAddRepr = this.showAction('add-repr')
toggleHelp = this.showAction('help')
setGranuality: ParamOnChange = ({ value }) => {
this.plugin.managers.interactivity.setProps({ granularity: value });
@@ -173,9 +197,9 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
return <>
<div className='msp-flex-row' style={{ background: 'none' }}>
<PureSelectControl title={`Picking Level for selecting and highlighting`} param={StructureSelectionParams.granularity} name='granularity' value={granularity} onChange={this.setGranuality} isDisabled={this.isDisabled} />
<ToggleButton icon={Union} title={`${ActionHeader.get('add')}. Hold shift key to keep menu open.`} toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
<ToggleButton icon={Subtract} title={`${ActionHeader.get('remove')}. Hold shift key to keep menu open.`} toggle={this.toggleRemove} isSelected={this.state.action === 'remove'} disabled={this.isDisabled} />
<ToggleButton icon={Intersect} title={`${ActionHeader.get('intersect')}. Hold shift key to keep menu open.`} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
<ToggleButton icon={UnionSvg} title={`${ActionHeader.get('add')}. Hold shift key to keep menu open.`} toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
<ToggleButton icon={SubtractSvg} title={`${ActionHeader.get('remove')}. Hold shift key to keep menu open.`} toggle={this.toggleRemove} isSelected={this.state.action === 'remove'} disabled={this.isDisabled} />
<ToggleButton icon={IntersectSvg} title={`${ActionHeader.get('intersect')}. Hold shift key to keep menu open.`} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
<ToggleButton icon={SetSvg} title={`${ActionHeader.get('set')}. Hold shift key to keep menu open.`} toggle={this.toggleSet} isSelected={this.state.action === 'set'} disabled={this.isDisabled} />
<ToggleButton icon={Brush} title='Color Selection' toggle={this.toggleColor} isSelected={this.state.action === 'color'} disabled={this.isDisabled} style={{ marginLeft: '10px' }} />
@@ -183,9 +207,10 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
<IconButton svg={Remove} title='Subtract Selection from Representations' onClick={this.subtract} disabled={this.isDisabled} />
<IconButton svg={Restore} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} title={undoTitle} />
<IconButton svg={CancelOutlined} title='Turn selection mode off' onClick={this.turnOff} style={{ marginLeft: '10px' }} />
<ToggleButton icon={HelpOutline} title='Show/hide help' toggle={this.toggleHelp} style={{ marginLeft: '10px' }} isSelected={this.state.action === 'help'} />
<IconButton svg={CancelOutlined} title='Turn selection mode off' onClick={this.turnOff} />
</div>
{(this.state.action && this.state.action !== 'color' && this.state.action !== 'add-repr') && <div className='msp-selection-viewport-controls-actions'>
{(this.state.action && this.state.action !== 'color' && this.state.action !== 'add-repr' && this.state.action !== 'help') && <div className='msp-selection-viewport-controls-actions'>
<ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} title='Click to close.' items={this.queries} onSelect={this.selectQuery} noOffset />
</div>}
{this.state.action === 'color' && <div className='msp-selection-viewport-controls-actions'>
@@ -198,6 +223,17 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
<AddComponentControls onApply={this.toggleAddRepr} forSelection />
</ControlGroup>
</div>}
{this.state.action === 'help' && <div className='msp-selection-viewport-controls-actions'>
<ControlGroup header='Help' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleHelp} topRightIcon={Close} maxHeight='300px'>
<HelpGroup header='Selection Operations'>
<HelpText>Use <Icon svg={UnionSvg} inline /> <Icon svg={SubtractSvg} inline /> <Icon svg={IntersectSvg} inline /> <Icon svg={SetSvg} inline /> to modify the selection.</HelpText>
</HelpGroup>
<HelpGroup header='Representation Operations'>
<HelpText>Use <Icon svg={Brush} inline /> <Icon svg={CubeSvg} inline /> <Icon svg={Remove} inline /> <Icon svg={Restore} inline /> to color, create selection/representation, subtract it, or undo actions.</HelpText>
</HelpGroup>
<ViewportHelpContent selectOnly={true} />
</ControlGroup>
</div>}
</>;
}
}

View File

@@ -254,6 +254,7 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
const { selection } = this.plugin.managers.structure.hierarchy;
if (selection.structures.length !== 1) return null;
const pivot = selection.structures[0];
if (!pivot.cell.parent) return null;
const t = StateSelection.tryFindDecorator(this.plugin.state.data, pivot.cell.transform.ref, StateTransforms.Model.TransformStructureConformation);
if (!t) return;

View File

@@ -0,0 +1,323 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react';
import FlipToFrontIcon from '@material-ui/icons/FlipToFront';
import HelpOutline from '@material-ui/icons/HelpOutline';
import LinearScaleIcon from '@material-ui/icons/LinearScale';
import ScatterPlotIcon from '@material-ui/icons/ScatterPlot';
import ArrowDownward from '@material-ui/icons/ArrowDownward';
import ArrowUpward from '@material-ui/icons/ArrowUpward';
import DeleteOutlined from '@material-ui/icons/DeleteOutlined';
import Tune from '@material-ui/icons/Tune';
import { CollapsableControls, PurePluginUIComponent } from '../base';
import { Icon } from '../controls/icons';
import { Button, ToggleButton, IconButton } from '../controls/common';
import { StructureElement, StructureSelection, QueryContext, Structure, StructureProperties } from '../../mol-model/structure';
import { Mat4 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { StateObjectRef, StateObjectCell, StateSelection } from '../../mol-state';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { alignAndSuperpose, superpose } from '../../mol-model/structure/structure/util/superposition';
import { StructureSelectionQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
import { structureElementStatsLabel, elementLabel } from '../../mol-theme/label';
import { ParameterControls } from '../controls/parameters';
import { stripTags } from '../../mol-util/string';
import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
import { ToggleSelectionModeButton } from './selection';
export class StructureSuperpositionControls extends CollapsableControls {
defaultState() {
return {
isCollapsed: false,
header: 'Superposition',
brand: { accent: 'gray' as const, svg: FlipToFrontIcon },
isHidden: true
};
}
componentDidMount() {
this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => {
this.setState({ isHidden: sel.structures.length < 2 });
});
}
renderControls() {
return <>
<SuperpositionControls />
</>;
}
}
export const StructureSuperpositionParams = {
alignSequences: PD.Boolean(true, { isEssential: true, description: 'Perform a sequence alignment and use the aligned residue pairs to guide the 3D superposition.' }),
};
const DefaultStructureSuperpositionOptions = PD.getDefaultValues(StructureSuperpositionParams);
export type StructureSuperpositionOptions = PD.ValuesFor<typeof StructureSuperpositionParams>
const SuperpositionTag = 'SuperpositionTransform';
type SuperpositionControlsState = {
isBusy: boolean,
action?: 'byChains' | 'byAtoms' | 'options',
options: StructureSuperpositionOptions
}
interface LociEntry {
loci: StructureElement.Loci,
label: string,
cell: StateObjectCell<PluginStateObject.Molecule.Structure>
};
interface AtomsLociEntry extends LociEntry {
atoms: StructureSelectionHistoryEntry[]
};
export class SuperpositionControls extends PurePluginUIComponent<{}, SuperpositionControlsState> {
state: SuperpositionControlsState = {
isBusy: false,
action: undefined,
options: DefaultStructureSuperpositionOptions
}
componentDidMount() {
this.subscribe(this.selection.events.changed, () => {
this.forceUpdate();
});
this.subscribe(this.selection.events.additionsHistoryUpdated, () => {
this.forceUpdate();
});
this.subscribe(this.plugin.behaviors.state.isBusy, v => {
this.setState({ isBusy: v });
});
}
get selection() {
return this.plugin.managers.structure.selection;
}
async transform(s: StateObjectRef<PluginStateObject.Molecule.Structure>, matrix: Mat4) {
const r = StateObjectRef.resolveAndCheck(this.plugin.state.data, s);
if (!r) return;
// TODO should find any TransformStructureConformation decorator instance
const o = StateSelection.findTagInSubtree(this.plugin.state.data.tree, r.transform.ref, SuperpositionTag);
const params = {
transform: {
name: 'matrix' as const,
params: { data: matrix, transpose: false }
}
};
// TODO add .insertOrUpdate to StateBuilder?
let b = o
? this.plugin.state.data.build().to(o).update(params)
: this.plugin.state.data.build().to(s)
.insert(StateTransforms.Model.TransformStructureConformation, params, { tags: SuperpositionTag });
await this.plugin.runTask(this.plugin.state.data.updateTree(b));
}
superposeChains = async () => {
const { query } = StructureSelectionQueries.trace;
const entries = this.chainEntries;
const traceLocis = entries.map((e, i) => {
const s = StructureElement.Loci.toStructure(e.loci);
const loci = StructureSelection.toLociWithSourceUnits(query(new QueryContext(s)));
return StructureElement.Loci.remap(loci, i === 0
? this.plugin.helpers.substructureParent.get(e.loci.structure.root)!.obj!.data
: loci.structure.root
);
});
const transforms = this.state.options.alignSequences
? alignAndSuperpose(traceLocis)
: superpose(traceLocis);
const eA = entries[0];
for (let i = 1, il = traceLocis.length; i < il; ++i) {
const eB = entries[i];
const { bTransform, rmsd } = transforms[i - 1];
await this.transform(eB.cell, bTransform);
const labelA = stripTags(eA.label);
const labelB = stripTags(eB.label);
this.plugin.log.info(`Superposed [${labelA}] and [${labelB}] with RMSD ${rmsd.toFixed(2)}.`);
}
}
superposeAtoms = async () => {
const entries = this.atomEntries;
const atomLocis = entries.map((e, i) => {
return StructureElement.Loci.remap(e.loci, i === 0
? this.plugin.helpers.substructureParent.get(e.loci.structure.root)!.obj!.data
: e.loci.structure.root
);
});
const transforms = superpose(atomLocis);
const eA = entries[0];
for (let i = 1, il = atomLocis.length; i < il; ++i) {
const eB = entries[i];
const { bTransform, rmsd } = transforms[i - 1];
await this.transform(eB.cell, bTransform);
const labelA = stripTags(eA.label);
const labelB = stripTags(eB.label);
const count = entries[i].atoms.length;
this.plugin.log.info(`Superposed ${count} ${count === 1 ? 'atom' : 'atoms'} of [${labelA}] and [${labelB}] with RMSD ${rmsd.toFixed(2)}.`);
}
}
toggleByChains = () => this.setState({ action: this.state.action === 'byChains' ? void 0 : 'byChains' });
toggleByAtoms = () => this.setState({ action: this.state.action === 'byAtoms' ? void 0 : 'byAtoms' });
toggleOptions = () => this.setState({ action: this.state.action === 'options' ? void 0 : 'options' });
highlight(loci: StructureElement.Loci) {
this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci }, false);
}
moveHistory(e: StructureSelectionHistoryEntry, direction: 'up' | 'down') {
this.plugin.managers.structure.selection.modifyHistory(e, direction, void 0, true);
}
focusLoci(loci: StructureElement.Loci) {
this.plugin.managers.camera.focusLoci(loci);
}
lociEntry(e: LociEntry, idx: number) {
return <div className='msp-flex-row' key={idx}>
<Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={this.plugin.managers.interactivity.lociHighlights.clearHighlights}>
<span dangerouslySetInnerHTML={{ __html: e.label }} />
</Button>
</div>;
}
historyEntry(e: StructureSelectionHistoryEntry, idx: number) {
const history = this.plugin.managers.structure.selection.additionsHistory;
return <div className='msp-flex-row' key={e.id}>
<Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={this.plugin.managers.interactivity.lociHighlights.clearHighlights}>
{idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} />
</Button>
{history.length > 1 && <IconButton svg={ArrowUpward} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} flex='20px' title={'Move up'} />}
{history.length > 1 && <IconButton svg={ArrowDownward} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'down')} flex='20px' title={'Move down'} />}
<IconButton svg={DeleteOutlined} small={true} className='msp-form-control' onClick={() => this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} flex title={'Remove'} />
</div>;
}
atomsLociEntry(e: AtomsLociEntry, idx: number) {
return <div key={idx}>
<div className='msp-control-group-header'>
<div className='msp-no-overflow' title={e.label}>{e.label}</div>
</div>
<div className='msp-control-offset'>
{e.atoms.map((h, i) => this.historyEntry(h, i))}
</div>
</div>;
}
get chainEntries() {
const location = StructureElement.Location.create();
const entries: LociEntry[] = [];
this.plugin.managers.structure.selection.entries.forEach(({ selection }, ref) => {
const cell = StateObjectRef.resolveAndCheck(this.plugin.state.data, ref);
if (!cell || StructureElement.Loci.isEmpty(selection)) return;
// only single polymer chain selections
const l = StructureElement.Loci.getFirstLocation(selection, location)!;
if (selection.elements.length > 1 || StructureProperties.entity.type(l) !== 'polymer') return;
const stats = StructureElement.Stats.ofLoci(selection);
const counts = structureElementStatsLabel(stats, { countsOnly: true });
const chain = elementLabel(l, { reverse: true, granularity: 'chain' }).split('|');
const label = `${counts} | ${chain[0]} | ${chain[chain.length - 1]}`;
entries.push({ loci: selection, label, cell });
});
return entries;
}
get atomEntries() {
const structureEntries = new Map<Structure, StructureSelectionHistoryEntry[]>();
const history = this.plugin.managers.structure.selection.additionsHistory;
for (let i = 0, il = history.length; i < il; ++i) {
const e = history[i];
if (StructureElement.Loci.size(e.loci) !== 1) continue;
const k = e.loci.structure;
if (structureEntries.has(k)) structureEntries.get(k)!.push(e);
else structureEntries.set(k, [e]);
}
const entries: AtomsLociEntry[] = [];
structureEntries.forEach((atoms, structure) => {
const cell = this.plugin.helpers.substructureParent.get(structure)!;
const elements: StructureElement.Loci['elements'][0][] = [];
for (let i = 0, il = atoms.length; i < il; ++i) {
// note, we don't do loci union here to keep order of selected atoms
// for atom pairing during superposition
elements.push(atoms[i].loci.elements[0]);
}
const loci = StructureElement.Loci(atoms[0].loci.structure, elements);
const label = loci.structure.label.split(' | ')[0];
entries.push({ loci, label, cell, atoms });
});
return entries;
}
addByChains() {
const entries = this.chainEntries;
return <>
{entries.length > 0 && <div className='msp-control-offset'>
{entries.map((e, i) => this.lociEntry(e, i))}
</div>}
{entries.length < 2 && <div className='msp-control-offset msp-help-text'>
<div className='msp-help-description'><Icon svg={HelpOutline} inline />Add 2 or more selections (toggle <ToggleSelectionModeButton inline /> mode) from separate structures. Selections must be limited to single polymer chains or residues therein.</div>
</div>}
{entries.length > 1 && <Button title='Superpose structures by selected chains.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeChains} style={{ marginTop: '1px' }}>
Superpose
</Button>}
</>;
}
addByAtoms() {
const entries = this.atomEntries;
return <>
{entries.length > 0 && <div className='msp-control-offset'>
{entries.map((e, i) => this.atomsLociEntry(e, i))}
</div>}
{entries.length < 2 && <div className='msp-control-offset msp-help-text'>
<div className='msp-help-description'><Icon svg={HelpOutline} inline />Add 1 or more selections (toggle <ToggleSelectionModeButton inline /> mode) from
separate structures. Selections must be limited to single atoms.</div>
</div>}
{entries.length > 1 && <Button title='Superpose structures by selected atoms.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeAtoms} style={{ marginTop: '1px' }}>
Superpose
</Button>}
</>;
}
private setOptions = (values: StructureSuperpositionOptions) => {
this.setState({ options: values });
}
render() {
return <>
<div className='msp-flex-row'>
<ToggleButton icon={LinearScaleIcon} label='By Chains' toggle={this.toggleByChains} isSelected={this.state.action === 'byChains'} disabled={this.state.isBusy} />
<ToggleButton icon={ScatterPlotIcon} label='By Atoms' toggle={this.toggleByAtoms} isSelected={this.state.action === 'byAtoms'} disabled={this.state.isBusy} />
<ToggleButton icon={Tune} label='' title='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isBusy} style={{ flex: '0 0 40px', padding: 0 }} />
</div>
{this.state.action === 'byChains' && this.addByChains()}
{this.state.action === 'byAtoms' && this.addByAtoms()}
{this.state.action === 'options' && <div className='msp-control-offset'>
<ParameterControls params={StructureSuperpositionParams} values={this.state.options} onChangeValues={this.setOptions} isDisabled={this.state.isBusy} />
</div>}
</>;
}
}

View File

@@ -94,7 +94,7 @@ export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStrea
if (!pivot.cell.parent) return null;
const bindings = pivot.volumeStreaming?.cell.transform.params?.entry.params.view.name === 'selection-box' && this.plugin.state.behaviors.cells.get(FocusLoci.id)?.params?.values?.bindings;
return <>
<UpdateTransformControl state={pivot.cell.parent} transform={pivot.volumeStreaming!.cell.transform} customHeader='none' noMargin autoHideApply />
<UpdateTransformControl state={pivot.cell.parent} transform={pivot.volumeStreaming!.cell.transform} customHeader='none' noMargin />
{bindings && <ExpandGroup header='Controls Help'>
<BindingsHelp bindings={bindings} />
</ExpandGroup>}

View File

@@ -9,16 +9,18 @@ import Autorenew from '@material-ui/icons/Autorenew';
import BuildOutlined from '@material-ui/icons/BuildOutlined';
import CameraOutlined from '@material-ui/icons/CameraOutlined';
import Close from '@material-ui/icons/Close';
import Crop from '@material-ui/icons/Crop';
import Fullscreen from '@material-ui/icons/Fullscreen';
import Tune from '@material-ui/icons/Tune';
import * as React from 'react';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { resizeCanvas } from '../mol-canvas3d/util';
import { PluginCommands } from '../mol-plugin/commands';
import { PluginConfig } from '../mol-plugin/config';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base';
import { ControlGroup, IconButton } from './controls/common';
import { ToggleSelectionModeButton } from './structure/selection';
import { DownloadScreenshotControls } from './viewport/screenshot';
import { SimpleSettingsControl } from './viewport/simple-settings';
@@ -60,10 +62,6 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
PluginCommands.Layout.Update(this.plugin, { state: { isExpanded: !this.plugin.layout.state.isExpanded } });
}
toggleSelectionMode = () => {
this.plugin.selectionMode = !this.plugin.selectionMode;
}
setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
}
@@ -79,7 +77,6 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
this.subscribe(this.plugin.behaviors.interaction.selectionMode, () => this.forceUpdate());
}
icon(icon: React.FC, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
@@ -110,7 +107,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
</div>
{this.plugin.config.get(PluginConfig.Viewport.ShowSelectionMode) && <div>
<div className='msp-semi-transparent-background' />
{this.icon(Crop, this.toggleSelectionMode, 'Toggle Selection Mode', this.plugin.behaviors.interaction.selectionMode.value)}
<ToggleSelectionModeButton />
</div>}
</div>
{this.state.isScreenshotExpanded && <div className='msp-viewport-controls-panel'>
@@ -169,13 +166,15 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
const canvas3d = this.plugin.canvas3d!;
this.subscribe(canvas3d.reprCount, this.handleLogo);
this.subscribe(canvas3d.input.resize, this.handleResize);
const resized = new Subject();
const resize = () => resized.next();
this.subscribe(resized.pipe(debounceTime(1000 / 24)), () => this.handleResize());
this.subscribe(canvas3d.input.resize, resize);
this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.interaction.click.next(e));
this.subscribe(canvas3d.interaction.hover, e => this.plugin.behaviors.interaction.hover.next(e));
this.subscribe(this.plugin.layout.events.updated, () => {
setTimeout(this.handleResize, 50);
});
this.subscribe(this.plugin.layout.events.updated, resize);
}
componentWillUnmount() {

View File

@@ -40,7 +40,7 @@ export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]:
}
}
class HelpText extends React.PureComponent {
export class HelpText extends React.PureComponent {
render() {
return <div className='msp-help-text'>
<div>{this.props.children}</div>
@@ -48,7 +48,7 @@ class HelpText extends React.PureComponent {
}
}
class HelpGroup extends React.PureComponent<{ header: string, initiallyExpanded?: boolean }, { isExpanded: boolean }> {
export class HelpGroup extends React.PureComponent<{ header: string, initiallyExpanded?: boolean }, { isExpanded: boolean }> {
state = {
header: this.props.header,
isExpanded: !!this.props.initiallyExpanded
@@ -75,7 +75,7 @@ function HelpSection(props: { header: string }) {
return <div className='msp-simple-help-section'>{props.header}</div>;
}
export class ViewportHelpContent extends PluginUIComponent {
export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolean }> {
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
}
@@ -87,10 +87,10 @@ export class ViewportHelpContent extends PluginUIComponent {
if (bindings) Object.assign(interactionBindings, bindings);
});
return <>
{this.plugin.canvas3d && <HelpGroup key='trackball' header='Moving in 3D'>
{(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
<BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
</HelpGroup>}
<HelpGroup key='interactions' header='Select, Highlight, Focus'>
<HelpGroup key='interactions' header='Mouse Controls'>
<BindingsHelp bindings={interactionBindings} />
</HelpGroup>
</>;

View File

@@ -16,9 +16,7 @@ import { Button, ExpandGroup } from '../controls/common';
import { CameraHelperProps } from '../../mol-canvas3d/helper/camera-helper';
import GetApp from '@material-ui/icons/GetApp';
import Launch from '@material-ui/icons/Launch';
import Warning from '@material-ui/icons/Warning';
import { PluginCommands } from '../../mol-plugin/commands';
import { Icon } from '../controls/icons';
import { StateExportImportControls, LocalStateSnapshotParams } from '../state/snapshots';
interface ImageControlsState {
@@ -152,9 +150,6 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
<ExpandGroup header='Save Options' initiallyExpanded={false} noOffset>
<LocalStateSnapshotParams />
</ExpandGroup>
<div className='msp-help-text' style={{ padding: '10px'}}>
<Icon svg={Warning} /> This is an experimental feature and stored states might not be openable in a future version.
</div>
</ExpandGroup>
</div>;
}

View File

@@ -18,19 +18,19 @@ import { SizeTheme } from '../../../../mol-theme/size';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { PluginCommands } from '../../../commands';
import { PluginContext } from '../../../context';
import { Color } from '../../../../mol-util/color';
const StructureFocusRepresentationParams = (plugin: PluginContext) => {
const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(void 0, plugin) as PD.Params;
return {
// TODO: min = 0 to turn them off?
expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }),
targetParams: PD.Group(reprParams, {
label: 'Target',
customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'physical', typeParams: { sizeFactor: 0.17 } })
customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'physical', typeParams: { sizeFactor: 0.17 }, colorParams: { carbon: Color.fromRgb(100, 100, 100) } })
}),
surroundingsParams: PD.Group(reprParams, {
label: 'Surroundings',
customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', color: 'element-symbol', size: 'physical', typeParams: { sizeFactor: 0.16 } })
customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', color: 'element-symbol', size: 'physical', typeParams: { sizeFactor: 0.16 }, colorParams: { carbon: Color.fromRgb(160, 160, 160) } })
}),
nciParams: PD.Group(reprParams, {
label: 'Non-covalent Int.',

View File

@@ -42,6 +42,9 @@ export const PluginConfig = {
Download: {
DefaultPdbProvider: item<PdbDownloadProvider>('download.default-pdb-provider', 'pdbe'),
DefaultEmdbProvider: item<EmdbDownloadProvider>('download.default-emdb-provider', 'pdbe'),
},
Structure: {
SizeThresholds: item('structure.size-thresholds', Structure.DefaultSizeThresholds),
}
};
@@ -61,8 +64,8 @@ export class PluginConfigManager {
this._config.delete(key);
}
constructor(initial?: Map<PluginConfigItem, unknown>) {
constructor(initial?: [PluginConfigItem, unknown][]) {
if (!initial) return;
initial.forEach((v, k) => this._config.set(k, v));
initial.forEach(([k, v]) => this._config.set(k, v));
}
}

View File

@@ -85,6 +85,9 @@ export class PluginContext {
state: {
isAnimating: this.ev.behavior<boolean>(false),
isUpdating: this.ev.behavior<boolean>(false),
// TODO: should there be separate "updated" event?
// Often, this is used to indicate that the state has updated
// and it might not be the best way to react to state updates.
isBusy: this.ev.behavior<boolean>(false)
},
interaction: {
@@ -134,6 +137,11 @@ export class PluginContext {
return this.state.data.build();
}
readonly helpers = {
substructureParent: new SubstructureParentHelper(this),
viewportScreenshot: void 0 as ViewportScreenshotHelper | undefined
} as const;
readonly managers = {
structure: {
hierarchy: new StructureHierarchyManager(this),
@@ -161,11 +169,6 @@ export class PluginContext {
readonly customStructureControls = new Map<string, { new(): PluginUIComponent<any, any, any> }>();
readonly genericRepresentationControls = new Map<string, (selection: StructureHierarchyManager['selection']) => [StructureHierarchyRef[], string]>();
readonly helpers = {
substructureParent: new SubstructureParentHelper(this),
viewportScreenshot: void 0 as ViewportScreenshotHelper | undefined
} as const;
/**
* Used to store application specific custom state which is then available
* to State Actions and similar constructs via the PluginContext.
@@ -240,6 +243,7 @@ export class PluginContext {
this.state.dispose();
this.tasks.dispose();
this.layout.dispose();
this.helpers.substructureParent.dispose();
objectForEach(this.managers, m => (m as any)?.dispose?.());
objectForEach(this.managers.structure, m => (m as any)?.dispose?.());
@@ -257,23 +261,25 @@ export class PluginContext {
let timeout: any = void 0;
const setBusy = () => {
isBusy.next(true);
if (!isBusy.value) isBusy.next(true);
};
const reset = () => {
if (timeout !== void 0) clearTimeout(timeout);
timeout = void 0;
};
merge(this.behaviors.state.isUpdating, this.behaviors.state.isAnimating).subscribe(v => {
const isUpdating = this.behaviors.state.isUpdating.value;
const isAnimating = this.behaviors.state.isAnimating.value;
if ((isUpdating || isAnimating) && !isBusy.value) {
if (timeout !== void 0) clearTimeout(timeout);
timeout = setTimeout(setBusy, timeoutMs);
// isBusy.next(true);
} else {
if (timeout !== void 0) clearTimeout(timeout);
timeout = void 0;
if (isBusy.value) {
isBusy.next(false);
if (isUpdating || isAnimating) {
if (!isBusy.value) {
reset();
timeout = setTimeout(setBusy, timeoutMs);
}
} else {
reset();
isBusy.next(false);
}
});

View File

@@ -15,7 +15,6 @@ import { Plugin } from '../mol-plugin-ui/plugin';
import { PluginBehaviors } from './behavior';
import { StructureFocusRepresentation } from './behavior/dynamic/selection/structure-focus-representation';
import { BoxifyVolumeStreaming, CreateVolumeStreamingBehavior, InitVolumeStreaming } from './behavior/dynamic/volume-streaming/transformers';
import { PluginConfig } from './config';
import { PluginContext } from './context';
import { PluginSpec } from './spec';
import { AssignColorVolume } from '../mol-plugin-state/actions/volume';
@@ -89,11 +88,7 @@ export const DefaultPluginSpec: PluginSpec = {
AnimateAssemblyUnwind,
AnimateUnitsExplode,
AnimateStateInterpolation
],
config: new Map([
[PluginConfig.State.DefaultServer, 'https://webchem.ncbr.muni.cz/molstar-state'],
[PluginConfig.VolumeStreaming.DefaultServer, 'https://ds.litemol.org']
])
]
};
export function createPlugin(target: HTMLElement, spec?: PluginSpec): PluginContext {

View File

@@ -30,7 +30,7 @@ interface PluginSpec {
controls?: React.ComponentClass
}
},
config?: Map<PluginConfigItem, unknown>
config?: [PluginConfigItem, unknown][]
}
namespace PluginSpec {

View File

@@ -114,6 +114,12 @@ class PluginState extends PluginComponent {
}
dispose() {
this.behaviors.cells.forEach(cell => {
if (PluginBehavior.Behavior.is(cell.obj)) {
cell.obj.data.unregister();
}
});
super.dispose();
this.data.dispose();
this.behaviors.dispose();

View File

@@ -8,10 +8,18 @@ import { Structure } from '../../mol-model/structure';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { State, StateObject, StateObjectCell, StateSelection } from '../../mol-state';
import { PluginContext } from '../context';
import { RxEventHelper } from '../../mol-util/rx-event-helper';
export { SubstructureParentHelper };
class SubstructureParentHelper {
private ev = RxEventHelper.create();
readonly events = {
updated: this.ev<{ ref: string, oldObj: PluginStateObject.Molecule.Structure | undefined, obj: PluginStateObject.Molecule.Structure }>(),
removed: this.ev<{ ref: string, obj: PluginStateObject.Molecule.Structure | undefined }>(),
}
// private decorators = new Map<string, string[]>();
private root = new Map<Structure, { ref: string, count: number }>();
private tracked = new Map<string, Structure>();
@@ -36,7 +44,7 @@ class SubstructureParentHelper {
}
private addMapping(state: State, ref: string, obj: StateObject) {
if (!PluginStateObject.Molecule.Structure.is(obj)) return;
if (!PluginStateObject.Molecule.Structure.is(obj)) return false;
this.tracked.set(ref, obj.data);
@@ -53,10 +61,11 @@ class SubstructureParentHelper {
this.root.set(obj.data, { ref: parent.transform.ref, count: 1 });
}
}
return true;
}
private removeMapping(ref: string) {
if (!this.tracked.has(ref)) return;
if (!this.tracked.has(ref)) return false;
const s = this.tracked.get(ref)!;
this.tracked.delete(ref);
@@ -68,13 +77,19 @@ class SubstructureParentHelper {
} else {
this.root.delete(s);
}
return true;
}
private updateMapping(state: State, ref: string, oldObj: StateObject | undefined, obj: StateObject) {
if (!PluginStateObject.Molecule.Structure.is(obj)) return;
if (!PluginStateObject.Molecule.Structure.is(obj)) return false;
this.removeMapping(ref);
this.addMapping(state, ref, obj);
return true;
}
dispose() {
this.ev.dispose();
}
constructor(private plugin: PluginContext) {
@@ -83,11 +98,15 @@ class SubstructureParentHelper {
});
plugin.state.data.events.object.removed.subscribe(e => {
this.removeMapping(e.ref);
if (this.removeMapping(e.ref)) {
this.events.removed.next({ ref: e.ref, obj: e.obj });
}
});
plugin.state.data.events.object.updated.subscribe(e => {
this.updateMapping(e.state, e.ref, e.oldObj, e.obj);
if (this.updateMapping(e.state, e.ref, e.oldObj, e.obj)) {
this.events.updated.next({ ref: e.ref, oldObj: e.oldObj, obj: e.obj });
}
});
}
}

View File

@@ -46,7 +46,7 @@ interface ComplexVisualBuilder<P extends StructureParams, G extends Geometry> {
createGeometry(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
createLocationIterator(structure: Structure): LocationIterator
getLoci(pickingId: PickingId, structure: Structure, id: number): Loci
eachLocation(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean): boolean,
eachLocation(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, isMarking: boolean): boolean,
setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure): void
}
@@ -173,11 +173,11 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
return false;
}
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
if (lociIsSuperset(loci)) {
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
} else {
return eachLocation(loci, currentStructure, apply);
return eachLocation(loci, currentStructure, apply, isMarking);
}
}

View File

@@ -51,7 +51,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
if (!_structure && !structure) {
throw new Error('missing structure');
} else if (structure && !_structure) {
// console.log(label, 'initial structure')
// console.log(label, 'initial structure');
// First call with a structure, create visuals for each group.
_groups = structure.unitSymmetryGroups;
for (let i = 0; i < _groups.length; i++) {
@@ -75,7 +75,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
const group = _groups[i];
const visualGroup = oldVisuals.get(group.hashCode);
if (visualGroup) {
// console.log(label, 'found visualGroup to reuse')
// console.log(label, 'found visualGroup to reuse');
// console.log('old', visualGroup.group)
// console.log('new', group)
const { visual } = visualGroup;
@@ -91,7 +91,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
applyMarkerAction(arr, Interval.ofBounds(0, arr.length), MarkerAction.RemoveHighlight);
}
} else {
// console.log(label, 'did not find visualGroup to reuse, creating new')
// console.log(label, 'did not find visualGroup to reuse, creating new');
// newGroups.push(group)
const visual = visualCtor(materialId);
const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure });
@@ -118,7 +118,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
// })
// unusedVisuals.forEach(visual => visual.destroy())
} else if (structure && structure !== _structure && Structure.areEquivalent(structure, _structure)) {
// console.log(label, 'structures equivalent but not identical')
// console.log(label, 'structures equivalent but not identical');
// Expects that for structures with the same hashCode,
// the unitSymmetryGroups are the same as well.
// Re-uses existing visuals for the groups of the new structure.
@@ -132,14 +132,13 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
const promise = visualGroup.visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure });
if (promise) await promise;
visualGroup.group = group;
} else {
throw new Error(`expected to find visual for hashCode ${group.hashCode}`);
}
if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length });
}
} else {
// console.log(label, 'no new structure')
// console.log(label, 'no new structure');
// No new structure given, just update all visuals with new props.
const visualsList: [ UnitsVisual<P>, Unit.SymmetryGroup ][] = []; // TODO avoid allocation
visuals.forEach(({ visual, group }) => visualsList.push([ visual, group ]));

View File

@@ -54,7 +54,7 @@ interface UnitsVisualBuilder<P extends StructureParams, G extends Geometry> {
createGeometry(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
createLocationIterator(structureGroup: StructureGroup): LocationIterator
getLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number): Loci
eachLocation(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean): boolean
eachLocation(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean, isMarking: boolean): boolean
setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup): void
}
@@ -224,11 +224,11 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
return false;
}
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
if (lociIsSuperset(loci)) {
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
} else {
return eachLocation(loci, currentStructureGroup, apply);
return eachLocation(loci, currentStructureGroup, apply, isMarking);
}
}

View File

@@ -155,7 +155,7 @@ function getBondLoci(pickingId: PickingId, structure: Structure, id: number) {
return EmptyLoci;
}
function eachBond(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
function eachBond(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, isMarking: boolean) {
let changed = false;
if (Bond.isLoci(loci)) {
if (!Structure.areEquivalent(loci.structure, structure)) return false;
@@ -181,7 +181,7 @@ function eachBond(loci: Loci, structure: Structure, apply: (interval: Interval)
OrderedSet.forEach(e.indices, v => {
if (!b.connectedIndices.includes(v)) return;
b.getEdges(v).forEach(bi => {
if (OrderedSet.has(otherLociIndices, bi.indexB)) {
if (!isMarking || OrderedSet.has(otherLociIndices, bi.indexB)) {
const idx = structure.interUnitBonds.getEdgeIndex(v, unit, bi.indexB, b.unitB);
if (apply(Interval.ofSingleton(idx))) changed = true;
}

View File

@@ -154,7 +154,7 @@ function getBondLoci(pickingId: PickingId, structureGroup: StructureGroup, id: n
return EmptyLoci;
}
function eachBond(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
function eachBond(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean, isMarking: boolean) {
let changed = false;
if (Bond.isLoci(loci)) {
const { structure, group } = structureGroup;
@@ -183,7 +183,7 @@ function eachBond(loci: Loci, structureGroup: StructureGroup, apply: (interval:
const { offset, b } = unit.bonds;
OrderedSet.forEach(e.indices, v => {
for (let t = offset[v], _t = offset[v + 1]; t < _t; t++) {
if (OrderedSet.has(e.indices, b[t])) {
if (!isMarking || OrderedSet.has(e.indices, b[t])) {
if (apply(Interval.ofSingleton(unitIdx * groupCount + t))) changed = true;
}
}

View File

@@ -15,6 +15,7 @@ import { fillSerial } from '../../../../mol-util/array';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { AssignableArrayLike } from '../../../../mol-util/type-helpers';
import { getBoundary } from '../../../../mol-math/geometry/boundary';
import { Box3D } from '../../../../mol-math/geometry';
/** Return a Loci for the elements of a whole residue the elementIndex belongs to. */
export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci {
@@ -99,6 +100,16 @@ export function includesUnitKind(unitKinds: UnitKind[], unit: Unit) {
//
const MaxCells = 500_000_000;
/** guard against overly high resolution for the given box size */
export function ensureReasonableResolution<T>(box: Box3D, props: { resolution: number } & T) {
const volume = Box3D.volume(box);
const approxCells = volume / props.resolution;
const resolution = approxCells > MaxCells ? volume / MaxCells : props.resolution;
return { ...props, resolution };
}
export function getConformation(unit: Unit) {
switch (unit.kind) {
case Unit.Kind.Atomic: return unit.model.atomicConformation;

View File

@@ -11,7 +11,7 @@ import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { GaussianDensityTexture, GaussianDensityTexture2d } from '../../../../mol-math/geometry/gaussian-density/gpu';
import { Texture } from '../../../../mol-gl/webgl/texture';
import { WebGLContext } from '../../../../mol-gl/webgl/context';
import { getUnitConformationAndRadius, getStructureConformationAndRadius, CommonSurfaceParams } from './common';
import { getUnitConformationAndRadius, getStructureConformationAndRadius, CommonSurfaceParams, ensureReasonableResolution } from './common';
import { BaseGeometry } from '../../../../mol-geo/geometry/base';
const SharedGaussianDensityParams = {
@@ -37,38 +37,48 @@ export type GaussianDensityTextureProps = typeof DefaultGaussianDensityTexturePr
//
export function computeUnitGaussianDensity(structure: Structure, unit: Unit, props: GaussianDensityProps, webgl?: WebGLContext) {
const { position, radius } = getUnitConformationAndRadius(structure, unit, props);
const { box } = unit.lookup3d.boundary;
const p = ensureReasonableResolution(box, props);
const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
return Task.create('Gaussian Density', async ctx => {
return await GaussianDensity(ctx, position, unit.lookup3d.boundary.box, radius, props, webgl);
return await GaussianDensity(ctx, position, box, radius, p, webgl);
});
}
export function computeUnitGaussianDensityTexture(structure: Structure, unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
const { position, radius } = getUnitConformationAndRadius(structure, unit, props);
const { box } = unit.lookup3d.boundary;
const p = ensureReasonableResolution(box, props);
const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
return Task.create('Gaussian Density', async ctx => {
return GaussianDensityTexture(webgl, position, unit.lookup3d.boundary.box, radius, props, texture);
return GaussianDensityTexture(webgl, position, box, radius, p, texture);
});
}
export function computeUnitGaussianDensityTexture2d(structure: Structure, unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
const { position, radius } = getUnitConformationAndRadius(structure, unit, props);
const { box } = unit.lookup3d.boundary;
const p = ensureReasonableResolution(box, props);
const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
return Task.create('Gaussian Density', async ctx => {
return GaussianDensityTexture2d(webgl, position, unit.lookup3d.boundary.box, radius, props, texture);
return GaussianDensityTexture2d(webgl, position, box, radius, p, texture);
});
}
//
export function computeStructureGaussianDensity(structure: Structure, props: GaussianDensityProps, webgl?: WebGLContext) {
const { box } = structure.lookup3d.boundary;
const p = ensureReasonableResolution(box, props);
const { position, radius } = getStructureConformationAndRadius(structure, props.ignoreHydrogens, props.traceOnly);
return Task.create('Gaussian Density', async ctx => {
return await GaussianDensity(ctx, position, structure.lookup3d.boundary.box, radius, props, webgl);
return await GaussianDensity(ctx, position, box, radius, p, webgl);
});
}
export function computeStructureGaussianDensityTexture(structure: Structure, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
const { box } = structure.lookup3d.boundary;
const p = ensureReasonableResolution(box, props);
const { position, radius } = getStructureConformationAndRadius(structure, props.ignoreHydrogens, props.traceOnly);
return Task.create('Gaussian Density', async ctx => {
return GaussianDensityTexture(webgl, position, structure.lookup3d.boundary.box, radius, props, texture);
return GaussianDensityTexture(webgl, position, box, radius, p, texture);
});
}

View File

@@ -6,7 +6,7 @@
import { Unit, Structure } from '../../../../mol-model/structure';
import { Task, RuntimeContext } from '../../../../mol-task';
import { getUnitConformationAndRadius, CommonSurfaceProps } from './common';
import { getUnitConformationAndRadius, CommonSurfaceProps, ensureReasonableResolution } from './common';
import { PositionData, DensityData, Box3D } from '../../../../mol-math/geometry';
import { MolecularSurfaceCalculationProps, calcMolecularSurface } from '../../../../mol-math/geometry/molecular-surface';
import { OrderedSet } from '../../../../mol-data/int';
@@ -34,9 +34,10 @@ function getPositionDataAndMaxRadius(structure: Structure, unit: Unit, props: Mo
export function computeUnitMolecularSurface(structure: Structure, unit: Unit, props: MolecularSurfaceProps) {
const { box } = unit.lookup3d.boundary;
const { position, boundary, maxRadius } = getPositionDataAndMaxRadius(structure, unit, props);
const p = ensureReasonableResolution(box, props);
const { position, boundary, maxRadius } = getPositionDataAndMaxRadius(structure, unit, p);
return Task.create('Molecular Surface', async ctx => {
return await MolecularSurface(ctx, position, boundary, maxRadius, box, props);
return await MolecularSurface(ctx, position, boundary, maxRadius, box, p);
});
}

View File

@@ -7,6 +7,7 @@
import { defaults } from '../mol-util';
import { Structure } from '../mol-model/structure';
import { VisualQuality } from '../mol-geo/geometry/base';
import { Box3D } from '../mol-math/geometry';
export interface VisualUpdateState {
updateTransform: boolean
@@ -93,8 +94,10 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
let resolution = defaults(props.resolution, 2);
let doubleSided = defaults(props.doubleSided, true);
let volume = 0;
if (quality === 'auto' && data instanceof Structure) {
quality = getStructureQuality(data.root);
volume = Box3D.volume(data.boundary.box);
}
switch (quality) {
@@ -102,49 +105,49 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
detail = 3;
radialSegments = 36;
linearSegments = 18;
resolution = 0.1;
resolution = Math.max(volume / 500_000_000, 0.1);
doubleSided = true;
break;
case 'higher':
detail = 3;
radialSegments = 28;
linearSegments = 14;
resolution = 0.3;
resolution = Math.max(volume / 400_000_000, 0.3);
doubleSided = true;
break;
case 'high':
detail = 2;
radialSegments = 20;
linearSegments = 10;
resolution = 0.5;
resolution = Math.max(volume / 300_000_000, 0.5);
doubleSided = true;
break;
case 'medium':
detail = 1;
radialSegments = 12;
linearSegments = 8;
resolution = 1;
resolution = Math.max(volume / 200_000_000, 1);
doubleSided = true;
break;
case 'low':
detail = 0;
radialSegments = 8;
linearSegments = 3;
resolution = 2;
resolution = Math.max(volume / 150_000_000, 2);
doubleSided = false;
break;
case 'lower':
detail = 0;
radialSegments = 4;
linearSegments = 2;
resolution = 4;
resolution = Math.max(volume / 100_000_000, 4);
doubleSided = false;
break;
case 'lowest':
detail = 0;
radialSegments = 2;
linearSegments = 1;
resolution = 8;
resolution = Math.max(volume / 75_000_000, 8);
doubleSided = false;
break;
case 'custom':

View File

@@ -45,7 +45,7 @@ interface Visual<D, P extends PD.Params> {
destroy: () => void
}
namespace Visual {
export type LociApply = (loci: Loci, apply: (interval: Interval) => boolean) => boolean
export type LociApply = (loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) => boolean
export function setVisibility(renderObject: GraphicsRenderObject | undefined, visible: boolean) {
if (renderObject) renderObject.state.visible = visible;
@@ -70,7 +70,7 @@ namespace Visual {
if (isEveryLoci(loci)) {
changed = applyMarkerAction(array, Interval.ofLength(count), action);
} else if (!isEmptyLoci(loci)) {
changed = lociApply(loci, interval => applyMarkerAction(array, interval, action));
changed = lociApply(loci, interval => applyMarkerAction(array, interval, action), true);
}
if (changed) ValueCell.update(tMarker, tMarker.ref.value);
return changed;
@@ -98,7 +98,7 @@ namespace Visual {
? clearOverpaint(array, start, end)
: applyOverpaintColor(array, start, end, color, overpaint.alpha);
};
lociApply(loci, apply);
lociApply(loci, apply, false);
}
ValueCell.update(tOverpaint, tOverpaint.ref.value);
}
@@ -123,7 +123,7 @@ namespace Visual {
const end = Interval.end(interval);
return applyTransparencyValue(array, start, end, value);
};
lociApply(loci, apply);
lociApply(loci, apply, false);
ValueCell.update(tTransparency, tTransparency.ref.value);
}

View File

@@ -24,6 +24,7 @@ const DefaultElementSymbolColor = Color(0xFFFFFF);
const Description = 'Assigns a color to every atom according to its chemical element.';
export const ElementSymbolColorThemeParams = {
carbon: PD.Color(ElementSymbolColors.C),
saturation: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
lightness: PD.Numeric(0.7, { min: -6, max: 6, step: 0.1 })
};
@@ -39,17 +40,20 @@ export function elementSymbolColor(colorMap: ElementSymbolColors, element: Eleme
export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<ElementSymbolColorThemeParams>): ColorTheme<ElementSymbolColorThemeParams> {
const colorMap = getAdjustedColorMap(ElementSymbolColors, props.saturation, props.lightness);
const carbonColor = Color.darken(Color.saturate(props.carbon, props.saturation), -props.lightness);
function color(location: Location): Color {
if (StructureElement.Location.is(location)) {
if (Unit.isAtomic(location.unit)) {
const { type_symbol } = location.unit.model.atomicHierarchy.atoms;
return elementSymbolColor(colorMap, type_symbol.value(location.element));
const element = type_symbol.value(location.element);
return element === 'C' ? carbonColor : elementSymbolColor(colorMap, element);
}
} else if (Bond.isLocation(location)) {
if (Unit.isAtomic(location.aUnit)) {
const { type_symbol } = location.aUnit.model.atomicHierarchy.atoms;
return elementSymbolColor(colorMap, type_symbol.value(location.aUnit.elements[location.aIndex]));
const element = type_symbol.value(location.aUnit.elements[location.aIndex]);
return element === 'C' ? carbonColor : elementSymbolColor(colorMap, element);
}
}
return DefaultElementSymbolColor;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -16,13 +16,12 @@ let isProductionMode = process.env.NODE_ENV === 'production';
*/
let isDebugMode = process.env.DEBUG === '*' || process.env.DEBUG === 'molstar';
if (typeof window !== 'undefined' && !(window as any).setMolStarDebugMode) {
try {
(window as any).setMolStarDebugMode = function setMolStarDebugMode(isDebug?: boolean, isProduction?: boolean) {
if (typeof isDebug !== 'undefined') isDebugMode = isDebug;
if (typeof isProduction !== 'undefined') isProductionMode = isProduction;
};
} catch { }
export { isProductionMode, isDebugMode };
export function setProductionMode(value?: boolean) {
if (typeof value !== 'undefined') isProductionMode = value;
}
export { isProductionMode, isDebugMode };
export function setDebugMode(value?: boolean) {
if (typeof value !== 'undefined') isDebugMode = value;
}