mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
129 Commits
support-sc
...
v4.14.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f8be5b8c6 | ||
|
|
2ab6e4b2e7 | ||
|
|
4a88546181 | ||
|
|
edbc70cf6e | ||
|
|
c22ad2910c | ||
|
|
28a2b52e3c | ||
|
|
470227af43 | ||
|
|
a0ccf46939 | ||
|
|
0ce8931fc5 | ||
|
|
3ddb29fc6f | ||
|
|
f34f879cf1 | ||
|
|
6da9557531 | ||
|
|
29e6d69d21 | ||
|
|
6b2b87e6c5 | ||
|
|
5299d5c0c4 | ||
|
|
7bab95f4cc | ||
|
|
35e78ce638 | ||
|
|
3abbcb6949 | ||
|
|
c3fc893ad0 | ||
|
|
80415a2771 | ||
|
|
bfef69e2e4 | ||
|
|
a265a579be | ||
|
|
c2af1b0b22 | ||
|
|
b739876726 | ||
|
|
56851cc328 | ||
|
|
b719568555 | ||
|
|
f842f19912 | ||
|
|
d6b0dd910c | ||
|
|
395eb8e8d0 | ||
|
|
e4376c6737 | ||
|
|
6f23fcba8a | ||
|
|
7d19bfdb9b | ||
|
|
6e4065f779 | ||
|
|
c42a68b560 | ||
|
|
c86d913b83 | ||
|
|
0f6fa5fe15 | ||
|
|
f664cb02b1 | ||
|
|
00a53de6e2 | ||
|
|
b1ce5c158e | ||
|
|
26826b61f9 | ||
|
|
f44f954a76 | ||
|
|
78aae8a2b4 | ||
|
|
92267d3264 | ||
|
|
73ed45564e | ||
|
|
8bc2ebbeff | ||
|
|
5306d5d15a | ||
|
|
3c3fb461c8 | ||
|
|
62b3281282 | ||
|
|
e85fadf15b | ||
|
|
a8a84e1dbf | ||
|
|
153599ef89 | ||
|
|
b67eda7cb5 | ||
|
|
8b431c50be | ||
|
|
e195b048a1 | ||
|
|
ae5bb81b27 | ||
|
|
8c04c57bc5 | ||
|
|
ec46a444f1 | ||
|
|
559e0326b6 | ||
|
|
82b93bc2a8 | ||
|
|
62f940bc48 | ||
|
|
4e0be8e7b4 | ||
|
|
128502edf0 | ||
|
|
aad4d4a86c | ||
|
|
9bc7e27243 | ||
|
|
a5111356c1 | ||
|
|
9b11f7ffde | ||
|
|
93ce6d2807 | ||
|
|
5c9d5d3a3d | ||
|
|
f40307db39 | ||
|
|
4e6000fa6c | ||
|
|
26e5817bf2 | ||
|
|
8469be80d0 | ||
|
|
029edc95c8 | ||
|
|
dd9aaf055f | ||
|
|
fcfb2d940c | ||
|
|
f5b5109d0f | ||
|
|
ca99c800f1 | ||
|
|
dbd5570370 | ||
|
|
3f805c7a82 | ||
|
|
12c71dc5ba | ||
|
|
2fe3a926aa | ||
|
|
60c2096575 | ||
|
|
f4e9df5e4d | ||
|
|
c304b82772 | ||
|
|
9edd171350 | ||
|
|
f7d1bd7c04 | ||
|
|
7422c255ab | ||
|
|
5497215784 | ||
|
|
577bf1c77c | ||
|
|
c9bddccaf7 | ||
|
|
ac292f9267 | ||
|
|
f0b8d75b10 | ||
|
|
0dacbcb3bc | ||
|
|
0789241ea3 | ||
|
|
ddb0799dc4 | ||
|
|
cbfa341fa3 | ||
|
|
1c19bd90df | ||
|
|
300e5c8985 | ||
|
|
0861a78db6 | ||
|
|
a8e403ad85 | ||
|
|
4e350496b2 | ||
|
|
dd21ddcc80 | ||
|
|
a88121f779 | ||
|
|
1ab91d1979 | ||
|
|
267788388d | ||
|
|
43c0333be3 | ||
|
|
3b90a269b0 | ||
|
|
4aa5e1d7fc | ||
|
|
679db48938 | ||
|
|
2f96b42df7 | ||
|
|
dd6f3bd76e | ||
|
|
f1b7e478c7 | ||
|
|
416442aa27 | ||
|
|
a5f65b6e6f | ||
|
|
938ac0cc8f | ||
|
|
7dacf60478 | ||
|
|
9cdb8a3a92 | ||
|
|
242982e661 | ||
|
|
6da20a6989 | ||
|
|
f27b651230 | ||
|
|
7c818c0cc9 | ||
|
|
e7d7ba26b0 | ||
|
|
7e64121059 | ||
|
|
894bba1d3a | ||
|
|
d9db775fe8 | ||
|
|
a7fbc7b4c4 | ||
|
|
c0596298d6 | ||
|
|
8f32dde599 | ||
|
|
4d8f00900d |
124
CHANGELOG.md
124
CHANGELOG.md
@@ -5,17 +5,127 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v4.14.1] - 2025-05-09
|
||||
- Do not raise error when creating duplicate state transformers and print console warning instead
|
||||
|
||||
## [v4.14.0] - 2025-05-07
|
||||
- Fix `Viewer.loadTrajectory` when loading a topology file
|
||||
- Fix `StructConn.residueCantorPairs` to not include identity pairs
|
||||
- Add format selection option to image export UI (PNG, WebP, JPEG)
|
||||
- Add `StateBuilder.To.updateState`
|
||||
- MVS:
|
||||
- Support updating transform states
|
||||
- Add support for `is_hidden` custom state as an extension
|
||||
- Add `queryMVSRef` and `createMVSRefMap` utility functions
|
||||
- Fix switching representation type in Volume UI
|
||||
- VolumeServer: Avoid grid expansion when requiring unit cell (avoids including an extra layer of cells outside the unit cell query box)
|
||||
|
||||
## [v4.13.0] - 2025-04-14
|
||||
- Support `--host` option for build-dev.mjs script
|
||||
- Add `Viewer.loadFiles` to open supported files
|
||||
- Support installing the viewer as a Progressive Web App (PWA)
|
||||
- `ihm-restraints` example: show entity labels
|
||||
- Fix `element-point` visual not using child unit
|
||||
- Ignore `renderables` with empty draw count
|
||||
- Add experimental support for `esbuild` for development
|
||||
- Use `npm run dev` for faster development builds
|
||||
- Use `StructureElement.Bundle` instead of expressions to serialize measurement elements
|
||||
- Fixes measurements not being supported for coarse models
|
||||
- Implementation of `ColorScale.createDiscrete` (#1458)
|
||||
- Add `ColorScale.createDiscrete` to the `uncertainty` color theme
|
||||
- Fix color palette shown in the UI (for non-gradient palettes)
|
||||
- Fix colors description in the UI (when using custom thresholds)
|
||||
- Fix an edge case in the UI when the user deletes all colors from the color list
|
||||
- Add `interactions` extension and a corresponding example that utilizes it
|
||||
- Add element source index to default atomic granularity hover labels
|
||||
- Add `StructureElement.Schema` based on corresponding MolViewSpec implementation that allows data-driven selection of structural elements
|
||||
- Add `StructureElement.Loci/Bundle.fromExpression/Query/Schema` helper functions
|
||||
- Add `addLinkCylinderMesh` (from `createLinkCylinderMesh`)
|
||||
- Add `Unit.transientCache` and `Unit.getCopy`
|
||||
- Fix `ElementBondIterator` indices mapping logic for inter-unit bonds
|
||||
- Fix `pickPadding` and `pickScale` not updating `PickHelper`
|
||||
- MolViewSpec extension: support loading extensions when loading multistate files
|
||||
- Do not add bonds for pairs of residues that have a `struct_conn` entry
|
||||
- Improved `ma_qa_metric` support
|
||||
- Parse all local metrics
|
||||
- Ability to select alternate metrics in the pLDDT/qmean themes
|
||||
- Do not assume PAE plot is symmetric
|
||||
- Added `PluginConfig.Viewport.ShowScreenshotControls` to control visibility of screenshot controls
|
||||
- Fix MolViewSpec builder for volumes.
|
||||
- Generalize `mvs-kinase-story` example to `mvs-stories`
|
||||
- Add TATA-binding protein story
|
||||
- Improve the Kinase story
|
||||
- Fix alpha orbitals example
|
||||
|
||||
## [v4.12.0] - 2025-02-28
|
||||
|
||||
- Fix PDBj structure data URL
|
||||
- Improve logic when to cull in renderer
|
||||
- Add `atom.ihm.has-seq-id` and `atom.ihm.overlaps-seq-id-range` symbol to the query language
|
||||
- MolViewSpec extension:
|
||||
- Add box, arrow, ellipse, ellipsoid, angle primitives
|
||||
- Add basic support for volumetric data (map, Volume Server)
|
||||
- Add support for `molstar_color_theme_name` custom extension
|
||||
- Better IH/M support:
|
||||
- Support `coarse` components
|
||||
- Support `spacefill` representation
|
||||
- Support `carbohydrate` representation
|
||||
- Support for `custom.molstar_use_default_coloring` property on Color node.
|
||||
- Use `atom.ihm.has-seq-id` and `atom.ihm.overlaps-seq-id-range` for matching `label_seq_id` locations to support querying coarse elements.
|
||||
- Add ihm-restraints example
|
||||
- Add `mvs-kinase-story` example
|
||||
- Remove static uses of `ColorTheme` and `SizeTheme` fields. Should resolvent "undefined" errors in certain builds
|
||||
- Add `transform` property to clip objects
|
||||
- Add support for trimming `image` geometry to a box
|
||||
- Improve/fix iso-level support of `slice` representation
|
||||
- Add support for rotating `slice` representation around an axis
|
||||
- Add default color support for palette based themes
|
||||
- Add `plane` structure representation
|
||||
- Can be colored with any structure theme
|
||||
- Can be colored with the `external-volume` theme
|
||||
- Can show atoms as a cutout
|
||||
- Supports principal axes and bounding box as a reference frame
|
||||
- Add `Camera` section to "Screenshot / State" controls
|
||||
- Add `CoarseIndex` for fast lookup of coarse elements
|
||||
|
||||
## [v4.11.0] - 2025-01-26
|
||||
|
||||
- Fix for tubular helices issue (Fixes #1422)
|
||||
- Volume UI improvements
|
||||
- Render all volume entries instead of selecting them one-by-one
|
||||
- Toggle visibility of all volumes
|
||||
- More accessible iso value control
|
||||
- Render all volume entries instead of selecting them one-by-one
|
||||
- Toggle visibility of all volumes
|
||||
- More accessible iso value control
|
||||
- Support wheel event on sliders
|
||||
- MolViewSpec extension:
|
||||
- Add validation for discriminated union params
|
||||
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
|
||||
- Add validation for discriminated union params
|
||||
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
|
||||
- UI configuration options
|
||||
- Support removal of independent selection controls in the viewport
|
||||
- Support custom selection controls
|
||||
- Support for custom granularity dropdown options
|
||||
- Support for custom Sequence Viewer mode options
|
||||
- Add `external-structure` theme that colors any geometry by structure properties
|
||||
- Support float and half-float data type for direct-volume rendering and GPU isosurface extraction
|
||||
- Minor documentation updates
|
||||
- Add support for position-location to `volume-value` color theme
|
||||
- Add support for color themes to `slice` representation
|
||||
- Improve/fix palette support in volume color themes
|
||||
- Fix `Plane3D.projectPoint`
|
||||
- Fix marking related `image` rendering issues
|
||||
- Handle pixels without a group
|
||||
- Take fog into account
|
||||
- MolViewSpec extension: Initial support for customizable representation parameters
|
||||
- Quick Styles section reorganized
|
||||
- UI color improvements (scrollbar contrast, toggle button hover color)
|
||||
- Add `overrideWater` param for entity-id color theme
|
||||
- Renames PDB-Dev to PDB-IHM and adjusts data source
|
||||
- Fix vertex based themes for spheres shader
|
||||
- Add volume dot representation
|
||||
- Add volume-value size theme
|
||||
- Sequence panel: Mark focused loci (bold+underline)
|
||||
- Change modifier key behavior in Normal Mode (default = select only, Ctrl/Cmd = add to selection, Shift = extend last selected range)
|
||||
- Handle Firefox's limit on vertex ids per draw (#1116)
|
||||
- Fix behavior of `Vec3.makeRotation(out, a, b)` when `a ≈ -b`
|
||||
|
||||
## [v4.10.0] - 2024-12-15
|
||||
|
||||
@@ -83,7 +193,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Fix `findPredecessorIndex` bug when repeating values
|
||||
- MolViewSpec: Support for transparency and custom properties
|
||||
- MolViewSpec: MVP Support for geometrical primitives (mesh, lines, line, label, distance measurement)
|
||||
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-Dev loader
|
||||
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-IHM/PDB-Dev loader
|
||||
- Fix Sequence View in Safari 18
|
||||
- Improve performance of `IndexPairBonds` assignment when operator keys are available
|
||||
- ModelArchive QualityAssessment extension:
|
||||
@@ -136,7 +246,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Improve entity-id coloring for structures with multiple models from the same source (#1221)
|
||||
- Wrap screenshot & image generation in a `Task`
|
||||
- AlphaFold DB: Add BinaryCIF support when fetching data
|
||||
- PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
|
||||
- PDB-IHM/PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
|
||||
- Fix polymer-gap visual coloring with cartoon theme
|
||||
- Add formal-charge color theme (#328)
|
||||
- Add more coloring options to cartoon theme
|
||||
|
||||
12
README.md
12
README.md
@@ -84,7 +84,16 @@ Wipes the `build` and `lib` directories and `.tsbuildinfo` files.
|
||||
|
||||
Runs the cleanup script prior to building the project, forcing a full rebuild of the project.
|
||||
|
||||
Use these commands to resolve occassional build failures which may arise after some dependency updates. Once done, `npm run build` should work again. Note that full rebuilds take more time to complete.
|
||||
Use these commands to resolve occasional build failures which may arise after some dependency updates. Once done, `npm run build` should work again. Note that full rebuilds take more time to complete.
|
||||
|
||||
### Develop with `esbuild`
|
||||
|
||||
Experimental support for faster builds with `esbuild`
|
||||
- `npm run dev:all` - watch mode for all apps and examples
|
||||
- `npm run dev:viewer` - watch mode for viewer
|
||||
- `npm run dev:apps` - watch mode for all apps
|
||||
- `npm run dev:examples` - watch mode for all examples
|
||||
- `npm run dev -- -a <app name 1> <app name 2> -e <example name 1> ...` - watch mode for specified apps/examples. `-a`/`-e` with without any names will build everything.
|
||||
|
||||
### Build for production:
|
||||
NODE_ENV=production npm run build
|
||||
@@ -103,7 +112,6 @@ From the root of the project:
|
||||
|
||||
and navigate to `build/viewer`
|
||||
|
||||
|
||||
### Code generation
|
||||
**CIF schemas**
|
||||
|
||||
|
||||
216
build-dev.mjs
Normal file
216
build-dev.mjs
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Eric E <etongfu@@outlook.com>
|
||||
*/
|
||||
import * as esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as argparse from 'argparse';
|
||||
import { sassPlugin } from 'esbuild-sass-plugin';
|
||||
import * as os from 'os';
|
||||
|
||||
const AllApps = [
|
||||
'viewer',
|
||||
'docking-viewer',
|
||||
'mesoscale-explorer'
|
||||
];
|
||||
|
||||
const AllExamples = [
|
||||
'proteopedia-wrapper',
|
||||
'basic-wrapper',
|
||||
'lighting',
|
||||
'alpha-orbitals',
|
||||
'alphafolddb-pae',
|
||||
'mvs-stories',
|
||||
'ihm-restraints',
|
||||
'interactions',
|
||||
];
|
||||
|
||||
function mkDir(dir) {
|
||||
try {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to create directory ${dir}:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileError(error, operation, path) {
|
||||
console.error(`Failed to ${operation} ${path}:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function fileLoaderPlugin(options) {
|
||||
mkDir(options.out);
|
||||
|
||||
return {
|
||||
name: 'file-loader',
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.jpg$/ }, async (args) => {
|
||||
try {
|
||||
const name = path.basename(args.path);
|
||||
mkDir(path.resolve(options.out, 'images'));
|
||||
await fs.promises.copyFile(args.path, path.resolve(options.out, 'images', name));
|
||||
return {
|
||||
contents: `images/${name}`,
|
||||
loader: 'text',
|
||||
};
|
||||
} catch (error) {
|
||||
handleFileError(error, 'copy', args.path);
|
||||
}
|
||||
});
|
||||
build.onLoad({ filter: /\.(html|ico)$/ }, async (args) => {
|
||||
const name = path.basename(args.path);
|
||||
await fs.promises.copyFile(args.path, path.resolve(options.out, name));
|
||||
return {
|
||||
contents: '',
|
||||
loader: 'empty',
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function examplesCssRenamePlugin({ root }) {
|
||||
return {
|
||||
name: 'example-css-rename',
|
||||
setup(build) {
|
||||
build.onEnd(async () => {
|
||||
if (fs.existsSync(path.resolve(root, 'index.css'))) {
|
||||
await fs.promises.rename(
|
||||
path.resolve(root, 'index.css'),
|
||||
path.resolve(root, 'molstar.css')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function watch(name, kind) {
|
||||
const prefix = kind === 'app'
|
||||
? `./build/${name}`
|
||||
: `./build/examples/${name}`;
|
||||
|
||||
let entry = `./src/${kind}s/${name}/index.ts`;
|
||||
if (!fs.existsSync(entry)) {
|
||||
entry = `./src/${kind}s/${name}/index.tsx`;
|
||||
}
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [entry],
|
||||
tsconfig: './tsconfig.json',
|
||||
bundle: true,
|
||||
globalName: 'molstar',
|
||||
outfile: kind === 'app'
|
||||
? `./build/${name}/molstar.js`
|
||||
: `./build/examples/${name}/index.js`,
|
||||
plugins: [
|
||||
fileLoaderPlugin({ out: prefix }),
|
||||
sassPlugin({
|
||||
type: 'css',
|
||||
silenceDeprecations: ['import'],
|
||||
logger: {
|
||||
warn: (msg) => console.warn(msg),
|
||||
debug: () => { },
|
||||
}
|
||||
}),
|
||||
...(kind === 'example' ? [examplesCssRenamePlugin({ root: prefix })] : []),
|
||||
],
|
||||
external: ['crypto', 'fs', 'path', 'stream'],
|
||||
loader: {
|
||||
},
|
||||
color: true,
|
||||
logLevel: 'info',
|
||||
});
|
||||
|
||||
await ctx.rebuild();
|
||||
await ctx.watch();
|
||||
}
|
||||
|
||||
const argParser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
description: 'Mol* development build'
|
||||
});
|
||||
argParser.add_argument('--apps', '-a', {
|
||||
help: 'Apps to build.',
|
||||
required: false,
|
||||
nargs: '*',
|
||||
});
|
||||
argParser.add_argument('--examples', '-e', {
|
||||
help: 'Examples to build.',
|
||||
required: false,
|
||||
nargs: '*',
|
||||
});
|
||||
argParser.add_argument('--port', '-p', {
|
||||
help: 'Port.',
|
||||
required: false,
|
||||
default: 1338,
|
||||
type: 'int',
|
||||
});
|
||||
|
||||
argParser.add_argument('--host', {
|
||||
help: 'Show all available host addresses.',
|
||||
required: false,
|
||||
action: 'store_true',
|
||||
});
|
||||
|
||||
const args = argParser.parse_args();
|
||||
|
||||
const apps = (!args.apps ? [] : (args.apps.length ? args.apps : AllApps)).filter(a => AllApps.includes(a));
|
||||
const examples = (!args.examples ? [] : (args.examples.length ? args.examples : AllExamples)).filter(e => AllExamples.includes(e));
|
||||
|
||||
console.log('Apps:', apps);
|
||||
console.log('Examples:', examples);
|
||||
console.log('');
|
||||
|
||||
function getLocalIPs() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const ips = [];
|
||||
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]) {
|
||||
// Skip internal and non-IPv4 addresses
|
||||
if (iface.internal || iface.family !== 'IPv4') continue;
|
||||
ips.push(iface.address);
|
||||
}
|
||||
}
|
||||
|
||||
return ips;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const promises = [];
|
||||
for (const app of apps) promises.push(watch(app, 'app'));
|
||||
for (const example of examples) promises.push(watch(example, 'example'));
|
||||
|
||||
console.log('Initial build...');
|
||||
|
||||
await Promise.all(promises);
|
||||
console.log('Done.');
|
||||
|
||||
const ctx = await esbuild.context({});
|
||||
ctx.serve({
|
||||
servedir: './',
|
||||
port: args.port,
|
||||
host: '0.0.0.0', // Always listen on all interfaces
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log(`Server URL: http://localhost:${args.port}`);
|
||||
if (args.host) {
|
||||
console.log('Available host addresses:');
|
||||
const ips = getLocalIPs();
|
||||
ips.forEach(ip => console.log(` http://${ip}:${args.port}`));
|
||||
}
|
||||
console.log('');
|
||||
console.log('Watching for changes...');
|
||||
console.log('');
|
||||
console.log('Press Ctrl+C to stop.');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
15
data/pwa/README.md
Normal file
15
data/pwa/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
The files in this directory are used for deploying the Mol* Viewer as a PWA (Progressive Web App) at https://molstar.org/viewer/. They may serve as an example for creating your own PWA but wont work as-is. See `/script/deploy.js` for where these files are copied and how they are transformed during deployment.
|
||||
|
||||
|
||||
## PWA features
|
||||
|
||||
- The Service Worker will cache static resources so the Viewer can be used without internet access. This works without installing, i.e., also in Firefox.
|
||||
- Once installed, file types listed in the Manifest can be opened from, e.g., the Windows File Explorer.
|
||||
|
||||
|
||||
## Notes for development
|
||||
|
||||
In Chrome you can see a list of installed PWAs at chrome://apps/. A right-click opens a menu with an option uninstall.
|
||||
|
||||
The Chrome Dev Tools have a section 'Application' to inspect and manage PWA aspects like the Manifest and Service Workers.
|
||||
BIN
data/pwa/logo-144.png
Normal file
BIN
data/pwa/logo-144.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
39
data/pwa/manifest.webmanifest
Normal file
39
data/pwa/manifest.webmanifest
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "https://molstar.org/viewer/",
|
||||
"name": "Mol* Viewer",
|
||||
"short_name": "Mol*",
|
||||
"description": "Mol* Viewer: a modern web app for 3D visualization and analysis of large biomolecular structures.",
|
||||
"start_url": "./index.html",
|
||||
"theme_color": "#eeece7",
|
||||
"background_color": "#eeece7",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "48x48"
|
||||
},
|
||||
{
|
||||
"src": "logo-144.png",
|
||||
"sizes": "144x144"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "./index.html",
|
||||
"accept": {
|
||||
"application/vnd.molstar": [".molx", ".molj"],
|
||||
"text/plain": [
|
||||
".mol", ".mol2", ".sdf", ".sd", ".pdb", ".ent", ".pdbqt", ".cif", ".mcif", ".mmcif", ".xyz", ".gro", ".lammpstrj",
|
||||
".cub", ".cube", ".dx"
|
||||
],
|
||||
"application/octet-stream": [
|
||||
".bcif",
|
||||
".dxbin", ".ccp4", ".mrc", ".map", ".dsn6", ".brix"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"launch_handler": {
|
||||
"client_mode": ["auto"]
|
||||
}
|
||||
}
|
||||
44
data/pwa/pwa.js
Normal file
44
data/pwa/pwa.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Andy Turner <agdturner@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
window.addEventListener('molstarViewerCreated', e => {
|
||||
const viewer = e.detail.viewer;
|
||||
|
||||
// Handle incoming files
|
||||
if ('launchQueue' in window) {
|
||||
launchQueue.setConsumer((launchParams) => {
|
||||
if (!launchParams.files.length) return;
|
||||
|
||||
const files = [];
|
||||
for (const fileHandle of launchParams.files) {
|
||||
files.push(fileHandle.getFile());
|
||||
}
|
||||
|
||||
Promise.all(files).then((files) => {
|
||||
viewer.loadFiles(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Register Progressive Web App service worker.
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('./sw.js')
|
||||
.then(function (registration) {
|
||||
// Registration was successful
|
||||
if (molstar.isDebugMode) {
|
||||
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
||||
}
|
||||
}, function (err) {
|
||||
// registration failed :(
|
||||
if (molstar.isDebugMode) {
|
||||
console.error('ServiceWorker registration failed: ', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
60
data/pwa/sw.js
Normal file
60
data/pwa/sw.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Andy Turner <agdturner@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
/** version from package.json, to be filled in during deployment */
|
||||
const VERSION = '__MOLSTAR_VERSION__';
|
||||
|
||||
const CACHE_NAME = `molstar-viewer-${VERSION}`;
|
||||
|
||||
// The static resources that the app needs to function.
|
||||
const APP_STATIC_RESOURCES = [
|
||||
'favicon.ico',
|
||||
'index.html',
|
||||
'molstar.css',
|
||||
'molstar.js',
|
||||
'manifest.webmanifest',
|
||||
'logo-144.png',
|
||||
'pwa.js'
|
||||
];
|
||||
|
||||
async function cacheStaticResources() {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.addAll(APP_STATIC_RESOURCES);
|
||||
await self.skipWaiting(); // Ensures the new service worker takes control immediately.
|
||||
}
|
||||
|
||||
async function deleteOldCaches() {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(
|
||||
keys.map((key) => {
|
||||
if (key !== CACHE_NAME) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
}),
|
||||
);
|
||||
await self.clients.claim(); // Ensures the new service worker takes control immediately.
|
||||
}
|
||||
|
||||
async function respondWithCacheFirst(request) {
|
||||
// Try to match the request with the cache
|
||||
const cachedResponse = await caches.match(request);
|
||||
return cachedResponse || fetch(request);
|
||||
}
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
// console.log(`Service Worker version ${VERSION} installed.`);
|
||||
event.waitUntil(cacheStaticResources());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// console.log(`Service Worker version ${VERSION} activated.`);
|
||||
event.waitUntil(deleteOldCaches());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(respondWithCacheFirst(event.request));
|
||||
});
|
||||
4
docs/docs/extensions/interactions.md
Normal file
4
docs/docs/extensions/interactions.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Interactions extension
|
||||
|
||||
The Interactions extension enables computing or providing custom interactions between multiple selections/structures.
|
||||
For usage, see the [example source code](https://github.com/molstar/molstar/tree/master/src/examples/interactions).
|
||||
@@ -25,7 +25,7 @@ import { PluginContext } from 'molstar/lib/mol-plugin/context';
|
||||
git clone https://github.com/molstar/molstar.git
|
||||
cd molstar
|
||||
npm install
|
||||
npm build
|
||||
npm run build
|
||||
```
|
||||
|
||||
--------------------
|
||||
|
||||
@@ -70,7 +70,6 @@ This relies on the concept of `Expression` which is basically a intermediate rep
|
||||
### Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object:
|
||||
|
||||
```typescript
|
||||
|
||||
import { MolScriptBuilder as MS, MolScriptBuilder } from 'molstar/lib/mol-script/language/builder';
|
||||
import { Expression } from 'molstar/lib/mol-script/language/expression';
|
||||
import { StructureSelectionQuery } from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query'
|
||||
@@ -82,8 +81,8 @@ export function select_multiple() {
|
||||
const groups: Expression[] = [];
|
||||
for (var chain of args) {
|
||||
groups.push(MS.struct.generator.atomGroups({
|
||||
"chain-test": MS.core.rel.eq([MolScriptBuilder.struct.atomProperty.macromolecular.auth_asym_id(), chain[0]]),
|
||||
"residue-test": MS.core.rel.inRange([MolScriptBuilder.struct.atomProperty.macromolecular.label_seq_id(), chain[1], chain[2]])
|
||||
'chain-test': MS.core.rel.eq([MolScriptBuilder.struct.atomProperty.macromolecular.auth_asym_id(), chain[0]]),
|
||||
'residue-test': MS.core.rel.inRange([MolScriptBuilder.struct.atomProperty.macromolecular.label_seq_id(), chain[1], chain[2]])
|
||||
}));
|
||||
}
|
||||
var sq = StructureSelectionQuery('residue_range_10_15_in_A_and_F', MS.struct.combinator.merge(groups))
|
||||
@@ -98,13 +97,52 @@ Inspect these examples to get a better feeling for this syntax: `https://github.
|
||||
|
||||
Furthermore, a query made this way can be converted to a `Loci` object which is important in many parts of the libary:
|
||||
```typescript
|
||||
|
||||
// Select residue 124 of chain A and convert to Loci
|
||||
const Q = MolScriptBuilder;
|
||||
var sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
||||
'chain-test' : Q.core.rel.eq([Q.struct.atomProperty.macromolecular.auth_asym_id(), A]),
|
||||
"residue-test": Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), 124]),
|
||||
}), objdata)
|
||||
'chain-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.auth_asym_id(), A]),
|
||||
'residue-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), 124]),
|
||||
}), objdata)
|
||||
|
||||
let loci = StructureSelection.toLociWithSourceUnits(sel);
|
||||
```
|
||||
|
||||
## Query Functions
|
||||
|
||||
Instead of building expressions, query functions can be created directly, e.g.:
|
||||
|
||||
```ts
|
||||
import { atoms } from 'mol-model/structure/query/queries/generators';
|
||||
|
||||
const query = atoms({
|
||||
residueTest: ctx => {
|
||||
const seqId = StructureProperties.residue.label_seq_id(ctx.element);
|
||||
return seqId > 10 && seqId < 25;
|
||||
},
|
||||
});
|
||||
|
||||
const selection = query(new QueryContext(structure));
|
||||
// ...
|
||||
```
|
||||
|
||||
## Selection Schema
|
||||
|
||||
For simple selections, the `StructureElement.Schema` can be used to reference elements within a protein structure using mmCIF `atom_site` field names, e.g.:
|
||||
|
||||
```ts
|
||||
const ala121: StructureElement.Schema = { label_asym_id: 'A', label_seq_id: 121 };
|
||||
const residues: StructureElement.Schema = {
|
||||
items: {
|
||||
auth_asym_id: ['A', 'B'],
|
||||
auth_seq_id: [10, 11],
|
||||
}
|
||||
};
|
||||
|
||||
const loci = StructureElement.Loci.fromSchema(structure, residues);
|
||||
```
|
||||
|
||||
Usually, a code editor such as VS Code will auto-suggest all the available field names.
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
@@ -56,6 +56,7 @@ nav:
|
||||
- MolViewSpec: 'extensions/mvs/index.md'
|
||||
- wwPDB StructConn: 'extensions/struct-conn.md'
|
||||
- Tunnels: 'extensions/tunnels.md'
|
||||
- Interactions: 'extensions/interactions.md'
|
||||
- Misc:
|
||||
- Interesting PDB entries: misc/interesting-pdb-entries.md
|
||||
- Exporting component data: exporting-components.md
|
||||
|
||||
456
examples/docking/ligands_1.sdf
Normal file
456
examples/docking/ligands_1.sdf
Normal file
@@ -0,0 +1,456 @@
|
||||
forcefield_3904
|
||||
RDKit 3D
|
||||
|
||||
49 52 0 0 0 0 0 0 0 0999 V2000
|
||||
7.1950 23.7840 6.1780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.7810 23.6440 6.1320 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.0380 24.7500 6.4550 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.8270 25.8570 5.6380 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.0460 26.9150 6.0970 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.4760 26.8670 7.3710 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.6720 25.7630 8.2160 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.0720 25.7780 9.4800 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.8090 25.4470 9.9650 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8080 26.1730 9.4160 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.4770 25.9480 9.8040 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5110 26.6810 9.2540 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.0950 27.9270 8.6960 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1890 28.9860 8.9110 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.6300 30.4110 8.7600 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7130 31.4650 8.9780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4040 31.2910 10.3270 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.9520 29.8780 10.4990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.8690 28.8240 10.2810 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7400 24.9710 10.7610 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.8850 24.4940 11.3660 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4690 23.5570 12.1930 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1090 23.4080 12.1480 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.3700 24.3040 11.2380 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.6480 24.4860 10.8910 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.4600 24.7130 7.7330 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.5263 23.8168 7.2264 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.6649 22.9280 5.6716 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.4879 24.7155 5.6716 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.2751 25.8961 4.6342 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.8776 27.7913 5.4538 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.8616 27.7104 7.7192 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.6420 26.0930 10.2520 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1673 28.2555 9.1874 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.9189 27.8009 7.6175 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.9466 28.8274 8.1294 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8310 30.5615 9.5009 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2424 30.5222 7.7366 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.2524 32.4632 8.9392 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4689 31.3496 8.1873 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.6781 31.4924 11.1285 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.2475 31.9954 10.3749 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.3529 29.7723 11.5179 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7404 29.7213 9.7481 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1096 28.9239 11.0705 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.3427 27.8318 10.3148 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1334 22.9663 12.8408 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.4510 22.7600 12.6820 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.6312 23.8345 8.3724 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 1 0
|
||||
3 4 2 0
|
||||
4 5 1 0
|
||||
5 6 2 0
|
||||
6 7 1 0
|
||||
7 8 1 0
|
||||
8 9 1 0
|
||||
9 10 2 0
|
||||
10 11 1 0
|
||||
11 12 1 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
11 20 2 0
|
||||
20 21 1 0
|
||||
21 22 2 0
|
||||
22 23 1 0
|
||||
23 24 1 0
|
||||
24 25 2 0
|
||||
7 26 2 0
|
||||
26 3 1 0
|
||||
25 9 1 0
|
||||
19 14 1 0
|
||||
24 20 1 0
|
||||
1 27 1 0
|
||||
1 28 1 0
|
||||
1 29 1 0
|
||||
4 30 1 0
|
||||
5 31 1 0
|
||||
6 32 1 0
|
||||
8 33 1 0
|
||||
13 34 1 0
|
||||
13 35 1 0
|
||||
14 36 1 0
|
||||
15 37 1 0
|
||||
15 38 1 0
|
||||
16 39 1 0
|
||||
16 40 1 0
|
||||
17 41 1 0
|
||||
17 42 1 0
|
||||
18 43 1 0
|
||||
18 44 1 0
|
||||
19 45 1 0
|
||||
19 46 1 0
|
||||
22 47 1 0
|
||||
23 48 1 0
|
||||
26 49 1 0
|
||||
M END
|
||||
> <ligandCode> (1)
|
||||
forcefield_3904
|
||||
|
||||
> <ligandName> (1)
|
||||
klr_22
|
||||
|
||||
$$$$
|
||||
forcefield_3905
|
||||
RDKit 3D
|
||||
|
||||
49 52 0 0 0 0 0 0 0 0999 V2000
|
||||
6.5460 25.1350 3.8360 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.2550 25.5560 3.9960 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.5630 25.8360 3.0240 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.7190 25.6170 5.3820 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.0120 26.7570 5.7730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.4880 26.8570 7.0690 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.6630 25.8340 8.0050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.1640 25.8720 9.3110 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.9510 25.5250 9.9030 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8900 26.1930 9.3960 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.3560 25.9480 9.8850 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4490 26.6220 9.3800 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.1050 27.7730 8.6070 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7010 29.0180 9.2810 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5340 30.2520 8.3770 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.5490 31.3650 8.6570 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.0430 31.3400 10.0950 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7320 30.0170 10.4330 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.1730 28.8170 9.6630 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.5160 25.0110 10.9050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5980 24.5310 11.6140 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.0930 23.6490 12.4490 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.2630 23.5390 12.3140 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.6490 24.4020 11.3310 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.8910 24.6070 10.8850 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.3890 24.7010 7.5990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.9170 24.5890 6.3060 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.0340 24.6220 4.5560 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
6.8360 24.9760 2.8790 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.8658 27.5811 5.0592 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.9275 27.7591 7.3554 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7800 26.2290 10.0270 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.0105 27.8693 8.5543 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5056 27.6720 7.5875 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.1446 29.1858 10.2149 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.5231 30.6584 8.5280 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7027 29.9170 7.3429 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0722 32.3370 8.4626 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4156 31.2068 7.9983 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1848 31.4776 10.7693 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7762 32.1504 10.2201 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.6091 29.8266 11.5094 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7879 30.1213 10.1428 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.2575 27.9194 10.2932 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7539 28.7064 8.7355 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.6897 23.0698 13.1690 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8800 22.9330 12.8360 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.5467 23.8809 8.3150 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.4866 23.6927 6.0193 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 2 0
|
||||
2 4 1 0
|
||||
4 5 2 0
|
||||
5 6 1 0
|
||||
6 7 2 0
|
||||
7 8 1 0
|
||||
8 9 1 0
|
||||
9 10 2 0
|
||||
10 11 1 0
|
||||
11 12 1 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
11 20 2 0
|
||||
20 21 1 0
|
||||
21 22 2 0
|
||||
22 23 1 0
|
||||
23 24 1 0
|
||||
24 25 2 0
|
||||
7 26 1 0
|
||||
26 27 2 0
|
||||
27 4 1 0
|
||||
25 9 1 0
|
||||
19 14 1 0
|
||||
24 20 1 0
|
||||
1 28 1 0
|
||||
1 29 1 0
|
||||
5 30 1 0
|
||||
6 31 1 0
|
||||
8 32 1 0
|
||||
13 33 1 0
|
||||
13 34 1 0
|
||||
14 35 1 0
|
||||
15 36 1 0
|
||||
15 37 1 0
|
||||
16 38 1 0
|
||||
16 39 1 0
|
||||
17 40 1 0
|
||||
17 41 1 0
|
||||
18 42 1 0
|
||||
18 43 1 0
|
||||
19 44 1 0
|
||||
19 45 1 0
|
||||
22 46 1 0
|
||||
23 47 1 0
|
||||
26 48 1 0
|
||||
27 49 1 0
|
||||
M END
|
||||
> <ligandCode> (2)
|
||||
forcefield_3905
|
||||
|
||||
> <ligandName> (2)
|
||||
1oiy-1
|
||||
|
||||
$$$$
|
||||
forcefield_14264
|
||||
RDKit 3D
|
||||
|
||||
50 53 0 0 0 0 0 0 0 0999 V2000
|
||||
4.9220 23.4040 3.0090 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.8970 24.7200 3.4040 S 0 0 0 0 0 6 0 0 0 0 0 0
|
||||
7.2120 24.2200 3.7260 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.6670 25.6980 2.3670 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.1310 25.2890 4.8970 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.3720 26.4580 4.8910 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7730 26.8990 6.0760 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.9290 26.1970 7.2780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.3430 26.5790 8.4890 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.1400 26.2820 9.1290 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.0830 26.9670 8.6370 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1520 26.7740 9.1730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2430 27.4630 8.6800 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.9280 28.7750 8.2140 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.2060 29.6320 8.2420 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.2120 29.0810 9.2730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.1810 30.1570 9.7450 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4520 31.3060 10.4440 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0250 31.5210 9.9360 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.8770 31.1190 8.4740 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.3060 25.8730 10.2240 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.3770 25.4460 10.9810 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8720 24.5730 11.8270 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.4750 24.4190 11.6520 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8530 25.2430 10.6320 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.0880 25.3990 10.1410 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.7090 25.0260 7.2550 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.3130 24.5730 6.0780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.1960 23.3080 3.7210 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.4460 22.5390 2.8810 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.2447 27.0304 3.9603 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.1666 27.8167 6.0634 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.8380 27.2640 9.0420 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1661 29.2261 8.8668 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.5358 28.7227 7.1876 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.6885 29.5690 7.2555 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7847 28.2635 8.8106 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.6467 28.7218 10.1456 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7231 30.5555 8.8747 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.8753 29.7014 10.4664 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.0232 32.2317 10.2804 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.3803 31.0434 11.5098 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7667 32.5851 10.0403 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.3511 30.8895 10.5336 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.5603 31.7316 7.8675 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8284 31.2816 8.1841 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4619 24.0333 12.5825 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.0910 23.8110 12.1720 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.8468 24.4538 8.1843 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.9261 23.6598 6.0846 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 2 0
|
||||
2 4 2 0
|
||||
2 5 1 0
|
||||
5 6 2 0
|
||||
6 7 1 0
|
||||
7 8 2 0
|
||||
8 9 1 0
|
||||
9 10 1 0
|
||||
10 11 2 0
|
||||
11 12 1 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
19 20 1 0
|
||||
12 21 2 0
|
||||
21 22 1 0
|
||||
22 23 2 0
|
||||
23 24 1 0
|
||||
24 25 1 0
|
||||
25 26 2 0
|
||||
8 27 1 0
|
||||
27 28 2 0
|
||||
28 5 1 0
|
||||
26 10 1 0
|
||||
20 15 1 0
|
||||
25 21 1 0
|
||||
1 29 1 0
|
||||
1 30 1 0
|
||||
6 31 1 0
|
||||
7 32 1 0
|
||||
9 33 1 0
|
||||
14 34 1 0
|
||||
14 35 1 0
|
||||
15 36 1 0
|
||||
16 37 1 0
|
||||
16 38 1 0
|
||||
17 39 1 0
|
||||
17 40 1 0
|
||||
18 41 1 0
|
||||
18 42 1 0
|
||||
19 43 1 0
|
||||
19 44 1 0
|
||||
20 45 1 0
|
||||
20 46 1 0
|
||||
23 47 1 0
|
||||
24 48 1 0
|
||||
27 49 1 0
|
||||
28 50 1 0
|
||||
M END
|
||||
> <ligandCode> (3)
|
||||
forcefield_14264
|
||||
|
||||
> <ligandName> (3)
|
||||
1h1s
|
||||
|
||||
$$$$
|
||||
forcefield_14265
|
||||
RDKit 3D
|
||||
|
||||
50 53 0 0 0 0 0 0 0 0999 V2000
|
||||
5.9560 25.0880 4.1850 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.6880 24.2300 5.6100 S 0 0 0 0 0 6 0 0 0 0 0 0
|
||||
4.9010 23.0800 5.2270 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
6.9470 24.1160 6.3060 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.6500 25.3510 6.5050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.3190 26.5790 5.9340 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.4970 27.4490 6.6410 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.0220 27.0950 7.9100 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.3680 25.8770 8.5100 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.9060 25.4620 9.7630 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.6520 25.1280 10.2760 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.6590 25.9320 9.8350 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.6150 25.7270 10.2650 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.6410 26.5420 9.8330 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2650 27.5510 8.8940 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4150 28.9670 9.4830 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0650 29.8960 8.4420 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7130 31.1080 9.0990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.8410 30.6980 10.0490 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.6490 29.3080 10.6570 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1750 28.9600 10.8270 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8720 24.6910 11.1610 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0060 24.2140 11.7870 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5910 23.2030 12.5210 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.2430 23.0050 12.3980 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.2320 23.9470 11.5330 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.5000 24.1080 11.1380 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.1890 25.0030 7.7800 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
6.4350 24.4790 3.5180 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.1080 25.4920 3.7910 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.7020 26.8549 4.9404 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.2187 28.4186 6.2023 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.3612 27.7900 8.4488 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.5640 25.4960 10.5280 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.2150 27.3950 8.6054 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.9312 27.4715 8.0223 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.4134 29.3591 9.7135 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2920 30.2413 7.7397 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.8482 29.3296 7.9168 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.9484 31.6568 9.6685 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.1423 31.7400 8.3076 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.8921 31.4320 10.8667 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7682 30.6682 9.4579 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.1367 29.2806 11.6426 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.0960 28.5721 9.9724 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7122 29.6974 11.4994 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1140 27.9439 11.2440 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.2486 22.5915 13.1563 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.3120 22.2980 12.8550 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.4733 24.0335 8.2150 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 2 0
|
||||
2 4 2 0
|
||||
2 5 1 0
|
||||
5 6 2 0
|
||||
6 7 1 0
|
||||
7 8 2 0
|
||||
8 9 1 0
|
||||
9 10 1 0
|
||||
10 11 1 0
|
||||
11 12 2 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
19 20 1 0
|
||||
20 21 1 0
|
||||
13 22 2 0
|
||||
22 23 1 0
|
||||
23 24 2 0
|
||||
24 25 1 0
|
||||
25 26 1 0
|
||||
26 27 2 0
|
||||
9 28 2 0
|
||||
28 5 1 0
|
||||
27 11 1 0
|
||||
21 16 1 0
|
||||
26 22 1 0
|
||||
1 29 1 0
|
||||
1 30 1 0
|
||||
6 31 1 0
|
||||
7 32 1 0
|
||||
8 33 1 0
|
||||
10 34 1 0
|
||||
15 35 1 0
|
||||
15 36 1 0
|
||||
16 37 1 0
|
||||
17 38 1 0
|
||||
17 39 1 0
|
||||
18 40 1 0
|
||||
18 41 1 0
|
||||
19 42 1 0
|
||||
19 43 1 0
|
||||
20 44 1 0
|
||||
20 45 1 0
|
||||
21 46 1 0
|
||||
21 47 1 0
|
||||
24 48 1 0
|
||||
25 49 1 0
|
||||
28 50 1 0
|
||||
M END
|
||||
> <ligandCode> (4)
|
||||
forcefield_14265
|
||||
|
||||
> <ligandName> (4)
|
||||
1oiu
|
||||
|
||||
$$$$
|
||||
9013
examples/docking/receptor_1.pdb
Normal file
9013
examples/docking/receptor_1.pdb
Normal file
File diff suppressed because it is too large
Load Diff
45576
package-lock.json
generated
45576
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.10.0",
|
||||
"version": "4.14.1",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -10,6 +10,9 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/molstar/molstar/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
@@ -32,6 +35,11 @@
|
||||
"watch-webpack": "webpack -w --mode development --stats minimal",
|
||||
"watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
|
||||
"watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
|
||||
"dev": "node build-dev.mjs",
|
||||
"dev:all": "node build-dev.mjs -a -e",
|
||||
"dev:viewer": "node build-dev.mjs -a viewer",
|
||||
"dev:apps": "node build-dev.mjs -a",
|
||||
"dev:examples": "node build-dev.mjs -e",
|
||||
"serve": "http-server -p 1338 -g",
|
||||
"model-server": "node lib/commonjs/servers/model/server.js",
|
||||
"model-server-watch": "nodemon --watch lib lib/commonjs/servers/model/server.js",
|
||||
@@ -114,7 +122,11 @@
|
||||
"Eric E <etongfu@outlook.com>",
|
||||
"Xavier Martinez <xavier.martinez.xm@gmail.com>",
|
||||
"Alex Chan <smalldirkalex@gmail.com>",
|
||||
"Simeon Borko <simeon.borko@gmail.com>"
|
||||
"Simeon Borko <simeon.borko@gmail.com>",
|
||||
"Ventura Rivera <venturaxrivera@gmail.com>",
|
||||
"Andy Turner <agdturner@gmail.com>",
|
||||
"Lukáš Polák <admin@lukaspolak.cz>",
|
||||
"Chetan Mishra <chetan.s115@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -122,19 +134,21 @@
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.16",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.1.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"cpx2": "^8.0.0",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "^8.57.1",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
@@ -143,22 +157,22 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.83.0",
|
||||
"sass": "^1.83.4",
|
||||
"sass-loader": "^16.0.4",
|
||||
"simple-git": "^3.27.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.7.3",
|
||||
"webpack": "^5.97.1",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.7.5",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^18.19.68",
|
||||
"@types/node": "^18.19.74",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/swagger-ui-dist": "3.30.5",
|
||||
"argparse": "^2.0.1",
|
||||
@@ -170,11 +184,11 @@
|
||||
"immutable": "^5.0.3",
|
||||
"io-ts": "^2.2.22",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-markdown": "^9.0.3",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-dist": "^5.18.2",
|
||||
"tslib": "^2.8.1",
|
||||
"util.promisify": "^1.1.2",
|
||||
"util.promisify": "^1.1.3",
|
||||
"xhr2": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -9,7 +9,10 @@ const path = require('path');
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
|
||||
const VERSION = require(path.resolve(__dirname, '../package.json')).version;
|
||||
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git";
|
||||
const dataDir = path.resolve(__dirname, '../data/');
|
||||
const buildDir = path.resolve(__dirname, '../build/');
|
||||
const deployDir = path.resolve(buildDir, 'deploy/');
|
||||
const localPath = path.resolve(deployDir, 'molstar.github.io/');
|
||||
@@ -17,6 +20,12 @@ const localPath = path.resolve(deployDir, 'molstar.github.io/');
|
||||
const analyticsTag = /<!-- __MOLSTAR_ANALYTICS__ -->/g;
|
||||
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics --><iframe src="https://web3dsurvey.com/collector-iframe.html" style="width: 1px; height: 1px;"></iframe>`;
|
||||
|
||||
const manifestTag = /<!-- __MOLSTAR_MANIFEST__ -->/g;
|
||||
const manifestCode = `<link rel="manifest" href="./manifest.webmanifest">`;
|
||||
|
||||
const pwaTag = /<!-- __MOLSTAR_PWA__ -->/g;
|
||||
const pwaCode = `<script src='./pwa.js'></script>`;
|
||||
|
||||
function log(command, stdout, stderr) {
|
||||
if (command) {
|
||||
console.log('\n###', command);
|
||||
@@ -31,17 +40,41 @@ function addAnalytics(path) {
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function addManifest(path) {
|
||||
const data = fs.readFileSync(path, 'utf8');
|
||||
const result = data.replace(manifestTag, manifestCode);
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function addPwa(path) {
|
||||
const data = fs.readFileSync(path, 'utf8');
|
||||
const result = data.replace(pwaTag, pwaCode);
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function addVersion(path) {
|
||||
const data = fs.readFileSync(path, 'utf8');
|
||||
const result = data.replace('__MOLSTAR_VERSION__', VERSION);
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function copyViewer() {
|
||||
console.log('\n###', 'copy viewer files');
|
||||
const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
|
||||
const viewerBuildPath = path.resolve(buildDir, 'viewer/');
|
||||
const viewerDeployPath = path.resolve(localPath, 'viewer/');
|
||||
fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(viewerDeployPath, 'index.html'));
|
||||
addManifest(path.resolve(viewerDeployPath, 'index.html'));
|
||||
addPwa(path.resolve(viewerDeployPath, 'index.html'));
|
||||
|
||||
const pwaDataPath = path.resolve(dataDir, 'pwa/');
|
||||
fse.copySync(pwaDataPath, viewerDeployPath, { overwrite: true });
|
||||
addVersion(path.resolve(viewerDeployPath, 'sw.js'));
|
||||
}
|
||||
|
||||
function copyMe() {
|
||||
console.log('\n###', 'copy me files');
|
||||
const meBuildPath = path.resolve(buildDir, '../build/mesoscale-explorer/');
|
||||
const meBuildPath = path.resolve(buildDir, 'mesoscale-explorer/');
|
||||
const meDeployPath = path.resolve(localPath, 'me/viewer/');
|
||||
fse.copySync(meBuildPath, meDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(meDeployPath, 'index.html'));
|
||||
@@ -49,12 +82,12 @@ function copyMe() {
|
||||
|
||||
function copyDemos() {
|
||||
console.log('\n###', 'copy demos files');
|
||||
const lightingBuildPath = path.resolve(buildDir, '../build/examples/lighting/');
|
||||
const lightingBuildPath = path.resolve(buildDir, 'examples/lighting/');
|
||||
const lightingDeployPath = path.resolve(localPath, 'demos/lighting/');
|
||||
fse.copySync(lightingBuildPath, lightingDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(lightingDeployPath, 'index.html'));
|
||||
|
||||
const orbitalsBuildPath = path.resolve(buildDir, '../build/examples/alpha-orbitals/');
|
||||
const orbitalsBuildPath = path.resolve(buildDir, 'examples/alpha-orbitals/');
|
||||
const orbitalsDeployPath = path.resolve(localPath, 'demos/alpha-orbitals/');
|
||||
fse.copySync(orbitalsBuildPath, orbitalsDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(orbitalsDeployPath, 'index.html'));
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import './index.html';
|
||||
import { ShowButtons, StructurePreset, ViewportComponent } from './viewport';
|
||||
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setDebugMode, setProductionMode } from '../../mol-util/debug';
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MesoFocusLoci } from './behavior/camera';
|
||||
import { GraphicsMode, MesoscaleState } from './data/state';
|
||||
import { MesoSelectLoci } from './behavior/select';
|
||||
import { Transparency } from '../../mol-gl/webgl/render-item';
|
||||
import { LoadModel, loadExampleEntry, loadPdb, loadPdbDev, loadUrl, openState } from './ui/states';
|
||||
import { LoadModel, loadExampleEntry, loadPdb, loadPdbIhm, loadUrl, openState } from './ui/states';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { AnimateCameraSpin } from '../../mol-plugin-state/animation/built-in/camera-spin';
|
||||
import { AnimateCameraRock } from '../../mol-plugin-state/animation/built-in/camera-rock';
|
||||
@@ -85,7 +85,7 @@ const DefaultMesoscaleExplorerOptions = {
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowSelectionMode: true,
|
||||
viewportShowAnimation: false,
|
||||
viewportShowTrajectoryControls: false,
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
@@ -120,8 +120,15 @@ export class MesoscaleExplorer {
|
||||
await loadPdb(this.plugin, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in v5. Use {@link loadPdbIhm | loadPdbIhm(id: string)} instead.
|
||||
*/
|
||||
async loadPdbDev(id: string) {
|
||||
await loadPdbDev(this.plugin, id);
|
||||
await this.loadPdbIhm(id);
|
||||
}
|
||||
|
||||
async loadPdbIhm(id: string) {
|
||||
await loadPdbIhm(this.plugin, id);
|
||||
}
|
||||
|
||||
static async create(elementOrId: string | HTMLElement, options: Partial<MesoscaleExplorerOptions> = {}) {
|
||||
@@ -143,7 +150,7 @@ export class MesoscaleExplorer {
|
||||
|
||||
PluginSpec.Behavior(MesoFocusLoci),
|
||||
PluginSpec.Behavior(MesoSelectLoci),
|
||||
|
||||
PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
|
||||
...o.extensions.map(e => Extensions[e]),
|
||||
],
|
||||
animations: [
|
||||
|
||||
@@ -73,7 +73,7 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
|
||||
this.ctx.managers.interactivity.lociSelects.deselectAll();
|
||||
return;
|
||||
}
|
||||
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : 'chain');
|
||||
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : this.ctx.managers.interactivity.props.granularity);
|
||||
this.ctx.managers.interactivity.lociSelects.toggle({ loci }, false);
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
|
||||
@@ -116,7 +116,7 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
|
||||
if (modifiers.control) {
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
|
||||
} else {
|
||||
const loci = Loci.normalize(current.loci, 'chain');
|
||||
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,12 +192,13 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
const t = isBinary ? d : utf8Read(d, 0, d.length);
|
||||
const file = Asset.File(new File([t], ent.file));
|
||||
|
||||
const color = ColorNames.skyblue;
|
||||
const color = (ent.color) ? Color.fromRgb(ent.color[0], ent.color[1], ent.color[2]) : ColorNames.skyblue;
|
||||
|
||||
const sizeFactor = ent.sizeFactor || 1;
|
||||
const tags = ent.groups.map(({ id, root }) => `${root}:${id}`);
|
||||
const instances = ent.instances && getAssetInstances(ent.instances);
|
||||
const description = ent.description;
|
||||
|
||||
const label = ent.label || ent.file.split('.')[0];
|
||||
build = build
|
||||
.toRoot()
|
||||
@@ -300,6 +301,7 @@ type GenericEntity = {
|
||||
file: string
|
||||
label?: string
|
||||
description?: string
|
||||
color?: number[]
|
||||
groups: {
|
||||
/** reference to `${GenericGroup.id}` */
|
||||
id: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,7 +12,7 @@ import { Color } from '../../../mol-util/color';
|
||||
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ParamMapping } from '../../../mol-util/param-mapping';
|
||||
import { EntityNode } from '../ui/entities';
|
||||
import { DistinctColorsProps, distinctColors } from '../../../mol-util/color/distinct';
|
||||
@@ -211,7 +211,8 @@ export function getClipObjects(values: SimpleClipProps, boundingSphere: Sphere3D
|
||||
invert: values.invert,
|
||||
position,
|
||||
scale,
|
||||
rotation: values.rotation
|
||||
rotation: values.rotation,
|
||||
transform: Mat4.identity(),
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -93,9 +93,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var pdbihm = getParam('pdbihm', '[^&]+').trim();
|
||||
if (pdbihm) {
|
||||
me.loadPdbIhm(pdbihm);
|
||||
return;
|
||||
}
|
||||
// support for deprecated pdb-dev param
|
||||
var pdbdev = getParam('pdbdev', '[^&]+').trim();
|
||||
if (pdbdev) {
|
||||
me.loadPdbDev(pdbdev);
|
||||
me.loadPdbIhm(pdbdev);
|
||||
return;
|
||||
}
|
||||
window.addEventListener('unload', () => {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
|
||||
import './favicon.ico';
|
||||
import './index.html';
|
||||
require('./style.scss');
|
||||
import './style.scss';
|
||||
export * from './app';
|
||||
|
||||
@@ -25,8 +25,8 @@ $logo-background: rgba(0,0,0,0.75);
|
||||
@return color.adjust($color, $lightness: $amount, $space: hsl);
|
||||
}
|
||||
|
||||
@import 'mol-plugin-ui/skin/base/base';
|
||||
@import 'mol-plugin-ui/skin/base/variables';
|
||||
@import '../../mol-plugin-ui/skin/base/base';
|
||||
@import '../../mol-plugin-ui/skin/base/variables';
|
||||
|
||||
a {
|
||||
color: $font-color;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { MesoFocusLoci } from '../behavior/camera';
|
||||
import Markdown from 'react-markdown';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { ColorLoaderControls } from './states';
|
||||
|
||||
function centerLoci(plugin: PluginContext, loci: Loci, durationMs = 250) {
|
||||
const { canvas3d } = plugin;
|
||||
@@ -619,6 +620,7 @@ class Node<P extends {}, S extends { isDisabled: boolean }> extends PluginUIComp
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean, action?: 'color' | 'clip' | 'root', isDisabled: boolean }> {
|
||||
state = {
|
||||
isCollapsed: !!this.props.cell.state.isCollapsed,
|
||||
@@ -881,12 +883,13 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
const root = (isRoot && this.allGroups.length > 1) && <IconButton svg={BrushSvg} toggleState={false} disabled={disabled} small onClick={this.toggleRoot} />;
|
||||
const clip = <IconButton svg={ContentCutSvg} toggleState={false} disabled={disabled} small onClick={this.toggleClip} />;
|
||||
const visibility = <IconButton svg={state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} disabled={disabled} small onClick={this.toggleVisible} />;
|
||||
|
||||
const loadColorButton = (depth === 0) && <ColorLoaderControls plugin={this.plugin} />;
|
||||
return <>
|
||||
<div className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${depth * 10 + 5}px` }}>
|
||||
{expand}
|
||||
{label}
|
||||
{root || color}
|
||||
{loadColorButton}
|
||||
{clip}
|
||||
{visibility}
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,10 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
|
||||
import { createGenericHierarchy } from '../data/generic/preset';
|
||||
import { createMmcifHierarchy } from '../data/mmcif/preset';
|
||||
import { createPetworldHierarchy } from '../data/petworld/preset';
|
||||
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { getAllEntities, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { now } from '../../../mol-util/now';
|
||||
import { readFromFile } from '../../../mol-util/data-source';
|
||||
|
||||
function adjustPluginProps(ctx: PluginContext) {
|
||||
const customState = ctx.customState as MesoscaleExplorerState;
|
||||
@@ -211,27 +212,54 @@ export async function loadPdb(ctx: PluginContext, id: string) {
|
||||
await createHierarchy(ctx, data.ref);
|
||||
}
|
||||
|
||||
export async function loadPdbDev(ctx: PluginContext, id: string) {
|
||||
export async function loadPdbIhm(ctx: PluginContext, id: string) {
|
||||
await reset(ctx);
|
||||
let url: string;
|
||||
// 4 character PDB id, TODO: support extended PDB ID
|
||||
if (id.match(/^[1-9][A-Z0-9]{3}$/i) !== null) {
|
||||
url = `https://pdb-dev.wwpdb.org/bcif/${id.toLowerCase()}.bcif`;
|
||||
url = `https://pdb-ihm.org/bcif/${id.toLowerCase()}.bcif`;
|
||||
} else {
|
||||
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
|
||||
url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
|
||||
url = `https://pdb-ihm.org/bcif/${nId.toUpperCase()}.bcif`;
|
||||
}
|
||||
const data = await ctx.builders.data.download({ url, isBinary: true });
|
||||
await createHierarchy(ctx, data.ref);
|
||||
}
|
||||
|
||||
async function loadColors(ctx: PluginContext, file: File) {
|
||||
const data = await ctx.runTask(readFromFile(file, 'string'));
|
||||
const colorData = JSON.parse(data);
|
||||
|
||||
const update = ctx.state.data.build();
|
||||
const allEntities = getAllEntities(ctx);
|
||||
|
||||
for (const entityCell of allEntities) {
|
||||
const label = getEntityLabel(ctx, entityCell);
|
||||
const tags = entityCell.transform.tags;
|
||||
const fullname = (tags?.[0].replace('comp:', '') ?? '') + '.' + label;
|
||||
// test each tag, siwtch to uniform color
|
||||
if (fullname in colorData) {
|
||||
const { x, y, z } = colorData[fullname];
|
||||
const color = Color.fromRgb(x, y, z);
|
||||
update.to(entityCell).update(old => {
|
||||
if (old.type) {
|
||||
old.colorTheme = { name: 'uniform', params: { value: color, lightness: old.colorTheme.params.lightness } };
|
||||
old.type.params.color = color;
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.color = color;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await update.commit();
|
||||
}
|
||||
//
|
||||
|
||||
export const LoadDatabase = StateAction.build({
|
||||
display: { name: 'Database', description: 'Load from Database' },
|
||||
params: (a, ctx: PluginContext) => {
|
||||
return {
|
||||
source: PD.Select('pdb', PD.objectToOptions({ pdb: 'PDB', pdbDev: 'PDB-Dev' })),
|
||||
source: PD.Select('pdb', PD.objectToOptions({ pdb: 'PDB', pdbIhm: 'PDB-IHM' })),
|
||||
entry: PD.Text(''),
|
||||
};
|
||||
},
|
||||
@@ -239,8 +267,8 @@ export const LoadDatabase = StateAction.build({
|
||||
})(({ params }, ctx: PluginContext) => Task.create('Loading from database...', async taskCtx => {
|
||||
if (params.source === 'pdb') {
|
||||
await loadPdb(ctx, params.entry);
|
||||
} else if (params.source === 'pdbDev') {
|
||||
await loadPdbDev(ctx, params.entry);
|
||||
} else if (params.source === 'pdbIhm') {
|
||||
await loadPdbIhm(ctx, params.entry);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -299,7 +327,6 @@ export const LoadModel = StateAction.build({
|
||||
}
|
||||
}));
|
||||
|
||||
//
|
||||
|
||||
export class DatabaseControls extends PluginUIComponent {
|
||||
componentDidMount() {
|
||||
@@ -337,6 +364,30 @@ export class ExampleControls extends PluginUIComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export function ColorLoaderControls({ plugin }: { plugin: PluginContext }) {
|
||||
const triggerLoadColors = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = async (e) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files || !input.files[0]) return;
|
||||
const file = input.files[0];
|
||||
await loadColors(plugin, new File([file], file.name));
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
svg={OpenInBrowserSvg}
|
||||
title="Load Colors"
|
||||
onClick={triggerLoadColors}
|
||||
small
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function openState(ctx: PluginContext, file: File) {
|
||||
const customState = ctx.customState as MesoscaleExplorerState;
|
||||
delete customState.stateRef;
|
||||
@@ -426,7 +477,7 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
|
||||
driver.setSteps([
|
||||
// Left panel
|
||||
{ element: '#explorerinfo', popover: { title: 'Explorer Header Info', description: 'This section displays the explorer header with version information, documentation access, and tour navigation. Use the right and left arrow keys to navigate the tour.', side: 'left', align: 'start' } },
|
||||
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-DEV databases.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-IHM databases.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#loader', popover: { title: 'Import from File', description: 'Load local files (.molx, .molj, .zip, .cif, .bcif) using this option.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#example', popover: { title: 'Example Models and Tours', description: 'Select from a range of example models and tours provided.', side: 'left', align: 'start' } },
|
||||
{ element: '#session', popover: { title: 'Session Management', description: 'Download the current session in .molx format.', side: 'top', align: 'start' } },
|
||||
@@ -486,7 +537,6 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MesoQuickStylesControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 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>
|
||||
@@ -24,7 +24,6 @@ import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
|
||||
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
|
||||
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
|
||||
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
|
||||
import { ZenodoImport } from '../../extensions/zenodo';
|
||||
@@ -58,16 +57,16 @@ import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { OpenFiles } from '../../mol-plugin-state/actions/file';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { consoleStats, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
|
||||
export { consoleStats, setDebugMode, setProductionMode, setTimingMode, isProductionMode, isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
|
||||
const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
export const ExtensionMap = {
|
||||
'volseg': PluginSpec.Behavior(Volseg),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
@@ -121,7 +120,6 @@ const DefaultViewerOptions = {
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
|
||||
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
|
||||
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
|
||||
@@ -202,7 +200,6 @@ export class Viewer {
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
|
||||
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
|
||||
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
|
||||
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
|
||||
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
|
||||
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
|
||||
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
|
||||
@@ -288,14 +285,21 @@ export class Viewer {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in v5. Use {@link loadPdbIhm | loadPdbIhm(pdbIhm: string)} instead.
|
||||
*/
|
||||
loadPdbDev(pdbDev: string) {
|
||||
return this.loadPdbIhm(pdbDev);
|
||||
}
|
||||
|
||||
loadPdbIhm(pdbIhm: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb-dev' as const,
|
||||
name: 'pdb-ihm' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdbDev,
|
||||
id: pdbIhm,
|
||||
encoding: 'bcif',
|
||||
},
|
||||
options: params.source.params.options,
|
||||
@@ -489,7 +493,8 @@ export class Viewer {
|
||||
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
|
||||
|
||||
const provider = plugin.dataFormats.get(params.model.format);
|
||||
model = await provider!.parse(plugin, data);
|
||||
const parsed = await provider!.parse(plugin, data);
|
||||
model = parsed.topology;
|
||||
}
|
||||
|
||||
const data = params.coordinates.kind === 'coordinates-data'
|
||||
@@ -553,6 +558,23 @@ export class Viewer {
|
||||
}
|
||||
}
|
||||
|
||||
loadFiles(files: File[]) {
|
||||
const sessions = files.filter(f => {
|
||||
const fn = f.name.toLowerCase();
|
||||
return fn.endsWith('.molx') || fn.endsWith('.molj');
|
||||
});
|
||||
|
||||
if (sessions.length > 0) {
|
||||
return PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: sessions[0] });
|
||||
} else {
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(OpenFiles, {
|
||||
files: files.map(f => Asset.File(f)),
|
||||
format: { name: 'auto', params: {} },
|
||||
visuals: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.plugin.layout.events.updated.next(void 0);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<!-- __MOLSTAR_MANIFEST__ -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
@@ -111,8 +112,11 @@
|
||||
var pdb = getParam('pdb', '[^&]+').trim();
|
||||
if (pdb) viewer.loadPdb(pdb);
|
||||
|
||||
var pdbIhm = getParam('pdb-ihm', '[^&]+').trim();
|
||||
if (pdbIhm) viewer.loadPdbIhm(pdbIhm);
|
||||
// support for deprecated pdb-dev param
|
||||
var pdbDev = getParam('pdb-dev', '[^&]+').trim();
|
||||
if (pdbDev) viewer.loadPdbDev(pdbDev);
|
||||
if (pdbDev) viewer.loadPdbIhm(pdbDev);
|
||||
|
||||
var emdb = getParam('emdb', '[^&]+').trim();
|
||||
if (emdb) viewer.loadEmdb(emdb);
|
||||
@@ -127,8 +131,12 @@
|
||||
// to aid GC
|
||||
viewer.dispose();
|
||||
});
|
||||
|
||||
const event = new CustomEvent("molstarViewerCreated", { detail: { viewer } });
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
</script>
|
||||
<!-- __MOLSTAR_PWA__ -->
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,5 +8,5 @@
|
||||
import './embedded.html';
|
||||
import './favicon.ico';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
export * from './app';
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
<div id="app"></div>
|
||||
<div id='controls'></div>
|
||||
<div id='sponsor'>
|
||||
<a href='https://www.entos.ai/envision' target="_blank" rel="noopener">
|
||||
<svg class="makeStyles-root-46" viewBox="0 0 190 36" xmlns="http://www.w3.org/2000/svg"><path d="M32.2591 28.6707C32.2591 32.3914 29.2421 35.407 25.5214 35.407C22.0752 35.407 19.2338 32.8206 18.8325 29.4831V29.4775C18.8143 29.3312 18.8018 29.1835 18.7934 29.0344C18.7934 29.0316 18.7921 29.0274 18.7921 29.0246V29.0177C18.7865 28.902 18.7837 28.7864 18.7837 28.6707C18.7837 26.2557 20.0532 24.1389 21.9609 22.9503C21.9623 22.9489 21.9651 22.9489 21.9665 22.9475C22.0933 22.8666 22.2243 22.7914 22.3581 22.7203C22.3581 22.7203 22.3595 22.7203 22.3595 22.7189C23.3029 22.2173 24.3787 21.933 25.5214 21.933C29.2421 21.933 32.2591 24.9486 32.2591 28.6707Z"></path><path d="M25.5214 14.0692C29.2421 14.0692 32.2591 11.0522 32.2591 7.33146C32.2591 3.61074 29.2421 0.59375 25.5214 0.59375C22.0529 0.59375 19.1962 3.21637 18.8255 6.58592C18.8185 6.67092 18.8116 6.75454 18.8018 6.83815C18.7893 7.00119 18.7837 7.16563 18.7837 7.33146C18.7837 9.73669 20.0434 11.8465 21.94 13.038C22.0891 13.116 22.2355 13.201 22.3776 13.2916C22.3783 13.2923 22.379 13.2926 22.3797 13.293C22.3804 13.2933 22.3811 13.2937 22.3818 13.2944C23.3196 13.7891 24.3871 14.0692 25.5214 14.0692Z"></path><path d="M19.3645 12.4113C20.2926 12.4113 21.1694 12.638 21.94 13.038C20.0434 11.8465 18.7837 9.73669 18.7837 7.33146C18.7837 7.16563 18.7893 7.00119 18.8018 6.83815C18.4688 9.76455 16.1385 12.0866 13.2065 12.4044C13.8545 13.1193 14.3785 13.9484 14.745 14.857C15.7497 13.3798 17.4443 12.4113 19.3645 12.4113Z"></path><path d="M14.7312 21.1249V21.1236C14.1279 20.2331 13.7767 19.1587 13.7767 18.0007V17.9728C13.7767 15.3084 12.2285 13.0035 9.9835 11.911C9.98141 11.9103 9.97967 11.9096 9.97793 11.9089C9.97619 11.9082 9.97444 11.9075 9.97235 11.9068C9.96817 11.904 9.96538 11.9026 9.9612 11.9012C9.95981 11.9012 9.95981 11.8998 9.95981 11.8998C9.9417 11.8915 9.92394 11.8831 9.90618 11.8747C9.8884 11.8664 9.87063 11.858 9.85251 11.8497C9.82046 11.8343 9.78701 11.819 9.75357 11.8051L9.74521 11.8009C8.91745 11.4372 8.0019 11.2351 7.03898 11.2351C3.31826 11.2351 0.30127 14.2521 0.30127 17.9728C0.30127 21.6935 3.31826 24.7105 7.03898 24.7105C7.98797 24.7105 8.89098 24.514 9.71037 24.1601C9.71246 24.1594 9.7142 24.1583 9.71594 24.1573C9.71768 24.1562 9.71943 24.1552 9.72152 24.1545C9.8107 24.1169 9.8985 24.0765 9.9849 24.0333L9.98629 24.0319C10.7625 23.6919 11.6181 23.5037 12.5197 23.5037C12.7524 23.5037 12.9824 23.5163 13.2081 23.54C13.2082 23.5399 13.2081 23.54 13.2081 23.54C15.0168 23.7365 16.5971 24.695 17.6185 26.0885C17.9195 25.1688 18.3752 24.3201 18.9563 23.5732C17.1964 23.4464 15.6635 22.5058 14.7312 21.1249Z"></path><g clip-path="url(#clip0)"><path d="M106.391 18.0021C106.391 11.3724 101.039 6 94.4389 6H88.4585C81.8581 6 76.5061 11.3724 76.5061 18.0021V30.0042H81.2845V18.0021C81.2845 14.0268 84.4941 10.8008 88.4585 10.8008H94.4347C98.395 10.8008 101.609 14.0226 101.609 18.0021V30.0042H106.391V18.0021Z"></path><path d="M149.432 6H142.258C135.653 6 130.301 11.3724 130.301 18.0021C130.301 24.6319 135.653 30.0042 142.258 30.0042H149.432C156.036 30.0042 161.388 24.6319 161.388 18.0021C161.388 11.3724 156.032 6 149.432 6ZM149.432 25.1992H142.258C138.297 25.1992 135.084 21.9774 135.084 17.9979C135.084 14.0183 138.293 10.7966 142.258 10.7966H149.432C153.392 10.7966 156.606 14.0183 156.606 17.9979C156.606 21.9774 153.392 25.1992 149.432 25.1992Z"></path><path d="M74.1151 25.1992H58.5736C55.4526 25.1992 52.804 23.1924 51.8171 20.3983H74.1151V17.9979C74.1151 17.1808 74.1868 16.3807 74.3175 15.5975H51.8171C52.804 12.8033 55.4526 10.7966 58.5736 10.7966H76.0383C77.1475 8.87458 78.6911 7.22773 80.5299 6H58.5736C51.969 6 46.6169 11.3724 46.6169 18.0021C46.6169 24.6276 51.969 30 58.5736 30H74.1151V25.1992Z"></path><path d="M120.74 6H115.958H102.369C104.212 7.22773 105.751 8.87458 106.861 10.8008H115.958V30H120.74V10.8008H129.838C130.947 8.87458 132.486 7.22773 134.329 6H120.74Z"></path><path d="M182.906 15.6017H169.756C168.436 15.6017 167.365 14.5264 167.365 13.2013C167.365 11.8762 168.436 10.8008 169.756 10.8008H188.882V6H169.756C165.796 6 162.582 9.22173 162.582 13.2013C162.582 17.1808 165.791 20.4025 169.756 20.4025H182.906C184.226 20.4025 185.297 21.4779 185.297 22.803C185.297 24.1281 184.226 25.2034 182.906 25.2034H161.852C160.743 27.1297 159.199 28.7765 157.361 30.0042H182.906C186.866 30.0042 190.08 26.7825 190.08 22.803C190.08 18.8234 186.866 15.6017 182.906 15.6017Z"></path></g><defs><clipPath id="clip0"><rect width="190" height="24" fill="white" transform="translate(0 6)"></rect></clipPath></defs></svg>
|
||||
<a href='https://www.iambic-envision.com/' target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 190 36" xmlns="http://www.w3.org/2000/svg"><path d="M32.2591 28.6707C32.2591 32.3914 29.2421 35.407 25.5214 35.407C22.0752 35.407 19.2338 32.8206 18.8325 29.4831V29.4775C18.8143 29.3312 18.8018 29.1835 18.7934 29.0344C18.7934 29.0316 18.7921 29.0274 18.7921 29.0246V29.0177C18.7865 28.902 18.7837 28.7864 18.7837 28.6707C18.7837 26.2557 20.0532 24.1389 21.9609 22.9503C21.9623 22.9489 21.9651 22.9489 21.9665 22.9475C22.0933 22.8666 22.2243 22.7914 22.3581 22.7203C22.3581 22.7203 22.3595 22.7203 22.3595 22.7189C23.3029 22.2173 24.3787 21.933 25.5214 21.933C29.2421 21.933 32.2591 24.9486 32.2591 28.6707Z"></path><path d="M25.5214 14.0692C29.2421 14.0692 32.2591 11.0522 32.2591 7.33146C32.2591 3.61074 29.2421 0.59375 25.5214 0.59375C22.0529 0.59375 19.1962 3.21637 18.8255 6.58592C18.8185 6.67092 18.8116 6.75454 18.8018 6.83815C18.7893 7.00119 18.7837 7.16563 18.7837 7.33146C18.7837 9.73669 20.0434 11.8465 21.94 13.038C22.0891 13.116 22.2355 13.201 22.3776 13.2916C22.3783 13.2923 22.379 13.2926 22.3797 13.293C22.3804 13.2933 22.3811 13.2937 22.3818 13.2944C23.3196 13.7891 24.3871 14.0692 25.5214 14.0692Z"></path><path d="M19.3645 12.4113C20.2926 12.4113 21.1694 12.638 21.94 13.038C20.0434 11.8465 18.7837 9.73669 18.7837 7.33146C18.7837 7.16563 18.7893 7.00119 18.8018 6.83815C18.4688 9.76455 16.1385 12.0866 13.2065 12.4044C13.8545 13.1193 14.3785 13.9484 14.745 14.857C15.7497 13.3798 17.4443 12.4113 19.3645 12.4113Z"></path><path d="M14.7312 21.1249V21.1236C14.1279 20.2331 13.7767 19.1587 13.7767 18.0007V17.9728C13.7767 15.3084 12.2285 13.0035 9.9835 11.911C9.98141 11.9103 9.97967 11.9096 9.97793 11.9089C9.97619 11.9082 9.97444 11.9075 9.97235 11.9068C9.96817 11.904 9.96538 11.9026 9.9612 11.9012C9.95981 11.9012 9.95981 11.8998 9.95981 11.8998C9.9417 11.8915 9.92394 11.8831 9.90618 11.8747C9.8884 11.8664 9.87063 11.858 9.85251 11.8497C9.82046 11.8343 9.78701 11.819 9.75357 11.8051L9.74521 11.8009C8.91745 11.4372 8.0019 11.2351 7.03898 11.2351C3.31826 11.2351 0.30127 14.2521 0.30127 17.9728C0.30127 21.6935 3.31826 24.7105 7.03898 24.7105C7.98797 24.7105 8.89098 24.514 9.71037 24.1601C9.71246 24.1594 9.7142 24.1583 9.71594 24.1573C9.71768 24.1562 9.71943 24.1552 9.72152 24.1545C9.8107 24.1169 9.8985 24.0765 9.9849 24.0333L9.98629 24.0319C10.7625 23.6919 11.6181 23.5037 12.5197 23.5037C12.7524 23.5037 12.9824 23.5163 13.2081 23.54C13.2082 23.5399 13.2081 23.54 13.2081 23.54C15.0168 23.7365 16.5971 24.695 17.6185 26.0885C17.9195 25.1688 18.3752 24.3201 18.9563 23.5732C17.1964 23.4464 15.6635 22.5058 14.7312 21.1249Z"></path><g clip-path="url(#clip0)"><path d="M106.391 18.0021C106.391 11.3724 101.039 6 94.4389 6H88.4585C81.8581 6 76.5061 11.3724 76.5061 18.0021V30.0042H81.2845V18.0021C81.2845 14.0268 84.4941 10.8008 88.4585 10.8008H94.4347C98.395 10.8008 101.609 14.0226 101.609 18.0021V30.0042H106.391V18.0021Z"></path><path d="M149.432 6H142.258C135.653 6 130.301 11.3724 130.301 18.0021C130.301 24.6319 135.653 30.0042 142.258 30.0042H149.432C156.036 30.0042 161.388 24.6319 161.388 18.0021C161.388 11.3724 156.032 6 149.432 6ZM149.432 25.1992H142.258C138.297 25.1992 135.084 21.9774 135.084 17.9979C135.084 14.0183 138.293 10.7966 142.258 10.7966H149.432C153.392 10.7966 156.606 14.0183 156.606 17.9979C156.606 21.9774 153.392 25.1992 149.432 25.1992Z"></path><path d="M74.1151 25.1992H58.5736C55.4526 25.1992 52.804 23.1924 51.8171 20.3983H74.1151V17.9979C74.1151 17.1808 74.1868 16.3807 74.3175 15.5975H51.8171C52.804 12.8033 55.4526 10.7966 58.5736 10.7966H76.0383C77.1475 8.87458 78.6911 7.22773 80.5299 6H58.5736C51.969 6 46.6169 11.3724 46.6169 18.0021C46.6169 24.6276 51.969 30 58.5736 30H74.1151V25.1992Z"></path><path d="M120.74 6H115.958H102.369C104.212 7.22773 105.751 8.87458 106.861 10.8008H115.958V30H120.74V10.8008H129.838C130.947 8.87458 132.486 7.22773 134.329 6H120.74Z"></path><path d="M182.906 15.6017H169.756C168.436 15.6017 167.365 14.5264 167.365 13.2013C167.365 11.8762 168.436 10.8008 169.756 10.8008H188.882V6H169.756C165.796 6 162.582 9.22173 162.582 13.2013C162.582 17.1808 165.791 20.4025 169.756 20.4025H182.906C184.226 20.4025 185.297 21.4779 185.297 22.803C185.297 24.1281 184.226 25.2034 182.906 25.2034H161.852C160.743 27.1297 159.199 28.7765 157.361 30.0042H182.906C186.866 30.0042 190.08 26.7825 190.08 22.803C190.08 18.8234 186.866 15.6017 182.906 15.6017Z"></path></g><defs><clipPath id="clip0"><rect width="190" height="24" fill="white" transform="translate(0 6)"></rect></clipPath></defs></svg>
|
||||
<div>
|
||||
Entos Envision
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { mountControls } from './controls';
|
||||
import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
import { setDebugMode, setTimingMode, consoleStats } from '../../mol-util/debug';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MAPairwiseScorePlot } from '../../extensions/model-archive/quality-asse
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { Model, ResidueIndex } from '../../mol-model/structure';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
export class AlphaFoldPAEExample {
|
||||
viewer: Viewer;
|
||||
@@ -46,7 +46,7 @@ export class AlphaFoldPAEExample {
|
||||
const model = this.viewer.plugin.managers.structure.hierarchy.current.models[0]?.cell.obj?.data!;
|
||||
const metric = pairwiseMetricFromAlphaFoldDbJson(model, json)!;
|
||||
|
||||
createRoot(document.getElementById(this.plotContainerId)!).render(
|
||||
plotRoot.render(
|
||||
<div className='msp-plugin' style={{ background: 'white' }}>
|
||||
<MAPairwiseScorePlot plugin={this.viewer.plugin} pairwiseMetric={metric} model={model} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isPositionLocation } from '../../mol-geo/util/location-iterator';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { ColorThemeCategory } from '../../mol-theme/color/categories';
|
||||
import { ThemeDataContext } from '../../mol-theme/theme';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
@@ -43,7 +44,7 @@ export function CustomColorTheme(
|
||||
export const CustomColorThemeProvider: ColorTheme.Provider<{}, 'basic-wrapper-custom-color-theme'> = {
|
||||
name: 'basic-wrapper-custom-color-theme',
|
||||
label: 'Custom Color Theme',
|
||||
category: ColorTheme.Category.Misc,
|
||||
category: ColorThemeCategory.Misc,
|
||||
factory: CustomColorTheme,
|
||||
getParams: () => ({}),
|
||||
defaultValues: { },
|
||||
|
||||
@@ -22,7 +22,7 @@ import { CustomToastMessage } from './controls';
|
||||
import { CustomColorThemeProvider } from './custom-theme';
|
||||
import './index.html';
|
||||
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData } from './superposition';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
|
||||
|
||||
|
||||
49
src/examples/ihm-restraints/index.html
Normal file
49
src/examples/ihm-restraints/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* IHM Restraints Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
#links {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.8rem;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="viewer"></div>
|
||||
<div id="links">
|
||||
<a href="#" id="mvs-data" filename="ihm-restraints.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/ihm-restraints" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
<script>
|
||||
loadIHMRestraints(document.getElementById('viewer')).then(state => {
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const data = JSON.stringify(state, null, 2);
|
||||
molStarDownload(new Blob([data], { type: 'application/json' }), 'ihm-restraints.mvsj');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
366
src/examples/ihm-restraints/index.tsx
Normal file
366
src/examples/ihm-restraints/index.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { loadMVS } from '../../extensions/mvs/load';
|
||||
import { MVSData_States, Snapshot } from '../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder } from '../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { parseCifText } from '../../mol-io/reader/cif/text/parser';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
|
||||
import { Model } from '../../mol-model/structure';
|
||||
import { CoarseElementKey, CoarseElementReference } from '../../mol-model/structure/model/properties/coarse';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { renderReact18 } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ajaxGet } from '../../mol-util/data-source';
|
||||
import { download } from '../../mol-util/download';
|
||||
import './index.html';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
async function createViewer(root: HTMLElement) {
|
||||
const spec = DefaultPluginUISpec();
|
||||
const plugin = await createPluginUI({
|
||||
target: root,
|
||||
render: renderReact18,
|
||||
spec: {
|
||||
...spec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: true,
|
||||
showControls: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
remoteState: 'none',
|
||||
},
|
||||
behaviors: [
|
||||
...spec.behaviors,
|
||||
PluginSpec.Behavior(MolViewSpec)
|
||||
],
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
[PluginConfig.Viewport.ShowTrajectoryControls, false],
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
interface IHMRestraintInfo {
|
||||
e1: CoarseElementKey & { label_comp_id: string },
|
||||
e2: CoarseElementKey & { label_comp_id: string },
|
||||
a: Vec3,
|
||||
b: Vec3,
|
||||
restraintType: 'harmonic' | 'upper bound' | 'lower bound',
|
||||
threshold: number,
|
||||
satisfied: boolean,
|
||||
distance: number,
|
||||
}
|
||||
|
||||
interface IHMStructureInfo {
|
||||
entity_labels: [id: string | undefined, label: string | undefined][],
|
||||
model_restraints: IHMRestraintInfo[][],
|
||||
}
|
||||
|
||||
function getCoarseElementPosition(e: CoarseElementReference, model: Model, position: Vec3) {
|
||||
if (!e.kind) Vec3.set(position, 0, 0, 0);
|
||||
const { x, y, z } = model.coarseConformation[e.kind!];
|
||||
const idx = e.index;
|
||||
Vec3.set(position, x[idx], y[idx], z[idx]);
|
||||
}
|
||||
|
||||
const _elementRef = CoarseElementReference();
|
||||
function resolvePosition(model: Model, key: CoarseElementKey, position: Vec3) {
|
||||
if (model.coarseHierarchy.index.findElement(key, _elementRef)) {
|
||||
getCoarseElementPosition(_elementRef, model, position);
|
||||
return true;
|
||||
}
|
||||
|
||||
const rI = model.atomicHierarchy.index.findResidueLabel(key);
|
||||
if (rI < 0) return false;
|
||||
|
||||
const atomStart = model.atomicHierarchy.residueAtomSegments.offsets[rI];
|
||||
const atomEnd = model.atomicHierarchy.residueAtomSegments.offsets[rI + 1];
|
||||
const atomId = model.atomicHierarchy.atoms.label_atom_id;
|
||||
let aI = atomStart;
|
||||
// Find CA otherwise use the first atom.
|
||||
// Possible future improvement: use the atom closest to the center of mass of the residue.
|
||||
for (; aI < atomEnd; aI++) {
|
||||
if (atomId.value(aI) === 'CA') break;
|
||||
}
|
||||
if (aI === atomEnd) aI = atomStart;
|
||||
|
||||
const { x, y, z } = model.atomicConformation;
|
||||
Vec3.set(position, x[aI], y[aI], z[aI]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const HarmonicRestraintTolerance = 0.1;
|
||||
|
||||
async function parseInfo(plugin: PluginContext, url: string): Promise<IHMStructureInfo> {
|
||||
const data = await plugin.runTask(ajaxGet(url)) as string;
|
||||
const parsed = await plugin.runTask(parseCifText(data));
|
||||
|
||||
if (parsed.isError) {
|
||||
console.error(parsed);
|
||||
return { entity_labels: [], model_restraints: [] };
|
||||
}
|
||||
|
||||
const trajectory = await plugin.runTask(trajectoryFromMmCIF(parsed.result.blocks[0], parsed.result));
|
||||
|
||||
const dataBlocks = parsed.result.blocks;
|
||||
|
||||
const ihm_cross_link_restraint = dataBlocks[0].categories['ihm_cross_link_restraint'];
|
||||
const entity_id_1 = ihm_cross_link_restraint.getField('entity_id_1')!;
|
||||
const asym_id_1 = ihm_cross_link_restraint.getField('asym_id_1')!;
|
||||
const seq_id_1 = ihm_cross_link_restraint.getField('seq_id_1')!;
|
||||
const comp_id_1 = ihm_cross_link_restraint.getField('comp_id_1')!;
|
||||
const entity_id_2 = ihm_cross_link_restraint.getField('entity_id_2')!;
|
||||
const asym_id_2 = ihm_cross_link_restraint.getField('asym_id_2')!;
|
||||
const seq_id_2 = ihm_cross_link_restraint.getField('seq_id_2')!;
|
||||
const comp_id_2 = ihm_cross_link_restraint.getField('comp_id_2')!;
|
||||
const restraint_type = ihm_cross_link_restraint.getField('restraint_type')!;
|
||||
const threshold = ihm_cross_link_restraint.getField('distance_threshold')!;
|
||||
|
||||
const e1key = CoarseElementKey();
|
||||
const e2key = CoarseElementKey();
|
||||
|
||||
const a = Vec3.zero();
|
||||
const b = Vec3.zero();
|
||||
|
||||
const entity_labels: IHMStructureInfo['entity_labels'] = [];
|
||||
const entity = dataBlocks[0].categories['entity'];
|
||||
const entity_id = entity.getField('id');
|
||||
const pdbx_description = entity.getField('pdbx_description');
|
||||
for (let i = 0; i < entity.rowCount; i++) {
|
||||
entity_labels.push([entity_id?.str(i), pdbx_description?.str(i)]);
|
||||
}
|
||||
|
||||
const model_restraints: IHMRestraintInfo[][] = [];
|
||||
|
||||
for (let modelIndex = 0; modelIndex < trajectory.frameCount; modelIndex++) {
|
||||
const _model = trajectory.getFrameAtIndex(modelIndex);
|
||||
const model = Task.is(_model) ? await plugin.runTask(_model) : _model;
|
||||
|
||||
const restraints: IHMRestraintInfo[] = [];
|
||||
model_restraints.push(restraints);
|
||||
|
||||
for (let i = 0; i < ihm_cross_link_restraint.rowCount; i++) {
|
||||
e1key.label_entity_id = entity_id_1.str(i);
|
||||
e1key.label_asym_id = asym_id_1.str(i);
|
||||
e1key.label_seq_id = seq_id_1.int(i);
|
||||
e2key.label_entity_id = entity_id_2.str(i);
|
||||
e2key.label_asym_id = asym_id_2.str(i);
|
||||
e2key.label_seq_id = seq_id_2.int(i);
|
||||
|
||||
if (!resolvePosition(model, e1key, a) || !resolvePosition(model, e2key, b)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const restraintType: 'harmonic' | 'upper bound' | 'lower bound' = restraint_type.str(i)?.toLowerCase() as any;
|
||||
const thresholdValue = threshold.float(i);
|
||||
const distance = Vec3.distance(a, b);
|
||||
|
||||
let satisfied = true;
|
||||
if (restraintType === 'harmonic') {
|
||||
const thresholdValue = threshold.float(i);
|
||||
satisfied = distance >= (1 - HarmonicRestraintTolerance) * thresholdValue && distance <= (1 + HarmonicRestraintTolerance) * thresholdValue;
|
||||
} else if (restraintType === 'upper bound') {
|
||||
satisfied = distance <= thresholdValue;
|
||||
} else if (restraintType === 'lower bound') {
|
||||
satisfied = distance >= thresholdValue;
|
||||
}
|
||||
|
||||
restraints.push({
|
||||
e1: { ...e1key, label_comp_id: comp_id_1.str(i) },
|
||||
e2: { ...e2key, label_comp_id: comp_id_2.str(i) },
|
||||
a: Vec3.clone(a),
|
||||
b: Vec3.clone(b),
|
||||
restraintType,
|
||||
threshold: thresholdValue,
|
||||
satisfied,
|
||||
distance,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { entity_labels, model_restraints };
|
||||
}
|
||||
|
||||
function baseStructure(url: string, modelIndex: number, info: IHMStructureInfo, options?: { noEntityLabels?: boolean }) {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const structure = builder
|
||||
.download({ url })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure({ model_index: modelIndex });
|
||||
|
||||
structure
|
||||
.component({ selector: 'coarse' })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ custom: { molstar_use_default_coloring: true } })
|
||||
.opacity({ opacity: 0.51 });
|
||||
|
||||
structure
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'cartoon' })
|
||||
.color({ custom: { molstar_use_default_coloring: true } })
|
||||
.opacity({ opacity: 0.51 });
|
||||
|
||||
if (!options?.noEntityLabels) {
|
||||
const primitives = structure.primitives();
|
||||
for (const [label_entity_id, text] of info.entity_labels) {
|
||||
if (!text) continue;
|
||||
primitives
|
||||
.label({ position: { label_entity_id }, text, label_size: 16, label_color: '#cccccc' });
|
||||
}
|
||||
}
|
||||
|
||||
return [builder, structure] as const;
|
||||
}
|
||||
|
||||
function drawConstraints([, structure]: ReturnType<typeof baseStructure>, restraints: IHMRestraintInfo[], options: {
|
||||
filter: (r: IHMRestraintInfo) => boolean,
|
||||
color: (r: IHMRestraintInfo) => any,
|
||||
radius?: (r: IHMRestraintInfo) => number,
|
||||
tooltip: (r: IHMRestraintInfo) => string | undefined,
|
||||
}) {
|
||||
const primitives = structure.primitives();
|
||||
for (const r of restraints) {
|
||||
if (!options.filter(r)) continue;
|
||||
|
||||
const radius = options.radius?.(r) ?? 1;
|
||||
|
||||
primitives.tube({
|
||||
start: r.a as any,
|
||||
end: r.b as any,
|
||||
color: options.color(r) || 'white',
|
||||
tooltip: options.tooltip(r),
|
||||
radius: radius,
|
||||
dash_length: radius,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function restraintTooltip(r: IHMRestraintInfo) {
|
||||
return `
|
||||
- Element 1: ${r.e1.label_entity_id} ${r.e1.label_asym_id} ${r.e1.label_seq_id} ${r.e1.label_comp_id}
|
||||
- Element 2: ${r.e2.label_entity_id} ${r.e2.label_asym_id} ${r.e2.label_seq_id} ${r.e2.label_comp_id}
|
||||
- Distance: ${r.distance.toFixed(2)} Å
|
||||
- Threshold: ${r.threshold.toFixed(2)} Å
|
||||
- Constraint: ${r.restraintType}
|
||||
- Satisfied: ${r.satisfied ? 'Yes' : 'No'}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function loadIHMRestraints(root: HTMLElement, url?: string) {
|
||||
url ??= 'https://pdb-ihm.org/cif/8zz1.cif';
|
||||
|
||||
const plugin = await createViewer(root);
|
||||
const info = await parseInfo(plugin, url);
|
||||
|
||||
const modelIndex = 0;
|
||||
const restraints = info.model_restraints[modelIndex];
|
||||
|
||||
const nVialoted = restraints.filter(r => !r.satisfied).length;
|
||||
const nSatisfied = restraints.length - nVialoted;
|
||||
|
||||
const snapshots: Snapshot[] = [];
|
||||
|
||||
let mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => true,
|
||||
color: r => r.e1.label_entity_id === r.e2.label_entity_id && r.e1.label_asym_id === r.e2.label_asym_id ? 'yellow' : 'blue',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'All Restraints',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### All Restraints
|
||||
|
||||
- Yellow: Intra-chain restraints
|
||||
- Blue: Inter-chain restraints
|
||||
`,
|
||||
}));
|
||||
|
||||
mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => true,
|
||||
color: r => r.satisfied ? 'green' : 'red',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'Restraint Validation',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### Restraint Validation
|
||||
|
||||
- Red: ${nVialoted} Violated restraints
|
||||
- Green: ${nSatisfied} Satisfied restraints
|
||||
`,
|
||||
}));
|
||||
|
||||
mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => !r.satisfied,
|
||||
color: r => r.satisfied ? 'green' : 'red',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'Violated Restraints',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### Violated Restraints
|
||||
|
||||
${nVialoted} restraints are violated.
|
||||
`,
|
||||
}));
|
||||
|
||||
mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => r.satisfied,
|
||||
color: r => r.satisfied ? 'green' : 'red',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'Satisfied Restraints',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### Satisfied Restraints
|
||||
|
||||
${nSatisfied} restraints are satisfied.
|
||||
`,
|
||||
}));
|
||||
|
||||
const data: MVSData_States = {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'I/HM Restraints',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
|
||||
await loadMVS(plugin, data, { sanityChecks: true, replaceExisting: true, keepSnapshotCamera: true });
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
(window as any).loadIHMRestraints = loadIHMRestraints;
|
||||
(window as any).molStarDownload = download;
|
||||
35
src/examples/ihm-restraints/readme.md
Normal file
35
src/examples/ihm-restraints/readme.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# I/HM Restraints Example
|
||||
|
||||
This example illustrates:
|
||||
|
||||
- Using Mol* to parse CIF files and extract custom data
|
||||
- Using MolViewSpec to visualize I/HM structure along with annotated crosslink restraints
|
||||
|
||||
### Usage
|
||||
|
||||
- Clone Mol* GitHub repo and build it.
|
||||
```bash
|
||||
git clone https://github.com/molstar/molstar.git
|
||||
cd molstar
|
||||
npm install
|
||||
npm build
|
||||
```
|
||||
|
||||
- Get `molstar.css` and `index.js` from `build/examples/ihm-restraints` and include these to your HTML page in a similar fashion to [index.html](./index.html):
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
...
|
||||
<div id="viewer"></div>
|
||||
...
|
||||
<script>
|
||||
loadIHMRestraints(document.getElementById('viewer'))
|
||||
</script>
|
||||
```
|
||||
|
||||
- For interactive development build (for production use `npm run build`) of the example that immediately reflects changes use:
|
||||
|
||||
```bash
|
||||
npm run dev -- -e ihm-restraints
|
||||
```
|
||||
46
src/examples/interactions/index.html
Normal file
46
src/examples/interactions/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* Interactions Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
}
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 40%;
|
||||
bottom: 8px;
|
||||
font-family: sans-serif;
|
||||
padding: 8px;
|
||||
border: 1px dotted #aaa;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
#controls select {
|
||||
margin-left: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="viewer"></div>
|
||||
<div id="controls">
|
||||
</div>
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
<script>
|
||||
initInteractionsExample('viewer', 'controls');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
383
src/examples/interactions/index.tsx
Normal file
383
src/examples/interactions/index.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { InteractionElementSchema, InteractionKind, StructureInteractionElement, StructureInteractions } from '../../extensions/interactions/model';
|
||||
import { ComputeContacts, CustomInteractions, InteractionsShape } from '../../extensions/interactions/transforms';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { ResidueIndex, Structure, StructureElement, StructureProperties, StructureQuery } from '../../mol-model/structure';
|
||||
import { atoms } from '../../mol-model/structure/query/queries/generators';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { MultiStructureSelectionFromBundle, StructureSelectionFromBundle } from '../../mol-plugin-state/transforms/model';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { renderReact18 } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
import './index.html';
|
||||
import { Task } from '../../mol-task';
|
||||
import { computeContacts } from '../../extensions/interactions/compute';
|
||||
|
||||
async function createViewer(root: HTMLElement) {
|
||||
const spec = DefaultPluginUISpec();
|
||||
const plugin = await createPluginUI({
|
||||
target: root,
|
||||
render: renderReact18,
|
||||
spec: {
|
||||
...spec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: true,
|
||||
showControls: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
remoteState: 'none',
|
||||
},
|
||||
behaviors: [
|
||||
...spec.behaviors,
|
||||
PluginSpec.Behavior(MolViewSpec)
|
||||
],
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
async function createBindingSiteRepresentation(plugin: PluginContext, interactions: StructureInteractions[], receptors: Map<string, Structure>) {
|
||||
const contactBundles = getBindingSiteBundles(interactions.flatMap(e => e.elements), receptors);
|
||||
const update = plugin.build();
|
||||
|
||||
for (const [ref, bundle] of contactBundles) {
|
||||
update.to(ref)
|
||||
.apply(StructureSelectionFromBundle, { bundle, label: 'Binding Site' })
|
||||
.apply(StructureRepresentation3D, {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.2 } },
|
||||
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
function getBindingSiteBundles(interactions: StructureInteractionElement[], receptors: Map<string, Structure>) {
|
||||
const residueIndices = new Map<string, Set<ResidueIndex>>();
|
||||
|
||||
const loc = StructureElement.Location.create();
|
||||
const add = (ref: string, loci: StructureElement.Loci) => {
|
||||
if (!receptors.has(ref)) return;
|
||||
|
||||
let set: Set<ResidueIndex>;
|
||||
if (residueIndices.has(ref)) {
|
||||
set = residueIndices.get(ref)!;
|
||||
} else {
|
||||
set = new Set<ResidueIndex>();
|
||||
residueIndices.set(ref, set);
|
||||
}
|
||||
StructureElement.Loci.forEachLocation(loci, l => {
|
||||
set.add(StructureProperties.residue.key(l));
|
||||
}, loc);
|
||||
};
|
||||
|
||||
for (const e of interactions) {
|
||||
add(e.aStructureRef!, e.a);
|
||||
add(e.bStructureRef!, e.b);
|
||||
}
|
||||
|
||||
const bundles: [ref: string, bundle: StructureElement.Bundle][] = [];
|
||||
|
||||
for (const [ref, indices] of Array.from(residueIndices.entries())) {
|
||||
if (indices.size === 0) continue;
|
||||
|
||||
const loci = StructureQuery.loci(
|
||||
atoms({
|
||||
residueTest: e => indices.has(StructureProperties.residue.key(e.element))
|
||||
}),
|
||||
receptors.get(ref)!,
|
||||
);
|
||||
if (StructureElement.Loci.isEmpty(loci)) continue;
|
||||
bundles.push([ref, StructureElement.Bundle.fromLoci(loci)]);
|
||||
}
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
async function loadComputedExample(
|
||||
plugin: PluginContext,
|
||||
{ receptorUrl, ligandUrl }: { receptorUrl: [url: string, format: BuiltInTrajectoryFormat], ligandUrl: [url: string, format: BuiltInTrajectoryFormat] },
|
||||
options: { receptor_label_asym_id: string | undefined, analyzeTrajectory?: boolean }
|
||||
) {
|
||||
await plugin.clear();
|
||||
|
||||
// Set up the receptor and ligand structures
|
||||
const receptorData = await plugin.builders.data.download({ url: receptorUrl[0] });
|
||||
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, receptorUrl[1]);
|
||||
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default', { representationPreset: 'polymer-cartoon' });
|
||||
|
||||
const ligandData = await plugin.builders.data.download({ url: ligandUrl[0] });
|
||||
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, ligandUrl[1]);
|
||||
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
|
||||
|
||||
// Compute the interactions
|
||||
const update = plugin.build();
|
||||
|
||||
const receptorRef = receptor?.structure.ref!;
|
||||
const ligandRef = ligand?.structure.ref!;
|
||||
|
||||
const refs = [receptorRef, ligandRef];
|
||||
const interactionsRef = update.toRoot()
|
||||
.apply(MultiStructureSelectionFromBundle, {
|
||||
selections: [
|
||||
{ key: 'a', ref: receptorRef, bundle: StructureElement.Schema.toBundle(receptor?.structure.data!, { label_asym_id: options.receptor_label_asym_id }) },
|
||||
{ key: 'b', ref: ligandRef, bundle: StructureElement.Schema.toBundle(ligand?.structure.data!, { }) },
|
||||
],
|
||||
isTransitive: true,
|
||||
label: 'Label'
|
||||
}, { dependsOn: refs })
|
||||
.apply(ComputeContacts);
|
||||
|
||||
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
|
||||
if (!options.analyzeTrajectory) {
|
||||
console.log('Interactions', interactionsRef.selector.data?.interactions);
|
||||
|
||||
// Create ball and stick representations for the binding site and focus on the ligand
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
[interactionsRef.selector.data?.interactions!],
|
||||
new Map([[receptorRef, receptor?.structure.data!]])
|
||||
);
|
||||
} else {
|
||||
const trajectoryInteractions: StructureInteractions[] = [];
|
||||
const receptorLoci = StructureElement.Schema.toLoci(receptor?.structure.data!, { label_asym_id: options.receptor_label_asym_id });
|
||||
for (let fI = 0; fI < ligandTrajectory.data!.frameCount; fI++) {
|
||||
const model = await Task.resolveInContext(ligandTrajectory.data!.getFrameAtIndex(fI));
|
||||
const structure = Structure.ofModel(model);
|
||||
const currentInteractions = await plugin.runTask(Task.create('Compute Contacts', ctx => {
|
||||
return computeContacts(ctx, [
|
||||
{ structureRef: receptorRef, loci: receptorLoci },
|
||||
{ structureRef: ligandRef, loci: Structure.toStructureElementLoci(structure) },
|
||||
]);
|
||||
}));
|
||||
trajectoryInteractions.push(currentInteractions);
|
||||
}
|
||||
|
||||
console.log('Interactions', trajectoryInteractions);
|
||||
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
trajectoryInteractions,
|
||||
new Map([[receptorRef, receptor?.structure.data!]])
|
||||
);
|
||||
}
|
||||
|
||||
PluginCommands.Camera.FocusObject(plugin, {
|
||||
targets: [{
|
||||
targetRef: ligand?.representation.representations.all.ref
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCustomExample(plugin: PluginContext) {
|
||||
await plugin.clear();
|
||||
|
||||
// Set up the receptor and ligand structures
|
||||
const receptorData = await plugin.builders.data.download({ url: '../../../examples/ace2.pdbqt' });
|
||||
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, 'pdbqt');
|
||||
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default');
|
||||
|
||||
const ligandData = await plugin.builders.data.download({ url: '../../../examples/ace2-hit.mol2' });
|
||||
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, 'mol2');
|
||||
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
|
||||
|
||||
// Compute the interactions
|
||||
const update = plugin.build();
|
||||
|
||||
const receptorRef = receptor?.representation.components.polymer.ref!;
|
||||
const ligandRef = ligand?.representation.components.all.ref!;
|
||||
|
||||
const refs = [receptorRef, ligandRef];
|
||||
|
||||
const interactionsRef = update.toRoot().apply(CustomInteractions, {
|
||||
interactions: [
|
||||
{
|
||||
kind: 'hydrogen-bond',
|
||||
aStructureRef: receptorRef,
|
||||
a: { auth_seq_id: 353, auth_atom_id: 'N' },
|
||||
bStructureRef: ligandRef,
|
||||
b: { atom_index: 9 },
|
||||
}
|
||||
]
|
||||
}, { dependsOn: refs });
|
||||
|
||||
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
|
||||
console.log('Interactions', interactionsRef.selector.data?.interactions);
|
||||
|
||||
// Create ball and stick representations for the binding site and focus on the ligand
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
[interactionsRef.selector.data?.interactions!],
|
||||
new Map([[receptorRef, receptor?.representation.components.polymer.data]])
|
||||
);
|
||||
PluginCommands.Camera.FocusObject(plugin, {
|
||||
targets: [{
|
||||
targetRef: ligand?.representation.representations.all.ref
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTestAllExample(plugin: PluginContext) {
|
||||
await plugin.clear();
|
||||
|
||||
// Set up the receptor and ligand structures
|
||||
const receptorData = await plugin.builders.data.download({ url: '../../../examples/ace2.pdbqt' });
|
||||
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, 'pdbqt');
|
||||
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default');
|
||||
|
||||
const ligandData = await plugin.builders.data.download({ url: '../../../examples/ace2-hit.mol2' });
|
||||
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, 'mol2');
|
||||
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
|
||||
|
||||
// Compute the interactions
|
||||
const update = plugin.build();
|
||||
|
||||
const receptorRef = receptor?.representation.components.polymer.ref!;
|
||||
const ligandRef = ligand?.representation.components.all.ref!;
|
||||
|
||||
const refs = [receptorRef, ligandRef];
|
||||
|
||||
const basic = (kind: InteractionKind, atom_index: number | number[], description?: string): InteractionElementSchema => {
|
||||
return {
|
||||
kind,
|
||||
aStructureRef: receptorRef,
|
||||
a: { auth_seq_id: 354, auth_atom_id: 'N' },
|
||||
bStructureRef: ligandRef,
|
||||
b: Array.isArray(atom_index) ? { items: { atom_index } } : { atom_index },
|
||||
description,
|
||||
};
|
||||
};
|
||||
|
||||
const covalent = (degree: number, atom_index: number): InteractionElementSchema => {
|
||||
return {
|
||||
kind: 'covalent',
|
||||
degree: degree === -1 ? 'aromatic' : Math.abs(degree) as 1 | 2 | 3 | 4,
|
||||
aStructureRef: receptorRef,
|
||||
a: { auth_seq_id: 354, auth_atom_id: 'N' },
|
||||
bStructureRef: ligandRef,
|
||||
b: { atom_index }
|
||||
};
|
||||
};
|
||||
|
||||
const interactionsRef = update.toRoot().apply(CustomInteractions, {
|
||||
interactions: [
|
||||
basic('unknown', 1),
|
||||
basic('ionic', 2),
|
||||
basic('pi-stacking', 3),
|
||||
basic('cation-pi', 4),
|
||||
basic('halogen-bond', 5),
|
||||
basic('hydrogen-bond', 6),
|
||||
basic('weak-hydrogen-bond', 7),
|
||||
basic('hydrophobic', 8),
|
||||
basic('metal-coordination', 9),
|
||||
basic('salt-bridge', 10),
|
||||
covalent(1, 11),
|
||||
covalent(2, 12),
|
||||
covalent(3, 13),
|
||||
covalent(-1, 14), // aromatic
|
||||
basic('unknown', [0, 1, 2, 3, 13, 14], 'Testing centroid for atom set'),
|
||||
]
|
||||
}, { dependsOn: refs });
|
||||
|
||||
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
|
||||
console.log('Interactions', interactionsRef.selector.data?.interactions);
|
||||
|
||||
// Create ball and stick representations for the binding site and focus on the ligand
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
[interactionsRef.selector.data?.interactions!],
|
||||
new Map([[receptorRef, receptor?.representation.components.polymer.data]])
|
||||
);
|
||||
PluginCommands.Camera.FocusObject(plugin, {
|
||||
targets: [{
|
||||
targetRef: ligand?.representation.representations.all.ref
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
const Examples = {
|
||||
'Computed (1iep)': (plugin: PluginContext) => loadComputedExample(plugin, {
|
||||
receptorUrl: ['https://files.rcsb.org/download/1IEP.cif', 'mmcif'],
|
||||
ligandUrl: ['https://models.rcsb.org/v1/1iep/atoms?label_asym_id=G©_all_categories=false', 'mmcif']
|
||||
}, { receptor_label_asym_id: 'A' }),
|
||||
'Computed (ACE2)': (plugin: PluginContext) => loadComputedExample(plugin, {
|
||||
receptorUrl: ['../../../examples/ace2.pdbqt', 'pdbqt'],
|
||||
ligandUrl: ['../../../examples/ace2-hit.mol2', 'mol2']
|
||||
}, { receptor_label_asym_id: 'B' }),
|
||||
'Computed (multiple)': (plugin: PluginContext) => loadComputedExample(plugin, {
|
||||
receptorUrl: ['../../../examples/docking/receptor_1.pdb', 'pdb'],
|
||||
ligandUrl: ['../../../examples/docking/ligands_1.sdf', 'sdf']
|
||||
}, { receptor_label_asym_id: undefined, analyzeTrajectory: true }),
|
||||
'Custom': loadCustomExample,
|
||||
'Synthetic': loadTestAllExample
|
||||
};
|
||||
|
||||
function SelectExampleUI({ state, load }: {
|
||||
state: BehaviorSubject<{ name?: keyof typeof Examples, isLoading?: boolean }>,
|
||||
load: (name: keyof typeof Examples) => void
|
||||
}) {
|
||||
const current = useBehavior(state);
|
||||
return <div>
|
||||
Select Example:{' '}
|
||||
<select value={current.name} onChange={e => load(e.target.value as any)} disabled={current.isLoading}>
|
||||
{Object.keys(Examples).map(k => <option key={k} value={k}>{k}</option>)}
|
||||
</select>
|
||||
</div>;
|
||||
}
|
||||
|
||||
async function init(viewer: HTMLElement | string, controls: HTMLElement | string, defaultExample: keyof typeof Examples = 'Computed (1iep)') {
|
||||
const root = typeof viewer === 'string' ? document.getElementById('viewer')! : viewer;
|
||||
const plugin = await createViewer(root);
|
||||
|
||||
const state = new BehaviorSubject<{ name?: keyof typeof Examples, isLoading?: boolean }>({});
|
||||
const loadExample = async (name: keyof typeof Examples) => {
|
||||
state.next({ name, isLoading: true });
|
||||
try {
|
||||
await Examples[name](plugin);
|
||||
state.next({ name });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
state.next({});
|
||||
}
|
||||
};
|
||||
|
||||
createRoot(
|
||||
typeof controls === 'string' ? document.getElementById('controls')! : controls
|
||||
).render(<SelectExampleUI state={state} load={loadExample} />);
|
||||
|
||||
loadExample(defaultExample);
|
||||
return { plugin, loadExample };
|
||||
}
|
||||
|
||||
|
||||
(window as any).initInteractionsExample = init;
|
||||
16
src/examples/interactions/readme.md
Normal file
16
src/examples/interactions/readme.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Interactions Example
|
||||
|
||||
An example showcasing features of the `interactions` extension, including:
|
||||
|
||||
- Computing interactions on demand using Mol*'s built-in interaction computation code
|
||||
- Triggering interaction computation separately for multiple ligands stored in a single SDF file
|
||||
- Specifying interaction manually from custom data source
|
||||
- Synthetic example showing all supported interaction types
|
||||
|
||||
To run development build locally from the root `molstar` directory (after `npm install`):
|
||||
|
||||
```bash
|
||||
npm run dev -- -e interactions
|
||||
```
|
||||
|
||||
and navigate to `build/examples/interactions` in the hosted server linked in the script output.
|
||||
@@ -14,7 +14,7 @@ import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
|
||||
|
||||
|
||||
37
src/examples/mvs-stories/context.ts
Normal file
37
src/examples/mvs-stories/context.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import type { MolComponentViewerModel } from './elements/viewer';
|
||||
|
||||
export type MolComponentCommand =
|
||||
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData }
|
||||
|
||||
|
||||
export class MolComponentContext {
|
||||
commands = new BehaviorSubject<MolComponentCommand | undefined>(undefined);
|
||||
behavior = {
|
||||
viewers: new BehaviorSubject<{ name?: string, model: MolComponentViewerModel }[]>([]),
|
||||
};
|
||||
|
||||
dispatch(command: MolComponentCommand) {
|
||||
this.commands.next(command);
|
||||
}
|
||||
|
||||
constructor(public name?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMolComponentContext(options?: { name?: string, container?: object }) {
|
||||
const container: any = options?.container ?? window;
|
||||
container.componentContexts ??= {};
|
||||
const name = options?.name ?? '<default>';
|
||||
if (!container.componentContexts[name]) {
|
||||
container.componentContexts[name] = new MolComponentContext(options?.name);
|
||||
}
|
||||
return container.componentContexts[name];
|
||||
}
|
||||
119
src/examples/mvs-stories/elements/snapshot-markdown.tsx
Normal file
119
src/examples/mvs-stories/elements/snapshot-markdown.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, distinctUntilChanged, map } from 'rxjs';
|
||||
import { PluginComponent } from '../../../mol-plugin-state/component';
|
||||
import { getMolComponentContext, MolComponentContext } from '../context';
|
||||
import { MolComponentViewerModel } from './viewer';
|
||||
import Markdown from 'react-markdown';
|
||||
import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
|
||||
import { MarkdownAnchor } from '../../../mol-plugin-ui/controls';
|
||||
import { PluginReactContext } from '../../../mol-plugin-ui/base';
|
||||
|
||||
export class MolComponentSnapshotMarkdownModel extends PluginComponent {
|
||||
readonly context: MolComponentContext;
|
||||
root: HTMLElement | undefined = undefined;
|
||||
|
||||
state = new BehaviorSubject<{
|
||||
entry?: PluginStateSnapshotManager.Entry,
|
||||
index?: number,
|
||||
all: PluginStateSnapshotManager.Entry[],
|
||||
}>({ all: [] });
|
||||
|
||||
get viewer() {
|
||||
return this.context.behavior.viewers.value?.find(v => this.options?.viewerName === v.name);
|
||||
}
|
||||
|
||||
sync() {
|
||||
const mng = this.viewer?.model.plugin?.managers.snapshot;
|
||||
this.state.next({
|
||||
entry: mng?.current,
|
||||
index: mng?.current ? mng?.getIndex(mng.current) : undefined,
|
||||
all: mng?.state.entries.toArray() ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
async mount(root: HTMLElement) {
|
||||
this.root = root;
|
||||
|
||||
createRoot(root).render(<MolComponentSnapshotMarkdownUI model={this} />);
|
||||
|
||||
let currentViewer: MolComponentViewerModel | undefined = undefined;
|
||||
let sub: { unsubscribe: () => void } | undefined = undefined;
|
||||
this.subscribe(this.context.behavior.viewers.pipe(
|
||||
map(xs => xs.find(v => this.options?.viewerName === v.name)),
|
||||
distinctUntilChanged((a, b) => a?.model === b?.model)
|
||||
), viewer => {
|
||||
if (currentViewer !== viewer) {
|
||||
currentViewer = viewer?.model;
|
||||
sub?.unsubscribe();
|
||||
}
|
||||
if (!viewer) return;
|
||||
sub = this.subscribe(viewer.model.plugin?.managers.snapshot.events.changed, () => {
|
||||
this.sync();
|
||||
});
|
||||
this.sync();
|
||||
});
|
||||
|
||||
this.sync();
|
||||
}
|
||||
|
||||
constructor(private options?: { context?: { name?: string, container?: object }, viewerName?: string }) {
|
||||
super();
|
||||
|
||||
this.context = getMolComponentContext(options?.context);
|
||||
}
|
||||
}
|
||||
|
||||
export function MolComponentSnapshotMarkdownUI({ model }: { model: MolComponentSnapshotMarkdownModel }) {
|
||||
const state = useBehavior(model.state);
|
||||
|
||||
if (state.all.length === 0) {
|
||||
return <div>
|
||||
<i>No snapshot loaded</i>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', width: '100%', gap: '8px' }} className='mc-snapshot-markdown-header'>
|
||||
<span style={{ lineHeight: '38px', minWidth: 60, maxWidth: 60, flexShrink: 0 }}>{typeof state.index === 'number' ? state.index + 1 : '-'}/{state.all.length}</span>
|
||||
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(-1)} style={{ flexGrow: 1, flexShrink: 0 }}>Prev</button>
|
||||
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(1)} style={{ flexGrow: 1, flexShrink: 0 }}>Next</button>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, overflow: 'hidden', overflowY: 'auto', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', inset: 0 }}>
|
||||
<PluginReactContext.Provider value={model.viewer?.model.plugin as any}>
|
||||
<Markdown skipHtml components={{ a: MarkdownAnchor }}>{state.entry?.description ?? 'Description not available'}</Markdown>
|
||||
</PluginReactContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export class MolComponentSnapshotMarkdownViewer extends HTMLElement {
|
||||
private model: MolComponentSnapshotMarkdownModel | undefined = undefined;
|
||||
|
||||
async connectedCallback() {
|
||||
this.model = new MolComponentSnapshotMarkdownModel({
|
||||
context: { name: this.getAttribute('context-name') ?? undefined },
|
||||
viewerName: this.getAttribute('viewer-name') ?? undefined,
|
||||
});
|
||||
await this.model.mount(this);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.model?.dispose();
|
||||
this.model = undefined;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('mc-snapshot-markdown', MolComponentSnapshotMarkdownViewer);
|
||||
113
src/examples/mvs-stories/elements/viewer.tsx
Normal file
113
src/examples/mvs-stories/elements/viewer.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MolViewSpec } from '../../../extensions/mvs/behavior';
|
||||
import { loadMVS } from '../../../extensions/mvs/load';
|
||||
import { MVSData } from '../../../extensions/mvs/mvs-data';
|
||||
import { PluginComponent } from '../../../mol-plugin-state/component';
|
||||
import { createPluginUI } from '../../../mol-plugin-ui';
|
||||
import { renderReact18 } from '../../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec } from '../../../mol-plugin-ui/spec';
|
||||
import { PluginConfig } from '../../../mol-plugin/config';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { PluginSpec } from '../../../mol-plugin/spec';
|
||||
import { getMolComponentContext, MolComponentContext } from '../context';
|
||||
|
||||
export class MolComponentViewerModel extends PluginComponent {
|
||||
readonly context: MolComponentContext;
|
||||
plugin?: PluginContext = undefined;
|
||||
|
||||
async mount(root: HTMLElement) {
|
||||
const spec = DefaultPluginUISpec();
|
||||
this.plugin = await createPluginUI({
|
||||
target: root,
|
||||
render: renderReact18,
|
||||
spec: {
|
||||
...spec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: false,
|
||||
showControls: false,
|
||||
controlsDisplay: 'landscape',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
remoteState: 'none',
|
||||
viewport: {
|
||||
snapshotDescription: EmptyDescription,
|
||||
}
|
||||
},
|
||||
behaviors: [
|
||||
...spec.behaviors,
|
||||
PluginSpec.Behavior(MolViewSpec)
|
||||
],
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribe(this.context.commands, async (cmd) => {
|
||||
if (!cmd) return;
|
||||
|
||||
if (cmd.kind === 'load-mvs') {
|
||||
if (cmd.url) {
|
||||
const data = await this.plugin!.runTask(this.plugin!.fetch({ url: cmd.url, type: 'string' }));
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
await loadMVS(this.plugin!, mvsData, { sanityChecks: true, sourceUrl: cmd.url, replaceExisting: true });
|
||||
} else if (cmd.data) {
|
||||
await loadMVS(this.plugin!, cmd.data, { sanityChecks: true, replaceExisting: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const viewers = this.context.behavior.viewers.value;
|
||||
const next = [...viewers, { name: this.options?.name, model: this }];
|
||||
this.context.behavior.viewers.next(next);
|
||||
}
|
||||
|
||||
constructor(private options?: { context?: { name?: string, container?: object }, name?: string }) {
|
||||
super();
|
||||
|
||||
this.context = getMolComponentContext(options?.context);
|
||||
|
||||
const viewers = this.context.behavior.viewers.value;
|
||||
const index = viewers.findIndex(v => v.name === options?.name);
|
||||
if (index >= 0) {
|
||||
const next = [...viewers];
|
||||
next[index].model.dispose();
|
||||
next.splice(index, 0);
|
||||
this.context.behavior.viewers.next(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function EmptyDescription() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export class MolComponentViewer extends HTMLElement {
|
||||
private model: MolComponentViewerModel | undefined = undefined;
|
||||
|
||||
async connectedCallback() {
|
||||
this.model = new MolComponentViewerModel({
|
||||
name: this.getAttribute('name') ?? undefined,
|
||||
context: { name: this.getAttribute('context-name') ?? undefined },
|
||||
});
|
||||
await this.model.mount(this);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.model?.dispose();
|
||||
this.model = undefined;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('mc-viewer', MolComponentViewer);
|
||||
116
src/examples/mvs-stories/index.html
Normal file
116
src/examples/mvs-stories/index.html
Normal file
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Molecular Stories</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 34%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 66%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 16px;
|
||||
padding-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-left: none;
|
||||
background: #F6F5F3;
|
||||
z-index: -2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#links {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 8px;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 0.6rem;
|
||||
z-index: -1;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#links a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 40%;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 60%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.markdown-explanation {
|
||||
font-size: 0.9rem !important;
|
||||
}
|
||||
|
||||
.markdown-explanation h3 {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.msp-viewport-controls-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="viewer">
|
||||
<mc-viewer name="v1" />
|
||||
</div>
|
||||
<div id="controls">
|
||||
<div id="select-story" class="select-story"></div>
|
||||
<div class="markdown-explanation" style="flex-grow: 1;">
|
||||
<mc-snapshot-markdown viewer-name="v1" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="links">
|
||||
<a href="#" id="mvs-data" filename="kinase-story.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.downloadStory();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.initStories();
|
||||
}, 0);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
99
src/examples/mvs-stories/index.tsx
Normal file
99
src/examples/mvs-stories/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { getMolComponentContext } from './context';
|
||||
import './index.html';
|
||||
import './elements/snapshot-markdown';
|
||||
import './elements/viewer';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
import './styles.scss';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Stories } from './stories';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
export class MolComponents {
|
||||
getContext(name?: string) {
|
||||
return getMolComponentContext({ name });
|
||||
}
|
||||
}
|
||||
|
||||
const MC = new MolComponents();
|
||||
|
||||
type Story = { kind: 'built-in', id: string } | { kind: 'url', url: string, format: 'mvsx' | 'mvsj' } | undefined;
|
||||
const CurrentStory = new BehaviorSubject<Story>(undefined);
|
||||
|
||||
function SelectStoryUI({ subject }: { subject: BehaviorSubject<Story> }) {
|
||||
const current = useBehavior(subject);
|
||||
const selectedId = current?.kind === 'built-in' ? current.id : current?.kind === 'url' ? 'url' : '';
|
||||
|
||||
return <select onChange={e => {
|
||||
const value = e.currentTarget.value;
|
||||
const s = Stories.find(s => s.id === value);
|
||||
if (!s) return;
|
||||
subject.next({ kind: 'built-in', id: s.id });
|
||||
}} value={selectedId}>
|
||||
{!current && <option value=''>Select a story...</option>}
|
||||
{Stories.map(s => <option key={s.name} value={s.id}>Story: {s.name}</option>)}
|
||||
{current?.kind === 'url' && <option disabled>------------------</option>}
|
||||
{current?.kind === 'url' && <option value='url'>{current.url}</option>}
|
||||
</select>;
|
||||
}
|
||||
|
||||
function init() {
|
||||
CurrentStory.subscribe(story => {
|
||||
if (!story) {
|
||||
history.replaceState({}, '', '');
|
||||
} else if (story.kind === 'url') {
|
||||
history.replaceState({}, '', story ? `?story-url=${encodeURIComponent(story.url)}&data-format=${story.format}` : '');
|
||||
MC.getContext().dispatch({
|
||||
kind: 'load-mvs',
|
||||
format: story.format,
|
||||
url: story.url,
|
||||
});
|
||||
} else if (story.kind === 'built-in') {
|
||||
history.replaceState({}, '', story ? `?story=${story.id}` : '');
|
||||
const s = Stories.find(s => s.id === story.id);
|
||||
if (s) {
|
||||
MC.getContext().dispatch({
|
||||
kind: 'load-mvs',
|
||||
data: s.buildStory(),
|
||||
});
|
||||
} else {
|
||||
console.warn('Story not found:', story.id);
|
||||
CurrentStory.next({ kind: 'built-in', id: Stories[0].id });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const storyUrl = urlParams.get('story-url');
|
||||
const dataFormat = urlParams.get('data-format') as 'mvsx' | 'mvsj' | null;
|
||||
const storyId = urlParams.get('story');
|
||||
|
||||
if (storyUrl) {
|
||||
CurrentStory.next({ kind: 'url', url: storyUrl, format: dataFormat ?? 'mvsj' });
|
||||
} else if (storyId) {
|
||||
CurrentStory.next({ kind: 'built-in', id: storyId });
|
||||
} else {
|
||||
CurrentStory.next({ kind: 'built-in', id: Stories[0].id });
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('select-story')!).render(<SelectStoryUI subject={CurrentStory} />);
|
||||
}
|
||||
|
||||
(window as any).mc = MC;
|
||||
(window as any).downloadStory = () => {
|
||||
if (CurrentStory.value?.kind !== 'built-in') return;
|
||||
const id = CurrentStory.value.id;
|
||||
const story = Stories.find(s => s.id === id);
|
||||
if (!story) return;
|
||||
const data = JSON.stringify(story.buildStory(), null, 2);
|
||||
download(new Blob([data], { type: 'application/json' }), 'story.mvsj');
|
||||
};
|
||||
(window as any).initStories = init;
|
||||
(window as any).CurrentStory = CurrentStory;
|
||||
57
src/examples/mvs-stories/readme.md
Normal file
57
src/examples/mvs-stories/readme.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# MolViewSpec Stories Example
|
||||
|
||||
This example illustrates:
|
||||
|
||||
- Using MolViewSpec to tell a story
|
||||
- A proof of concept for separating Mol* into a ready-to-use web component library.
|
||||
- Ability to load MVS states
|
||||
|
||||
### Usage
|
||||
|
||||
- Clone Mol* GitHub repo and build it.
|
||||
```bash
|
||||
git clone https://github.com/molstar/molstar.git
|
||||
cd molstar
|
||||
npm install
|
||||
npm build
|
||||
```
|
||||
|
||||
- Get `molstar.css` and `index.js` from `build/examples/mvs-stories` and include these to your HTML page
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
```
|
||||
|
||||
- Plate the components in your page wrapper in `<div>` elements to set up positioning:
|
||||
|
||||
```html
|
||||
<div class="viewer">
|
||||
<mc-viewer name="v1" />
|
||||
</div>
|
||||
<div class="snapshot">
|
||||
<mc-snapshot-markdown viewer-name="v1" />
|
||||
</div>
|
||||
```
|
||||
|
||||
- Load MolViewSpec state:
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.mc.getContext().dispatch({
|
||||
kind: 'load-mvs',
|
||||
format: 'mvsj',
|
||||
url: 'https://path/to/file.mvsj',
|
||||
// or provide data directly
|
||||
// data: mvsJSON
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
See [index.html](./index.html) for a full example.
|
||||
|
||||
- For interactive development build (for production use `npm run build`) of the example that immediately reflects changes use:
|
||||
|
||||
```bash
|
||||
npm run dev -- -e mvs-stories
|
||||
```
|
||||
13
src/examples/mvs-stories/stories/index.ts
Normal file
13
src/examples/mvs-stories/stories/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { buildStory as kinase } from './kinase';
|
||||
import { buildStory as tbp } from './tbp';
|
||||
|
||||
export const Stories = [
|
||||
{ id: 'kinase', name: 'BCR-ABL: A Kinase Out of Control', buildStory: kinase },
|
||||
{ id: 'tata', name: 'TATA-Binding Protein and its Role in Transcription Initiation ', buildStory: tbp },
|
||||
] as const;
|
||||
756
src/examples/mvs-stories/stories/kinase.ts
Normal file
756
src/examples/mvs-stories/stories/kinase.ts
Normal file
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
|
||||
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder, Structure as MVSStructure, Representation, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
import { ColorT, ComponentExpressionT, isPrimitiveComponentExpressions, PrimitivePositionT } from '../../../extensions/mvs/tree/mvs/param-types';
|
||||
import { Mat3, Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
const Domains = {
|
||||
ChainA: { auth_asym_id: 'A' },
|
||||
|
||||
SH2: { auth_asym_id: 'A', beg_auth_seq_id: 146, end_auth_seq_id: 247 },
|
||||
SH3: { auth_asym_id: 'A', beg_auth_seq_id: 83, end_auth_seq_id: 145 },
|
||||
P_loop: { auth_asym_id: 'A', beg_auth_seq_id: 246, end_auth_seq_id: 255 },
|
||||
Activation_loop: { auth_asym_id: 'A', beg_auth_seq_id: 384, end_auth_seq_id: 402 },
|
||||
};
|
||||
|
||||
const DomainColors = {
|
||||
SH2: '#8ED1A4' as ColorT,
|
||||
SH2_BCR: '#D03B4B' as ColorT,
|
||||
SH3: '#64B9AA' as ColorT,
|
||||
P_loop: 'pink' as ColorT,
|
||||
Activation_loop: 'red' as ColorT,
|
||||
DFG_motif: 'orange' as ColorT,
|
||||
};
|
||||
|
||||
const Colors = {
|
||||
'1opl': '#4577B2' as ColorT,
|
||||
'2gqg': '#BC536D' as ColorT,
|
||||
'2g2i': '#BC536D' as ColorT,
|
||||
'1iep': '#B9E3A0' as ColorT,
|
||||
'3ik3': '#F3774B' as ColorT,
|
||||
'3oxz': '#7D7EA5' as ColorT,
|
||||
|
||||
'active-site': '#F3794C' as ColorT,
|
||||
'binding-site': '#FEEB9F' as ColorT,
|
||||
};
|
||||
|
||||
// Obtained using https://www.rcsb.org/alignment
|
||||
// Aligned to 1iep
|
||||
const Superpositions = {
|
||||
'1opl': [-0.6321036327, 0.3450463255, 0.6938213248, 0, -0.6288677634, -0.7515716885, -0.1991615756, 0, 0.4527364948, -0.5622126202, 0.6920597055, 0, 36.3924122492, 118.2516908402, -26.4992054179, 1] as unknown as Mat4,
|
||||
'3ik3': [-0.7767826245, -0.6295936551, 0.0148520572, 0, 0.6059737752, -0.7408035481, 0.2898376906, 0, -0.1714775143, 0.2341408391, 0.9569605684, 0, 21.0648276775, 53.0266628762, -0.3385906075, 1] as unknown as Mat4,
|
||||
'2gqg': [0.0648740828, -0.7163272638, 0.6947421137, 0, 0.0160329972, -0.6953706204, -0.7184724374, 0, 0.9977646498, 0.0577490387, -0.0336266582, 0, -31.0690973964, 146.0940883054, 39.7107422531, 1] as unknown as Mat4,
|
||||
'2g2i': [-0.5680242227, 0.6527660987, 0.5012433569, 0, -0.10067389, 0.5493518768, -0.8295042395, 0, -0.8168312251, -0.5216406194, -0.2463286704, 0, -8.1905690894, 75.7603329146, -6.1327389269, 1] as unknown as Mat4,
|
||||
'3oxz': [0.7989033646, 0.5984398921, -0.0601922711, 0, -0.1303123126, 0.269921501, 0.9540236289, 0, 0.5871729857, -0.754328893, 0.2936252816, 0, -8.0697093741, 58.1709160658, 19.0363028443, 1] as unknown as Mat4,
|
||||
};
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'A Kinase Out of Control',
|
||||
key: 'intro',
|
||||
description: `
|
||||
### The Structural Story of BCR-ABL: A Kinase Out of Control
|
||||
|
||||
BCR-ABL is a classic case of how structural biology can drive drug discovery. This story will help you understand:
|
||||
|
||||
- How the [ABL kinase is normally regulated](#regulated-kinase).
|
||||
- How a small genetic fusion creates a [rogue kinase](#rogue-kinase).
|
||||
- How ATP binding fuels [uncontrolled cancer growth](#unstoppable-signaling).
|
||||
- How [Imatinib revolutionized treatment](#imatinib) by locking the kinase in an inactive state.
|
||||
- How a [single mutation (T315I) enabled resistance](#mutation) and brought new challenges.
|
||||
- How [Ponatinib](#ponatinib) and future inhibitors are being designed to keep up in this ongoing battle.
|
||||
`,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1opl = structure(builder, '1opl');
|
||||
const [_1opl_poly,] = polymer(_1opl, { color: Colors['1opl'] });
|
||||
_1opl_poly.label({ text: 'ABL Kinase' });
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'C' },
|
||||
uniform_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'D' },
|
||||
surface: true,
|
||||
carbon_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [103.72, 69.35, 20.52],
|
||||
target: [0.36, 55.32, 21.8],
|
||||
up: [-0.01, 0.01, -1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The ABL Kinase: A Well-Regulated Enzyme',
|
||||
key: 'regulated-kinase',
|
||||
description: `
|
||||
### The ABL Kinase: A Well-Regulated Enzyme
|
||||
|
||||
Normally, the ABL kinase ([PDB ID 1OPL](${wwPDBLink('1opl')})) is a well-regulated enzyme, kept in check by its SH3 and SH2 domains which fold back onto the kinase domain like a safety lock.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1opl = structure(builder, '1opl');
|
||||
const [_1opl_poly, _1opl_poly_repr] = polymer(_1opl, { color: Colors['1opl'] });
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'C' },
|
||||
uniform_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'D' },
|
||||
surface: true,
|
||||
carbon_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
domains(_1opl, _1opl_poly_repr, [
|
||||
[Domains.SH2, DomainColors.SH2, 'SH2'],
|
||||
[Domains.SH3, DomainColors.SH3, 'SH3'],
|
||||
], { label_size: 9 });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-18.33, -30.35, 48.2],
|
||||
target: [-10.37, 49.7, 12.68],
|
||||
up: [-0.27, -0.37, -0.89],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The Birth of a Rogue Kinase',
|
||||
key: 'rogue-kinase',
|
||||
transition_duration_ms: 750,
|
||||
description: `
|
||||
### The Birth of a Rogue Kinase
|
||||
|
||||
But in BCR-ABL, this safety mechanism is gone. A reciprocal translocation between chromosomes 9 and 22 creates the Philadelphia chromosome (Ph),
|
||||
fusing the ABL1 gene from chromosome 9 with the BCR gene on chromosome 22. This fusion produces the chimeric protein, BCR-ABL, which lacks the
|
||||
regulation of the wildtype protein. Read more about this [here](https://www.cancer.gov/publications/dictionaries/cancer-terms/def/philadelphia-chromosome)
|
||||
and [the history of its discovery](https://pmc.ncbi.nlm.nih.gov/articles/PMC1934591/).
|
||||
|
||||
Comparing the normal protein to the kinase domain alone ([PDB ID 2GQG](${wwPDBLink('2gqg')}), in light red), you can
|
||||
see how the SH3 and SH2 domains (teal in normal ABL, red in BCR-ABL, with SH3 domain being unresolved in the crystal structure) are no longer positioned to restrain the kinase.
|
||||
|
||||
With this lock removed, BCR-ABL is stuck in an active conformation, like an accelerator pedal jammed to the floor. Without
|
||||
its normal regulation, BCR-ABL will keep signaling, unchecked causing unregulated cell growth and cancer — [chronic myeloid leukemia (CML)](https://en.wikipedia.org/wiki/Chronic_myelogenous_leukemia).
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1opl = structure(builder, '1opl');
|
||||
const [_1opl_poly, _1opl_poly_repr] = polymer(_1opl, { color: Colors['1opl'] });
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'C' },
|
||||
uniform_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'D' },
|
||||
surface: true,
|
||||
carbon_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
domains(_1opl, _1opl_poly_repr, [
|
||||
[Domains.SH2, DomainColors.SH2, 'SH2'],
|
||||
[Domains.SH3, DomainColors.SH3, 'SH3'],
|
||||
], { label_size: 9 });
|
||||
|
||||
const _2gqg = structure(builder, '2gqg');
|
||||
const [, _2gqg_poly_repr] = polymer(_2gqg, { color: '#BF99A1' });
|
||||
|
||||
domains(_2gqg, _2gqg_poly_repr, [
|
||||
[Domains.SH2, DomainColors['SH2_BCR'], 'SH2 (BCR)'],
|
||||
], { label_size: 6 });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [30.7, -18.5, 13.47],
|
||||
target: [3.99, 47.45, 0.08],
|
||||
up: [-0.22, -0.28, -0.94],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'ATP Binding and Unstoppable Signaling [1/2]',
|
||||
key: 'unstoppable-signaling',
|
||||
description: `
|
||||
### ATP Binding and Unstoppable Signaling
|
||||
|
||||
To function, every kinase needs [ATP](https://en.wikipedia.org/wiki/Kinase), and BCR-ABL is no exception. ATP donates a phosphate group that is transferred to a substrate
|
||||
during phosphorylation — a key step in signaling pathways that control cell growth. However, ATP is chemically unstable under the conditions
|
||||
used for crystallography. It often breaks down into ADP (adenosine diphosphate), losing one of its three phosphate
|
||||
groups — the very group that would normally be transferred during catalysis.
|
||||
|
||||
Because of this instability, we don't have crystal structures of BCR-ABL bound to ATP itself. Instead, researchers have studied
|
||||
the H396P mutant, which locks the kinase in a permanently active conformation, to understand how it binds nucleotides.
|
||||
In this structure, [ADP](https://www.ebi.ac.uk/pdbe-srv/pdbechem/chemicalCompound/show/ADP) is clearly nestled in the kinase's active site.
|
||||
|
||||
Key catalytic residues — Lys271, Glu286, and Asp381 (in orange) — form a highly conserved network that helps position and stabilize the nucleotide.
|
||||
Glu286, in particular, forms a salt bridge with Lys271, anchoring the active site in a catalytically competent conformation. This arrangement
|
||||
supports efficient phosphate transfer, which is central to BCR-ABL's ability to activate downstream signaling pathways.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _2g2i = structure(builder, '2g2i');
|
||||
const [, _2g2i_poly_repr] = polymer(_2g2i, { color: Colors['2g2i'] });
|
||||
|
||||
ligand(_2g2i, {
|
||||
selector: { label_asym_id: 'E' },
|
||||
surface: true,
|
||||
label: 'ADP',
|
||||
label_size: 2,
|
||||
label_color: Colors['2g2i'],
|
||||
});
|
||||
|
||||
domains(_2g2i, _2g2i_poly_repr, [
|
||||
[Domains.SH2, DomainColors['SH2_BCR'], 'SH2'],
|
||||
[Domains.P_loop, DomainColors['P_loop'], 'P Loop'],
|
||||
[Domains.Activation_loop, DomainColors['Activation_loop'], 'Activation Loop (active)', { label_size: 3 }],
|
||||
], { label_size: 3 });
|
||||
|
||||
drawInteractions(_2g2i, [
|
||||
['Salt Bridge', { auth_asym_id: 'A', auth_seq_id: 271, auth_atom_id: 'NZ' }, { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE1' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
bindingSite(_2g2i, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [49.01, 78.47, 38.92],
|
||||
target: [15.59, 54.81, 12.37],
|
||||
up: [0.61, 0.03, -0.79],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'ATP Binding and Unstoppable Signaling [2/2]',
|
||||
description: `
|
||||
### ATP Binding and Unstoppable Signaling
|
||||
|
||||
Note the location of the activation loop (in red) which sits in its active conformation.
|
||||
|
||||
In normal cells, kinases like ABL are tightly regulated — ATP binding and activation only occur when needed. But in BCR-ABL, this regulation is lost.
|
||||
ATP binds freely, phosphorylation proceeds unchecked, and the signaling pathways that drive leukemia remain constantly switched on.
|
||||
`,
|
||||
state: (): Root => {
|
||||
return Steps.find((s: any) => s.key === 'unstoppable-signaling')?.state()!;
|
||||
},
|
||||
camera: {
|
||||
position: [98.66, 82.23, 14.15],
|
||||
target: [12.31, 54.23, 18.79],
|
||||
up: [0.06, -0.35, -0.93],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Imatinib: The Drug That Changed Everything [1/2]',
|
||||
key: 'imatinib',
|
||||
description: `
|
||||
### Imatinib: The Drug That Changed Everything
|
||||
|
||||
For years, chronic myeloid leukemia (CML) was a death sentence. Then came Imatinib (Gleevec) — a small molecule designed to fit into the ATP-binding pocket of BCR-ABL and lock the kinase in an inactive conformation. It was the first targeted cancer therapy of its kind.
|
||||
|
||||
Take a look at the Imatinib-bound structure ([PDB ID 1IEP](${wwPDBLink('1iep')})), and you'll notice a key difference — this time, the kinase is frozen in its inactive form. The drug (shown in colour) nestles deep in the ATP-binding site and blocks ATP from binding.
|
||||
|
||||
Imatinib forms specific interactions with several important residues:
|
||||
- A hydrogen bond with Thr315, the gatekeeper residue, which plays a major role in drug sensitivity and resistance.
|
||||
- Asp381, part of the DFG motif, which helps coordinate catalytic magnesium ions and position the phosphate for transfer.
|
||||
- Glu286, located in the αC-helix, normally forms a salt bridge with Lys271 in the active conformation — but here, it's flipped away.
|
||||
- Ile360 and His361, part of the activation loop, help stabilize the inactive conformation that Imatinib prefers.
|
||||
|
||||
Together, these interactions stabilize the inactive kinase, shutting down its activity and halting the signaling cascade that drives leukemia.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1iep = structure(builder, '1iep');
|
||||
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { label_asym_id: 'G' },
|
||||
surface: true,
|
||||
label: 'Imatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['1iep'],
|
||||
});
|
||||
|
||||
drawInteractions(_1iep, [
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'G', label_atom_id: 'N21' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 315, auth_atom_id: 'OG1' }, { label_asym_id: 'G', label_atom_id: 'N13' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'O29' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'Thr315',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
|
||||
], { color: Colors['binding-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [40.32, 68.65, 13.5],
|
||||
target: [16, 53.82, 14.88],
|
||||
up: [0.26, -0.5, -0.83],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Imatinib: The Drug That Changed Everything [2/2]',
|
||||
description: `
|
||||
### Imatinib: The Drug That Changed Everything
|
||||
|
||||
Notice how the P-loop, which normally cradles ATP in the active state, which was not visible in the active state, has shifted into a stabilised closed and collapsed conformation.
|
||||
At the same time, the activation loop (in green), which needs to be extended and open for the kinase to catalyse phosphorylation, is now flipped into a closed, inactive position.
|
||||
|
||||
Imatinib doesn't just block ATP from binding — it locks BCR-ABL into an inactive conformation, one where the active site is misaligned and the kinase simply can't function.
|
||||
|
||||
By switching between the ADP-bound active structure and the Imatinib-bound inactive structure, you can clearly see the conformational changes. The shift is dramatic and decisive: the enzyme goes from a catalytically ready state to one that is completely switched off.
|
||||
|
||||
The change is decisive: BCR-ABL is finally silenced.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1iep = structure(builder, '1iep');
|
||||
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { label_asym_id: 'G' },
|
||||
surface: true,
|
||||
label: 'Imatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['1iep'],
|
||||
});
|
||||
|
||||
domains(_1iep, _1iep_poly_repr, [
|
||||
[Domains.P_loop, DomainColors['P_loop'], 'P Loop'],
|
||||
[Domains.Activation_loop, DomainColors['Activation_loop'], 'Activation Loop (inactive)', { label_size: 3 }],
|
||||
], { label_size: 3 });
|
||||
|
||||
drawInteractions(_1iep, [
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'G', label_atom_id: 'N21' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 315, auth_atom_id: 'OG1' }, { label_asym_id: 'G', label_atom_id: 'N13' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'O29' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'Thr315',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
|
||||
], { color: Colors['binding-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [91.47, 73.63, 20.78],
|
||||
target: [12.53, 54.2, 19.09],
|
||||
up: [0.04, -0.07, -1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Resistance Strikes: The T315I Mutation [1/2]',
|
||||
key: 'mutation',
|
||||
description: `
|
||||
### Resistance Strikes: The T315I Mutation
|
||||
|
||||
For a while, it seemed like leukemia had been beaten. But then, in some patients, the cancer returned. The culprit?
|
||||
What was once a threonine (Thr) is now an isoleucine (Ile), a single mutation [T315I](https://doi.org/10.1016/j.ccr.2011.03.003), shown on [PDB ID 3IK3](${wwPDBLink('3ik3')}) in orange.
|
||||
|
||||
Forming a hydrogen bond with Imatinib, Thr315 was a crucial contact point. With bulkier and non-polar isoleucine in its place, the contact is lost and the drug won't bind.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1iep = structure(builder, '1iep');
|
||||
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { label_asym_id: 'G' },
|
||||
surface: true,
|
||||
label: 'Imatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['1iep'],
|
||||
});
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
carbon_color: Colors['1iep'],
|
||||
opacity: 0.51,
|
||||
});
|
||||
|
||||
const _3ik3 = structure(builder, '3ik3');
|
||||
const [, _3ik3_poly_repr] = polymer(_3ik3, { color: Colors['3ik3'] });
|
||||
|
||||
ligand(_3ik3, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'T315I',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [13.69, 72.8, 4.44],
|
||||
target: [13.02, 54.12, 9.71],
|
||||
up: [0.39, -0.26, -0.88],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Resistance Strikes: The T315I Mutation [2/2]',
|
||||
description: `
|
||||
### Resistance Strikes: The T315I Mutation
|
||||
|
||||
This mutation prevents Imatinib binding but still allows ATP (here represented by the ADP) to nestle into the active site.
|
||||
The result? Resistance. BCR-ABL is active again, and the leukemia returns, this time untouchable by Imatinib.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _2g2i = structure(builder, '2g2i');
|
||||
const [, _2g2i_poly_repr] = polymer(_2g2i, { color: Colors['2g2i'] });
|
||||
|
||||
ligand(_2g2i, {
|
||||
selector: { label_asym_id: 'E' },
|
||||
surface: true,
|
||||
label: 'ADP',
|
||||
label_size: 2,
|
||||
label_color: Colors['2g2i'],
|
||||
});
|
||||
|
||||
drawInteractions(_2g2i, [
|
||||
['Salt Bridge', { auth_asym_id: 'A', auth_seq_id: 271, auth_atom_id: 'NZ' }, { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE1' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
bindingSite(_2g2i, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
const _3ik3 = structure(builder, '3ik3');
|
||||
const [, _3ik3_poly_repr] = polymer(_3ik3, { color: Colors['3ik3'] });
|
||||
|
||||
ligand(_3ik3, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'T315I',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-3.29, 89.29, 2.7],
|
||||
target: [16.64, 55.48, 15.94],
|
||||
up: [0.24, -0.23, -0.94],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Fighting Back: Ponatinib and the Future of Kinase Inhibitors',
|
||||
key: 'ponatinib',
|
||||
description: `
|
||||
### Fighting Back: Ponatinib and the Future of Kinase Inhibitors
|
||||
|
||||
The battle didn't end there. Scientists knew they needed a new inhibitor—one that could work even against T315I. Enter Ponatinib (shown in [PDB ID 3OXZ](${wwPDBLink('3oxz')})), a next-generation
|
||||
drug designed to bypass this resistance. Viewing the Ponatinib-bound structure, you'll see how it differs from Imatinib. Instead of being blocked by T315I,
|
||||
Ponatinib has a flexible triple-bond linker, allowing it to slip into the binding site without clashing with the mutation.
|
||||
|
||||
Look closely at the interactions—Ponatinib forms new hydrophobic contacts that compensate for the loss of the Thr315 interaction. This structure tells a story of rational drug design: scientists
|
||||
used everything they learned about BCR-ABL's structure to engineer a molecule that could fit where others failed.
|
||||
|
||||
But the story isn't over. New mutations continue to arise, and leukemia is still finding ways to outmaneuver our drugs. The future may lie in allosteric
|
||||
inhibitors that bind outside the ATP pocket, or even in protein degradation strategies that eliminate BCR-ABL entirely. Whatever the next breakthrough is,
|
||||
it will start here—with a deep understanding of structure and function, and the power of visualization to reveal the molecular battles happening
|
||||
inside every cancer cell.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _3oxz = structure(builder, '3oxz');
|
||||
const [, _3oxz_poly_repr] = polymer(_3oxz, { color: Colors['3oxz'] });
|
||||
|
||||
ligand(_3oxz, {
|
||||
selector: { label_asym_id: 'B' },
|
||||
surface: true,
|
||||
label: 'Ponatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['3oxz'],
|
||||
});
|
||||
|
||||
ligand(_3oxz, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'T315I',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
drawInteractions(_3oxz, [
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'B', label_atom_id: 'N4' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'B', label_atom_id: 'N4' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'B', label_atom_id: 'N2' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'B', label_atom_id: 'O1' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'B', label_atom_id: 'N1' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
bindingSite(_3oxz, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [61.15, 66.58, 19.72],
|
||||
target: [9.61, 50.49, 14.08],
|
||||
up: [0.15, -0.15, -0.98],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The End',
|
||||
key: 'end',
|
||||
description: `
|
||||
### The End
|
||||
|
||||
That's all folks! We hope you enjoyed this interactive journey through the structural biology of BCR-ABL.
|
||||
|
||||
The next time you look at a macromolecular structure, remember: each atom tells a story, and each discovery shapes the future of medicine.
|
||||
|
||||
Read more [here](https://pmc.ncbi.nlm.nih.gov/articles/PMC3513788/).
|
||||
`,
|
||||
state: (): Root => {
|
||||
return Steps[0].state();
|
||||
},
|
||||
camera: {
|
||||
position: [103.72, 69.35, 20.52],
|
||||
target: [0.36, 55.32, 21.8],
|
||||
up: [-0.01, 0.01, -1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
type Interaction = [label: string, polymer: PrimitivePositionT, ligand: PrimitivePositionT, options?: { skipResidue?: boolean }]
|
||||
|
||||
function drawInteractions(structure: MVSStructure, interactions: Interaction[]) {
|
||||
const primitives = structure.primitives();
|
||||
|
||||
const interactingResidues: ComponentExpressionT[] = [];
|
||||
const addedResidues = new Set<string>();
|
||||
|
||||
for (const [tooltip, a, b, options] of interactions) {
|
||||
primitives.tube({ start: a, end: b, color: '#4289B5', tooltip, radius: 0.1, dash_length: 0.1 });
|
||||
|
||||
if (options?.skipResidue) continue;
|
||||
|
||||
const expressions = isPrimitiveComponentExpressions(a) ? a.expressions! : [a as ComponentExpressionT];
|
||||
for (const _e of expressions) {
|
||||
const e = { ..._e };
|
||||
delete e.auth_atom_id;
|
||||
delete e.label_atom_id;
|
||||
|
||||
const key = JSON.stringify(e);
|
||||
if (addedResidues.has(key)) continue;
|
||||
interactingResidues.push(e);
|
||||
addedResidues.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (interactingResidues.length === 0) return;
|
||||
|
||||
structure
|
||||
.component({ selector: interactingResidues })
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'element-symbol', params: {} } },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function transform(structure: MVSStructure, id: keyof typeof Superpositions) {
|
||||
const rotation = Mat3.fromMat4(Mat3.zero(), Superpositions[id]);
|
||||
const translation = Mat4.getTranslation(Vec3.zero(), Superpositions[id]) as any;
|
||||
return structure.transform({ rotation, translation });
|
||||
}
|
||||
|
||||
function wwPDBLink(id: string) {
|
||||
return `https://doi.org/10.2210/pdb${id.toLowerCase()}/pdb`;
|
||||
}
|
||||
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
let ret = builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
|
||||
if (id in Superpositions) {
|
||||
ret = transform(ret, id as any);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function domains(structure: MVSStructure, reprensentation: Representation, domains: [selector: ComponentExpressionT, color: ColorT, label?: string, options?: { label_size?: number }][], options?: { label_size?: number }) {
|
||||
const hasLabels = domains.some(d => !!d[2]);
|
||||
const primitives = hasLabels ? structure.primitives() : undefined;
|
||||
|
||||
for (const [selector, color, label, opts] of domains) {
|
||||
reprensentation.color({ selector, color });
|
||||
if (label) primitives!.label({ position: selector, text: label, label_color: color, label_size: opts?.label_size ?? options?.label_size ?? 1.5 });
|
||||
}
|
||||
}
|
||||
|
||||
function polymer(structure: MVSStructure, options: { color: ColorT }) {
|
||||
const component = structure.component({ selector: { label_asym_id: 'A' } });
|
||||
const reprensentation = component.representation({ type: 'cartoon' });
|
||||
reprensentation.color({ color: options.color });
|
||||
return [component, reprensentation] as const;
|
||||
}
|
||||
|
||||
function ligand(structure: MVSStructure, options: {
|
||||
selector: ComponentExpressionT | ComponentExpressionT[],
|
||||
label?: string,
|
||||
surface?: boolean,
|
||||
carbon_color?: ColorT,
|
||||
uniform_color?: ColorT,
|
||||
label_color?: ColorT,
|
||||
label_size?: number,
|
||||
opacity?: number,
|
||||
}) {
|
||||
const comp = structure.component({ selector: options.selector });
|
||||
const coloring = options.uniform_color
|
||||
? { color: options.uniform_color }
|
||||
: {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: options?.carbon_color ? { name: 'uniform', params: { value: decodeColor(options?.carbon_color) } } : { name: 'element-symbol', params: {} } }
|
||||
}
|
||||
};
|
||||
|
||||
if (options.surface) comp.representation({ type: 'surface' }).color(coloring).opacity({ opacity: 0.33 });
|
||||
const repr = comp.representation({ type: 'ball_and_stick' }).color(coloring);
|
||||
if (options.opacity) repr.opacity({ opacity: options.opacity });
|
||||
|
||||
const label_color: ColorT = options?.label_color ?? options.uniform_color ?? options.carbon_color ?? '#5B53A4';
|
||||
if (options.label) {
|
||||
structure.primitives().label({
|
||||
position: Array.isArray(options.selector) ? { expressions: options.selector } : options.selector,
|
||||
text: options.label,
|
||||
label_color,
|
||||
label_size: options?.label_size ?? 1.5
|
||||
});
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
function bindingSite(structure: MVSStructure, residues: [selector: ComponentExpressionT, label: string][], options: {
|
||||
color?: ColorT,
|
||||
label_size?: number,
|
||||
}) {
|
||||
const color: ColorT = options.color ?? '#5B53A4';
|
||||
const coloring = {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'uniform', params: { value: decodeColor(color) } } }
|
||||
}
|
||||
};
|
||||
|
||||
structure.component({ selector: residues.map(r => r[0]) }).representation({ type: 'ball_and_stick' }).color(coloring);
|
||||
|
||||
const primitives = structure.primitives();
|
||||
for (const [selector, label] of residues) {
|
||||
primitives.label({
|
||||
position: selector,
|
||||
text: label,
|
||||
label_color: color,
|
||||
label_size: options?.label_size ?? 1.5
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pdbUrl(id: string) {
|
||||
return `https://www.ebi.ac.uk/pdbe/entry-files/download/${id.toLowerCase()}.bcif`;
|
||||
}
|
||||
|
||||
export function buildStory(): MVSData_States {
|
||||
const snapshots = Steps.map((s, i) => {
|
||||
const builder = s.state();
|
||||
if (s.camera) builder.camera(s.camera);
|
||||
|
||||
const description = i > 0 ? `${s.description}\n\n[Go to start](#intro)` : s.description;
|
||||
|
||||
return builder.getSnapshot({
|
||||
title: s.header,
|
||||
key: s.key,
|
||||
description,
|
||||
description_format: 'markdown',
|
||||
linger_duration_ms: 5000,
|
||||
transition_duration_ms: s.transition_duration_ms ?? 1500,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'The Structural Story of BCR-ABL: A Kinase Out of Control',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
541
src/examples/mvs-stories/stories/tbp.ts
Normal file
541
src/examples/mvs-stories/stories/tbp.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
*/
|
||||
|
||||
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder, Structure as MVSStructure, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
import {
|
||||
ColorT,
|
||||
ComponentExpressionT,
|
||||
isPrimitiveComponentExpressions,
|
||||
PrimitivePositionT
|
||||
} from '../../../extensions/mvs/tree/mvs/param-types';
|
||||
import { Mat3, Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
|
||||
|
||||
const Colors = {
|
||||
'1vok': '#4577B2' as ColorT,
|
||||
'1cdw': '#BC536D' as ColorT,
|
||||
'1cdw-2': '#c5a3af' as ColorT,
|
||||
'1vtl': '#B9E3A0' as ColorT,
|
||||
'7enc': '#0072B2' as ColorT,
|
||||
'7enc-2': '#D55E00' as ColorT,
|
||||
'7enc-3': '#009E73' as ColorT,
|
||||
'7enc-4': '#56B4E9' as ColorT,
|
||||
};
|
||||
|
||||
// Obtained using https://www.rcsb.org/alignment
|
||||
const Superpositions = {
|
||||
'1cdw': [-0.4665815186, 0.6063873444, -0.6438913535, 0, -0.581544075, -0.7588303199, -0.2932286385, 0, -0.6664144171, 0.2376361381, 0.7066971703, 0, 135.0863694935, 105.5007997009, 153.6890178993, 1] as unknown as Mat4,
|
||||
'1vtl': [-0.4769460004, 0.7214347188, -0.5020502557, 0, -0.297882932, 0.4047204695, 0.8645617968, 0, 0.8269149119, 0.5619014932, 0.0218732801, 0, 65.6043682658, -3.7328402905, -16.8650755387, 1] as unknown as Mat4,
|
||||
'7enc': [0.8975055044, -0.4316347566, -0.0904174009, 0, 0.247274877, 0.3227849997, 0.9136000105, 0, -0.3651561375, -0.8423189899, 0.3964337454, 0, -189.7572972798, 304.0841220076, -411.5005782853, 1] as unknown as Mat4,
|
||||
};
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'TATA-Binding Protein',
|
||||
key: 'intro',
|
||||
description: `
|
||||
### TATA-Binding Protein Tells RNA Polymerase Where To Get Started on a Gene
|
||||
|
||||
Specialized DNA sequences next to genes, called promoters, define the proper start site and direction for transcription.
|
||||
Promoters vary in sequence and location from organism to organism.
|
||||
In eukaryotic cells, a complex promoter system that uses dozens of different proteins ensures that the proper RNA polymerase is targeted to each gene.
|
||||
The TATA-binding protein (TBP) is the central element of this system, participating in transcription by all three RNA polymerases.
|
||||
|
||||
The crystal structure of TBP from *Arabidopsis thaliana* in its apo form is shown ([PDB ID 1VOK](${wwPDBLink('1vok')})).
|
||||
The structure reveals a highly symmetric DNA-binding fold composed of two topologically identical domains derived from the two direct repeats in the phylogenetically conserved sequence.
|
||||
The domains are related by an approximate 2-fold axis, and consist of a five-stranded, antiparallel beta sheet and two alpha helices.
|
||||
It is thought that an ancient gene duplication created this protein by combining two copies of the same gene.
|
||||
|
||||
The intramolecular symmetry generates a saddle-shaped structure dominated by a curved antiparallel beta sheet, which forms the saddle's concave face and interacts with the DNA.
|
||||
The convex face of the saddle would interact with other proteins during transcription initiation.
|
||||
`,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1vok = structure(builder, '1vok');
|
||||
const _1vok_comp = _1vok.component({ selector: { label_asym_id: 'A' } });
|
||||
_1vok_comp.representation({ type: 'cartoon' })
|
||||
.color({
|
||||
custom: {
|
||||
molstar_color_theme_name: 'sequence-id',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'sequence-id', params: {} } },
|
||||
}
|
||||
});
|
||||
_1vok.primitives().label({
|
||||
position: { label_asym_id: 'A', label_seq_id: 88, label_atom_id: 'OD2' },
|
||||
text: 'TATA-Binding Protein',
|
||||
label_size: 5,
|
||||
label_color: Colors['1vok']
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [155.18, 118.49, 49.18],
|
||||
target: [74.81, 66.8, 30.7],
|
||||
up: [-0.55, 0.83, 0.1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Highly Conserved in Eukaryotes [1/2]',
|
||||
key: 'highly-conserved-1',
|
||||
description: `
|
||||
### TATA-Binding Protein Is Highly Conserved in Eukaryotes
|
||||
|
||||
TBP has a phylogenetically conserved, 180 amino-acid carboxy-terminal domain (ranging from 38 to 93% identity among eukaryotes and archaebacteria), containing two structural repeats flanking a highly basic segment known as the basic repeat.
|
||||
The C-terminal or core portion of the protein binds the TATA consensus sequence with high affinity, interacting with the minor groove and promoting DNA bending.
|
||||
Structural superposition of TBP bound to DNA in human ([PDB ID 1CDW](${wwPDBLink('1cdw')})) and *A. thaliana* ([PDB ID 1VTL](${wwPDBLink('1vtl')})) shows that their sequences are 83% identical and the RMSD between the structures is 0.43 Å.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'protein' });
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'nucleic', opacity: 0.5 })[0].label({ text: 'DNA' });
|
||||
// uses a range to 'adjust' font size of label
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', beg_label_seq_id: 160, end_label_seq_id: 177 }, text: 'TBP (H. sapiens)' });
|
||||
|
||||
const _1vtl = structure(builder, '1vtl');
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'E' } });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'A' }, opacity: 0.5 });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'B' }, opacity: 0.5 });
|
||||
// uses a range to 'adjust' font size of label
|
||||
label(_1vtl, { selector: { label_asym_id: 'E', beg_label_seq_id: 75, end_label_seq_id: 92 }, text: 'TBP (A. thaliana)' });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Minor Groove [2/2]',
|
||||
key: 'highly-conserved-2',
|
||||
description: `
|
||||
### TATA-Binding Protein Is Highly Conserved in Eukaryotes
|
||||
|
||||
Eukaryotic protein-coding genes transcribed by RNA polymerase II (pol II) have a characteristic sequence of nucleotides, termed the TATA box, in front of the start site of transcription.
|
||||
The typical sequence is something like T-A-T-A-a/t-A-a/t, where a/t refers to positions that can be either A or T.
|
||||
TBP recognizes this TATA sequence and binds to it, creating a landmark that directs pol II to the transcription start site.
|
||||
The structures of *A. thaliana* and human core TBP-TATA element co-crystal structures demonstrate a common induced-fit mechanism of protein-DNA recognition involving subtle conformation changes in the protein and an unprecedented DNA distortion.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'protein' });
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'nucleic', opacity: 0.5 });
|
||||
// select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'A', beg_label_seq_id: 6, end_label_seq_id: 11 } })[0].label({ text: 'TATA box' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 5 }, text: 'T' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 6 }, text: 'A' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 7 }, text: 'T' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 8 }, text: 'A' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 9 }, text: 'A' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 10 }, text: 'A' });
|
||||
|
||||
const _1vtl = structure(builder, '1vtl');
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'E' } });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'A' }, opacity: 0.5 });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'B' }, opacity: 0.5 });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [108.48, 92.12, 4.1],
|
||||
target: [80.16, 56.15, 28.96],
|
||||
up: [-0.71, 0.68, 0.17],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Binding to TATA Box [1/5]',
|
||||
key: 'tata-box-overview',
|
||||
description: `
|
||||
### TATA-Binding Protein Binds to the Minor Groove of the TATA Box
|
||||
|
||||
When the first structures of TBP-DNA complexes were determined, researchers discovered that TBP is not gentle when it binds to DNA.
|
||||
Instead, it grabs the TATA sequence, bends and unwinds it to open up the minor groove, and kinks it sharply in two places (*e.g.*, [PDB ID 1VTL](${wwPDBLink('1vtl')})) and ([PDB ID 1CDW](${wwPDBLink('1cdw')})).
|
||||
|
||||
Interactions with the minor groove can be divided into different classes as seen in PDB ([PDB ID 1CDW](${wwPDBLink('1cdw')})).
|
||||
The combination of interactions allows TATA-binding protein to recognize the proper DNA sequence.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Arginine [2/5]',
|
||||
key: 'tata-box-1',
|
||||
description: `
|
||||
### Arginine Residues
|
||||
|
||||
A string of arginine amino acids interact with the phosphate groups of the DNA and glues the protein to the DNA: Arg192, Arg199, Arg204, and Arg290.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 38 }, text: 'Arg192' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 7, label_atom_id: 'OP1' }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 6, label_atom_id: `O3'` }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH1' }, { label_asym_id: 'B', label_seq_id: 7, label_atom_id: 'OP1' }],
|
||||
]);
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 45 }, text: 'Arg199' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 45, label_atom_id: 'NE' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'OP1' }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 45, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'OP1' }],
|
||||
]);
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 136 }, text: 'Arg290' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 136, label_atom_id: 'NH1' }, { label_asym_id: 'A', label_seq_id: 8, label_atom_id: 'OP1' }],
|
||||
]);
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 50 }, text: 'Arg204' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'OP1' }, { label_asym_id: 'C', label_seq_id: 50, label_atom_id: 'NH1' }],
|
||||
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'OP1' }, { label_asym_id: 'C', label_seq_id: 50, label_atom_id: 'NH2' }],
|
||||
]);
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [113.87, 71.89, 26.29],
|
||||
target: [77.29, 61.61, 18.73],
|
||||
up: [-0.28, 0.96, 0.04],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Phenylalanine [3/5]',
|
||||
key: 'tata-box-2',
|
||||
description: `
|
||||
### Phenylalanine-Induced Kinks
|
||||
|
||||
At either end of the TATA element there are two pairs of phenylalanine side chains partially inserted between adjacent base pairs, producing the two dramatic kinks (Phe193, Phe210, Phe284, and Phe301). Between the two kinks the DNA is partially unwound.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
bindingSite(_1cdw, [
|
||||
[{ label_asym_id: 'C', label_seq_id: 39 }, 'Phe193'],
|
||||
[{ label_asym_id: 'C', label_seq_id: 56 }, 'Phe210'],
|
||||
[{ label_asym_id: 'C', label_seq_id: 130 }, 'Phe284'],
|
||||
[{ label_asym_id: 'C', label_seq_id: 147 }, 'Phe301'],
|
||||
], { color: Colors['1cdw'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [111.01, 69.92, -3.14],
|
||||
target: [85.52, 57.53, 19.15],
|
||||
up: [-0.51, 0.85, -0.11],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: H-Bonds in Minor Groove [4/5]',
|
||||
key: 'tata-box-3',
|
||||
description: `
|
||||
### Hydrogen Bonds in the Minor Groove
|
||||
|
||||
Polar side chains make minor groove hydrogen bonds with acceptors of base pairs centred about the approximate 2-fold symmetry axis (Asn163, Asn253, and Thr309).
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
// Asn: 9, 99, 155
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 9 }, text: 'Asn163' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 155 }, text: 'Thr309' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 9, label_atom_id: 'ND2' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'O2' }],
|
||||
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'O2' }, { label_asym_id: 'C', label_seq_id: 9, label_atom_id: 'ND2' }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 155, label_atom_id: 'OG1' }, { label_asym_id: 'A', label_seq_id: 8, label_atom_id: 'N3' }],
|
||||
]);
|
||||
bindingSite(_1cdw, [
|
||||
[{ label_asym_id: 'C', label_seq_id: 99 }, 'Asn253'],
|
||||
], { color: 'gray', label_color: Colors['1cdw'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [69.71, 56.65, 17.62],
|
||||
target: [75.68, 63.48, 32.29],
|
||||
up: [-0.9, 0.39, 0.18],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Non-Polar Interactions [5/5]',
|
||||
key: 'tata-box-4',
|
||||
description: `
|
||||
### Hydrophobic and van der Waals Interactions
|
||||
|
||||
Several residues projecting from the concave surface of TBP make hydrophobic or van der Waals side-chain/base contacts (<4 Å between non-hydrogen atoms) with the TATA element.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
surface(_1cdw, {
|
||||
selector: { label_asym_id: 'A' },
|
||||
carbon_color: Colors['1cdw-2'],
|
||||
});
|
||||
surface(_1cdw, {
|
||||
selector: { label_asym_id: 'B' },
|
||||
carbon_color: Colors['1cdw-2'],
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP and Transcription Pre-Initiation Complex',
|
||||
key: 'pre-init-complex',
|
||||
description: `
|
||||
### TATA-Binding Protein and the Transcription Pre-Initiation Complex
|
||||
|
||||
The structure shown here, from [PDB ID 7ENC](${wwPDBLink('7enc')}), is that of a pol II “pre-initiation complex” (PIC) poised to start transcription.
|
||||
This complex includes eukaryotic Mediator and pol II, along with a collection of general transcription factors that perform the necessary tasks of recognizing the transcription start site of genes, separating the two strands of the DNA double helix, and facilitating transcription initiation by RNA polymerase II.
|
||||
|
||||
TBP starts the process of assembling the PIC and works with TBP-associated factors (TAFs) as part of a large multi-component transcription factor, TFIID, that belongs to the collection of general transcription factors that cradle pol II with in the PIC.
|
||||
After TBP binds to the promoter, it recruits additional transcription factors.
|
||||
TFIIA and TFIIB interact with surrounding regions of the DNA and, along with TFIIF, assist with positioning of RNA polymerase II at the transcription start site.
|
||||
TFIIE, TFIIH, and other TFIID subunits bring additional functionality to the complex.
|
||||
In particular, part of TFIIH is a translocase that separates the two strands of DNA in preparation for transcription, and the CAK module of TFIIH adds phosphate groups to the long tail of RNA polymerase II, sending the signal that it is time to get started with mRNA synthesis.
|
||||
|
||||
TBP is a critical player that gets the transcription process started and is central to the pre-initiation complex.
|
||||
Lying at the very heart of this complex macromolecular machine, it is highly conserved among eukaryotes and *archaea*.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _7enc = structure(builder, '7enc');
|
||||
select(_7enc, { color: Colors['7enc'], selector: { label_asym_id: 'CB' } })[0].label({ text: 'TBP' });
|
||||
select(_7enc, { color: Colors['7enc-2'], selector: { label_asym_id: 'GB' } });
|
||||
select(_7enc, { color: Colors['7enc-2'], selector: { label_asym_id: 'HB' } });
|
||||
select(_7enc, { color: Colors['7enc-3'], opacity: 0.5 });
|
||||
|
||||
_7enc.primitives()
|
||||
.label({ position: { label_entity_id: '57' }, text: 'pol II', label_size: 20, label_color: Colors['7enc'] })
|
||||
.label({ position: { label_entity_id: '53' }, text: 'DNA', label_size: 20, label_color: Colors['7enc-2'] })
|
||||
.label({ position: { label_entity_id: '21' }, text: 'mediator', label_size: 20, label_color: Colors['7enc-3'] })
|
||||
.label({ position: { label_entity_id: '42' }, text: 'TBP-associated factors (TAFs)', label_size: 20, label_color: Colors['7enc-4'] })
|
||||
.label({ position: [100, 150, 0], text: 'PIC', label_size: 30, label_color: Colors['1vok'] })
|
||||
;
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The End',
|
||||
key: 'end',
|
||||
description: `
|
||||
### The End
|
||||
|
||||
That's all folks! We hope you enjoyed this interactive journey through the structural biology of the TATA-binding protein.
|
||||
|
||||
The next time you look at a macromolecular structure, remember: each atom tells a story, and each discovery shapes the future of medicine.
|
||||
|
||||
Read more in the relevant publications on PDB IDs [1VOK](https://doi.org/10.1038/nsb0994-621), [1CDW](https://doi.org/10.1073/pnas.93.10.4862), [1VTL](https://doi.org/10.1038/365520a0), and [7ENC](https://doi.org/10.1126/science.abg0635) as well as the Molecule of the Month articles on the [TATA-binding protein](https://pdb101.rcsb.org/motm/67) and [Mediator](https://pdb101.rcsb.org/motm/289).
|
||||
`,
|
||||
state: (): Root => {
|
||||
return Steps[Steps.length - 2].state();
|
||||
},
|
||||
camera: {
|
||||
position: [461.95, 218.66, 140.67],
|
||||
target: [87.56, -22.14, 54.59],
|
||||
up: [-0.55, 0.83, 0.1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}
|
||||
];
|
||||
|
||||
type Interaction = [label: string, res1: PrimitivePositionT, res2: PrimitivePositionT, options?: { skipResidue?: boolean }]
|
||||
|
||||
function drawInteractions(structure: MVSStructure, interactions: Interaction[]) {
|
||||
const primitives = structure.primitives();
|
||||
|
||||
const interactingResidues: ComponentExpressionT[] = [];
|
||||
const addedResidues = new Set<string>();
|
||||
|
||||
function drawResidue(a: PrimitivePositionT) {
|
||||
const expressions = isPrimitiveComponentExpressions(a) ? a.expressions! : [a as ComponentExpressionT];
|
||||
for (const _e of expressions) {
|
||||
const e = { ..._e };
|
||||
delete e.auth_atom_id;
|
||||
delete e.label_atom_id;
|
||||
|
||||
const key = JSON.stringify(e);
|
||||
if (addedResidues.has(key)) continue;
|
||||
interactingResidues.push(e);
|
||||
addedResidues.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [tooltip, a, b, options] of interactions) {
|
||||
primitives.tube({ start: a, end: b, color: '#4289B5', tooltip, radius: 0.1, dash_length: 0.1 });
|
||||
|
||||
if (options?.skipResidue) continue;
|
||||
|
||||
drawResidue(a);
|
||||
drawResidue(b);
|
||||
}
|
||||
|
||||
if (interactingResidues.length === 0) return;
|
||||
|
||||
structure
|
||||
.component({ selector: interactingResidues })
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'element-symbol', params: {} } },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function transform(structure: MVSStructure, id: keyof typeof Superpositions) {
|
||||
const rotation = Mat3.fromMat4(Mat3.zero(), Superpositions[id]);
|
||||
const translation = Mat4.getTranslation(Vec3.zero(), Superpositions[id]) as any;
|
||||
return structure.transform({ rotation, translation });
|
||||
}
|
||||
|
||||
function wwPDBLink(id: string) {
|
||||
return `https://doi.org/10.2210/pdb${id.toLowerCase()}/pdb`;
|
||||
}
|
||||
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
let ret = builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
|
||||
if (id in Superpositions) {
|
||||
ret = transform(ret, id as any);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function select(structure: MVSStructure, { color, opacity = 1.0, selector = 'polymer' }: { color: ColorT, opacity?: number, selector?: Partial<MVSNodeParams<'component'>>['selector'] }) {
|
||||
const component = structure.component({ selector });
|
||||
const representation = component.representation({ type: 'cartoon' });
|
||||
representation.color({ color }).opacity({ opacity });
|
||||
return [component, representation] as const;
|
||||
}
|
||||
|
||||
function label(structure: MVSStructure, { selector, text }: { selector: Partial<MVSNodeParams<'component'>>['selector'], text: string }) {
|
||||
structure.component({ selector })
|
||||
.label({ text });
|
||||
}
|
||||
|
||||
function surface(structure: MVSStructure, options: {
|
||||
selector: ComponentExpressionT | ComponentExpressionT[],
|
||||
carbon_color?: ColorT,
|
||||
}) {
|
||||
const comp = structure.component({ selector: options.selector });
|
||||
const coloring = {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: options?.carbon_color ? { name: 'uniform', params: { value: decodeColor(options?.carbon_color) } } : { name: 'element-symbol', params: { } } }
|
||||
}
|
||||
};
|
||||
comp.representation({ type: 'surface' }).color(coloring).opacity({ opacity: 0.33 });
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
function bindingSite(structure: MVSStructure, residues: [selector: ComponentExpressionT, label: string][], options: {
|
||||
color?: ColorT,
|
||||
label_size?: number,
|
||||
label_color?: ColorT,
|
||||
}) {
|
||||
const color: ColorT = options.color ?? '#5B53A4';
|
||||
const coloring = {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'uniform', params: { value: decodeColor(color) } } }
|
||||
}
|
||||
};
|
||||
|
||||
structure.component({ selector: residues.map(r => r[0]) }).representation({ type: 'ball_and_stick' }).color(coloring);
|
||||
|
||||
const primitives = structure.primitives();
|
||||
for (const [selector, label] of residues) {
|
||||
primitives.label({
|
||||
position: selector,
|
||||
text: label,
|
||||
label_color: options?.label_color ?? color,
|
||||
label_size: options?.label_size ?? 1.5
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pdbUrl(id: string) {
|
||||
return `https://www.ebi.ac.uk/pdbe/entry-files/download/${id.toLowerCase()}.bcif`;
|
||||
}
|
||||
|
||||
export function buildStory(): MVSData_States {
|
||||
const snapshots = Steps.map((s, i) => {
|
||||
const builder = s.state();
|
||||
if (s.camera) builder.camera(s.camera);
|
||||
|
||||
const description = i > 0 ? `${s.description}\n\n[Go to start](#intro)` : s.description;
|
||||
|
||||
return builder.getSnapshot({
|
||||
title: s.header,
|
||||
key: s.key,
|
||||
description,
|
||||
description_format: 'markdown',
|
||||
linger_duration_ms: 5000,
|
||||
transition_duration_ms: 1500,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'The Structural Story of TATA-Binding Protein and its Role in Transcription Initiation',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
182
src/examples/mvs-stories/styles.scss
Normal file
182
src/examples/mvs-stories/styles.scss
Normal file
@@ -0,0 +1,182 @@
|
||||
.select-story {
|
||||
select {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 8px;
|
||||
color: #555;
|
||||
line-height: 38px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-explanation {
|
||||
// Adapted from skeleton.css, The MIT License (MIT), Copyright (c) 2011-2014 Dave Gamache
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #222;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -.1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.6rem;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -.1rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -.1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.7rem;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -.08rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -.05rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 24px;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 38px;
|
||||
letter-spacing: .1rem;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: circle inside;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal inside;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ol,
|
||||
ol ul {
|
||||
margin: 1.5rem 0 1.5rem 3rem;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: .2rem .5rem;
|
||||
margin: 0 .2rem;
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
background: #F1F1F1;
|
||||
border: 1px solid #E1E1E1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre>code {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #E1E1E1;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
fieldset {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
pre,
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
table,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
form {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-width: 0;
|
||||
border-top: 1px solid #E1E1E1;
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,11 @@ import { PluginContext } from '../../mol-plugin/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
|
||||
import { AlphaOrbital, Basis, CubeGrid, CubeGridFormat, isCubeGridData } from './data-model';
|
||||
import { createSphericalCollocationDensityGrid } from './density';
|
||||
import { Tensor } from '../../mol-math/linear-algebra';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
|
||||
export class BasisAndOrbitals extends PluginStateObject.Create<{ basis: Basis, order: SphericalBasisOrder, orbitals: AlphaOrbital[] }>({ name: 'Basis', typeClass: 'Object' }) { }
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Location } from '../../mol-model/location';
|
||||
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
|
||||
import { getPalette, getPaletteParams } from '../../mol-util/color/palette';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
import { ColorThemeCategory } from '../../mol-theme/color/categories';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC);
|
||||
|
||||
@@ -102,7 +103,7 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
|
||||
export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams, AssemblySymmetryData.Tag.Cluster> = {
|
||||
name: AssemblySymmetryData.Tag.Cluster,
|
||||
label: 'Assembly Symmetry Cluster',
|
||||
category: ColorTheme.Category.Symmetry,
|
||||
category: ColorThemeCategory.Symmetry,
|
||||
factory: AssemblySymmetryClusterColorTheme,
|
||||
getParams: getAssemblySymmetryClusterColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getColorMapParams } from '../../../mol-util/color/params';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { ObjectKeys } from '../../../mol-util/type-helpers';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
|
||||
const Description = 'Assigns colors to confal pyramids';
|
||||
|
||||
@@ -62,7 +63,7 @@ export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values
|
||||
export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramidsColorThemeParams, 'confal-pyramids'> = {
|
||||
name: 'confal-pyramids',
|
||||
label: 'Confal Pyramids',
|
||||
category: ColorTheme.Category.Residue,
|
||||
category: ColorThemeCategory.Residue,
|
||||
factory: ConfalPyramidsColorTheme,
|
||||
getParams: getConfalPyramidsColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getColorMapParams } from '../../../mol-util/color/params';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { ObjectKeys } from '../../../mol-util/type-helpers';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
|
||||
const Description = 'Assigns colors to NtC Tube segments';
|
||||
|
||||
@@ -87,7 +88,7 @@ export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTub
|
||||
export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = {
|
||||
name: 'ntc-tube',
|
||||
label: 'NtC Tube',
|
||||
category: ColorTheme.Category.Residue,
|
||||
category: ColorThemeCategory.Residue,
|
||||
factory: NtCTubeColorTheme,
|
||||
getParams: getNtCTubeColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams),
|
||||
|
||||
106
src/extensions/interactions/compute.ts
Normal file
106
src/extensions/interactions/compute.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { InteractionsProps } from '../../mol-model-props/computed/interactions';
|
||||
import { FeatureType } from '../../mol-model-props/computed/interactions/common';
|
||||
import { computeInteractions, Interactions } from '../../mol-model-props/computed/interactions/interactions';
|
||||
import { Structure, StructureElement, Unit } from '../../mol-model/structure';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { InteractionInfo, InteractionTypeToKind, StructureInteractions } from './model';
|
||||
|
||||
export interface ComputeInteractionsOptions {
|
||||
interactions?: InteractionsProps
|
||||
}
|
||||
|
||||
export async function computeContacts(
|
||||
ctx: RuntimeContext,
|
||||
selection: readonly { structureRef: string, loci: StructureElement.Loci }[],
|
||||
options?: ComputeInteractionsOptions
|
||||
): Promise<StructureInteractions> {
|
||||
const unitIdToStructureRef = new Map<number, string>();
|
||||
const unitIdToContactGroupId = new Map<number, number>();
|
||||
const units: Unit[] = [];
|
||||
|
||||
let contactGroupId = 0;
|
||||
const builder = Structure.Builder();
|
||||
for (const { structureRef, loci } of selection) {
|
||||
const s = StructureElement.Loci.toStructure(loci);
|
||||
for (const unit of s.units) {
|
||||
const newUnit = builder.copyUnit(unit, { propagateTransientCache: true });
|
||||
units.push(newUnit);
|
||||
unitIdToStructureRef.set(newUnit.id, structureRef);
|
||||
unitIdToContactGroupId.set(newUnit.id, contactGroupId);
|
||||
}
|
||||
|
||||
contactGroupId++;
|
||||
}
|
||||
|
||||
const structure = builder.getStructure();
|
||||
const interactions = await computeInteractions(
|
||||
{ runtime: ctx, assetManager: new AssetManager() },
|
||||
structure,
|
||||
options?.interactions ?? {},
|
||||
{
|
||||
skipIntraContacts: true,
|
||||
unitPairTest: (a, b) => unitIdToContactGroupId.get(a.id) !== unitIdToContactGroupId.get(b.id)
|
||||
}
|
||||
);
|
||||
|
||||
const { edges } = interactions.contacts;
|
||||
const result: StructureInteractions = { kind: 'structure-interactions', elements: [] };
|
||||
for (const e of edges) {
|
||||
if (e.unitA > e.unitB) continue;
|
||||
|
||||
const [a, aType] = processFeature(structure, interactions, e.unitA, e.indexA);
|
||||
const [b] = processFeature(structure, interactions, e.unitB, e.indexB);
|
||||
|
||||
const kind = InteractionTypeToKind[e.props.type] ?? 'unknown';
|
||||
const info: InteractionInfo = { kind };
|
||||
|
||||
if (kind === 'hydrogen-bond' || kind === 'weak-hydrogen-bond') {
|
||||
const isADonor = aType === FeatureType.HydrogenDonor || aType === FeatureType.WeakHydrogenDonor;
|
||||
|
||||
result.elements.push({
|
||||
info,
|
||||
aStructureRef: isADonor ? unitIdToStructureRef.get(e.unitA)! : unitIdToStructureRef.get(e.unitB)!,
|
||||
bStructureRef: isADonor ? unitIdToStructureRef.get(e.unitB)! : unitIdToStructureRef.get(e.unitA)!,
|
||||
a: isADonor ? a : b,
|
||||
b: isADonor ? b : a,
|
||||
});
|
||||
} else {
|
||||
result.elements.push({
|
||||
info,
|
||||
aStructureRef: unitIdToStructureRef.get(e.unitA)!,
|
||||
bStructureRef: unitIdToStructureRef.get(e.unitB)!,
|
||||
a,
|
||||
b,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const _loc = StructureElement.Location.create();
|
||||
function processFeature(structure: Structure, interactions: Interactions, unitId: number, featureIndex: number) {
|
||||
_loc.structure = structure;
|
||||
_loc.unit = structure.unitMap.get(unitId);
|
||||
const xs = interactions.unitsFeatures.get(unitId)!;
|
||||
|
||||
let type: FeatureType = FeatureType.None;
|
||||
const builder = structure.subsetBuilder(false);
|
||||
builder.beginUnit(_loc.unit.id);
|
||||
for (let o = xs.offsets[featureIndex], uIEnd = xs.offsets[featureIndex + 1]; o < uIEnd; o++) {
|
||||
const unitIndex = xs.members[o];
|
||||
_loc.element = _loc.unit.elements[unitIndex];
|
||||
builder.addElement(_loc.element);
|
||||
type = xs.types[o];
|
||||
}
|
||||
builder.commitUnit();
|
||||
|
||||
return [Structure.toStructureElementLoci(builder.getStructure()), type] as const;
|
||||
}
|
||||
35
src/extensions/interactions/custom.ts
Normal file
35
src/extensions/interactions/custom.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure, StructureElement } from '../../mol-model/structure';
|
||||
import { InteractionElementSchema, InteractionInfo, StructureInteractionElement, StructureInteractions } from './model';
|
||||
|
||||
export function getCustomInteractionData(interactions: InteractionElementSchema[], structures: { [ref: string]: Structure }): StructureInteractions {
|
||||
const elements: StructureInteractionElement[] = [];
|
||||
|
||||
for (const schema of interactions) {
|
||||
let info: InteractionInfo;
|
||||
if (schema.kind === 'covalent') {
|
||||
info = { kind: schema.kind, degree: schema.degree };
|
||||
} else {
|
||||
info = { kind: schema.kind };
|
||||
}
|
||||
elements.push({
|
||||
sourceSchema: schema,
|
||||
info,
|
||||
aStructureRef: schema.aStructureRef,
|
||||
a: resolveLoci(structures[schema.aStructureRef!], schema.a),
|
||||
bStructureRef: schema.bStructureRef,
|
||||
b: resolveLoci(structures[schema.bStructureRef!], schema.b),
|
||||
});
|
||||
}
|
||||
|
||||
return { kind: 'structure-interactions', elements };
|
||||
}
|
||||
|
||||
function resolveLoci(structure: Structure, schema: StructureElement.Schema) {
|
||||
return StructureElement.Schema.toLoci(structure, schema);
|
||||
}
|
||||
86
src/extensions/interactions/model.ts
Normal file
86
src/extensions/interactions/model.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { InteractionType } from '../../mol-model-props/computed/interactions/common';
|
||||
import { StructureElement } from '../../mol-model/structure';
|
||||
|
||||
interface InteractionElementSchemaBase {
|
||||
aStructureRef?: string,
|
||||
a: StructureElement.Schema,
|
||||
bStructureRef?: string,
|
||||
b: StructureElement.Schema,
|
||||
description?: string,
|
||||
}
|
||||
|
||||
export type InteractionElementSchema =
|
||||
| { kind: 'unknown' } & InteractionElementSchemaBase
|
||||
| { kind: 'ionic' } & InteractionElementSchemaBase
|
||||
| { kind: 'pi-stacking' } & InteractionElementSchemaBase
|
||||
| { kind: 'cation-pi' } & InteractionElementSchemaBase
|
||||
| { kind: 'halogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
|
||||
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
|
||||
| { kind: 'salt-bridge' } & InteractionElementSchemaBase
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
|
||||
|
||||
export type InteractionKind = InteractionElementSchema['kind']
|
||||
|
||||
export const InteractionKinds: InteractionKind[] = [
|
||||
'unknown',
|
||||
'ionic',
|
||||
'pi-stacking',
|
||||
'cation-pi',
|
||||
'halogen-bond',
|
||||
'hydrogen-bond',
|
||||
'weak-hydrogen-bond',
|
||||
'hydrophobic',
|
||||
'metal-coordination',
|
||||
'salt-bridge',
|
||||
'covalent',
|
||||
];
|
||||
|
||||
export type InteractionInfo =
|
||||
| { kind: 'unknown' }
|
||||
| { kind: 'ionic' }
|
||||
| { kind: 'pi-stacking' }
|
||||
| { kind: 'cation-pi' }
|
||||
| { kind: 'halogen-bond' }
|
||||
| { kind: 'hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'hydrophobic' }
|
||||
| { kind: 'metal-coordination' }
|
||||
| { kind: 'salt-bridge' }
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
|
||||
|
||||
export interface StructureInteractionElement {
|
||||
// Pass the schema when loading from custom data
|
||||
sourceSchema?: InteractionElementSchema,
|
||||
|
||||
info: InteractionInfo,
|
||||
aStructureRef?: string,
|
||||
a: StructureElement.Loci,
|
||||
bStructureRef?: string,
|
||||
b: StructureElement.Loci,
|
||||
}
|
||||
|
||||
export interface StructureInteractions {
|
||||
kind: 'structure-interactions',
|
||||
elements: StructureInteractionElement[],
|
||||
}
|
||||
|
||||
export const InteractionTypeToKind = {
|
||||
[InteractionType.Unknown]: 'unknown' as InteractionKind,
|
||||
[InteractionType.Ionic]: 'ionic' as InteractionKind,
|
||||
[InteractionType.CationPi]: 'cation-pi' as InteractionKind,
|
||||
[InteractionType.PiStacking]: 'pi-stacking' as InteractionKind,
|
||||
[InteractionType.HydrogenBond]: 'hydrogen-bond' as InteractionKind,
|
||||
[InteractionType.HalogenBond]: 'halogen-bond' as InteractionKind,
|
||||
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
|
||||
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
|
||||
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
|
||||
};
|
||||
78
src/extensions/interactions/transforms.ts
Normal file
78
src/extensions/interactions/transforms.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { InteractionsParams } from '../../mol-model-props/computed/interactions';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { PluginStateObject as SO } from '../../mol-plugin-state/objects';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { computeContacts } from './compute';
|
||||
import { getCustomInteractionData } from './custom';
|
||||
import { InteractionElementSchema, StructureInteractions } from './model';
|
||||
import { buildInteractionsShape, InteractionVisualParams } from './visuals';
|
||||
|
||||
const Factory = StateTransformer.builderFactory('interactions-extension');
|
||||
|
||||
export class InteractionData extends SO.Create<{ interactions: StructureInteractions }>({ name: 'Interactions', typeClass: 'Data' }) { }
|
||||
|
||||
export const ComputeContacts = Factory({
|
||||
name: 'compute-contacts',
|
||||
display: 'Compute Contacts',
|
||||
from: SO.Molecule.Structure.Selections,
|
||||
to: InteractionData,
|
||||
params: {
|
||||
interactions: PD.Group(InteractionsParams),
|
||||
},
|
||||
})({
|
||||
apply({ params, a }) {
|
||||
return Task.create('Compute Contacts', async ctx => {
|
||||
const interactions = await computeContacts(ctx, a.data, { interactions: params.interactions });
|
||||
return new InteractionData({ interactions }, { label: 'Interactions' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const CustomInteractions = Factory({
|
||||
name: 'custom-interactions',
|
||||
display: 'Custom Interactions',
|
||||
from: SO.Root,
|
||||
to: InteractionData,
|
||||
params: {
|
||||
interactions: PD.Value<InteractionElementSchema[]>([], { isHidden: true }),
|
||||
},
|
||||
})({
|
||||
apply({ params, dependencies }) {
|
||||
return Task.create('Custom Interactions', async ctx => {
|
||||
const structures: { [ref: string]: Structure } = {};
|
||||
for (const [k, v] of Object.entries(dependencies ?? {})) {
|
||||
structures[k] = v.data as Structure;
|
||||
}
|
||||
const interactions = getCustomInteractionData(params.interactions, structures);
|
||||
return new InteractionData({ interactions }, { label: 'Custom Interactions' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const InteractionsShape = Factory({
|
||||
name: 'interactions-shape',
|
||||
display: { name: 'Interactions Shape' },
|
||||
from: InteractionData,
|
||||
to: SO.Shape.Provider,
|
||||
params: InteractionVisualParams
|
||||
})({
|
||||
canAutoUpdate: () => true,
|
||||
apply({ a, params }) {
|
||||
return new SO.Shape.Provider({
|
||||
label: 'Interactions Shape Provider',
|
||||
data: { interactions: a.data.interactions, params },
|
||||
params: PD.withDefaults(Mesh.Params, { }),
|
||||
getShape: (_, data: { interactions: StructureInteractions, params: InteractionVisualParams }, __, prev: any) => buildInteractionsShape(data.interactions, data.params, prev?.geometry),
|
||||
geometryUtils: Mesh.Utils,
|
||||
}, { label: 'Interactions Shape Provider' });
|
||||
}
|
||||
});
|
||||
199
src/extensions/interactions/visuals.ts
Normal file
199
src/extensions/interactions/visuals.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { addFixedCountDashedCylinder, addSimpleCylinder, BasicCylinderProps } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { StructureElement } from '../../mol-model/structure';
|
||||
import { addLinkCylinderMesh, AddLinkOptions, AddLinkParams, DefaultLinkCylinderProps, LinkStyle } from '../../mol-repr/structure/visual/util/link';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { InteractionKinds, StructureInteractions } from './model';
|
||||
|
||||
function visualParams({ color, style = 'dashed', radius = 0.04 }: { color: Color, style?: 'dashed' | 'solid', radius?: number }) {
|
||||
return PD.Group({
|
||||
color: PD.Color(color),
|
||||
style: PD.Select<'dashed' | 'solid'>(style, [['dashed', 'Dashed'], ['solid', 'Solid']]),
|
||||
radius: PD.Numeric(radius, { min: 0.01, max: 1, step: 0.01 }),
|
||||
});
|
||||
}
|
||||
|
||||
function hydrogenVisualParams({ color, style = 'dashed', radius = 0.04, showArrow = true, arrowOffset = 0.18 }: { color: Color, style?: 'dashed' | 'solid', radius?: number, showArrow?: boolean, arrowOffset?: number }) {
|
||||
return PD.Group({
|
||||
color: PD.Color(color),
|
||||
style: PD.Select<'dashed' | 'solid'>(style, [['dashed', 'Dashed'], ['solid', 'Solid']]),
|
||||
radius: PD.Numeric(radius, { min: 0.01, max: 1, step: 0.01 }),
|
||||
showArrow: PD.Boolean(showArrow),
|
||||
arrowOffset: PD.Numeric(arrowOffset, { min: 0, max: 1, step: 0.001 }),
|
||||
});
|
||||
}
|
||||
|
||||
export const InteractionVisualParams = {
|
||||
kinds: PD.MultiSelect(InteractionKinds, InteractionKinds.map(k => [k, stringToWords(k)])),
|
||||
styles: PD.Group({
|
||||
'unknown': visualParams({ color: Color(0x0) }),
|
||||
'ionic': visualParams({ color: Color(0xADD8E6) }),
|
||||
'pi-stacking': visualParams({ color: Color(0x1E3F66) }),
|
||||
'cation-pi': visualParams({ color: Color(0x06402B) }),
|
||||
'halogen-bond': visualParams({ color: Color(0xFFDE21) }),
|
||||
'hydrogen-bond': hydrogenVisualParams({ color: Color(0x0), style: 'solid' }),
|
||||
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
|
||||
'hydrophobic': visualParams({ color: Color(0x555555) }),
|
||||
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
|
||||
'salt-bridge': visualParams({ color: Color(0xF54029) }),
|
||||
'covalent': PD.Group({
|
||||
color: PD.Color(Color(0x999999)),
|
||||
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
export type InteractionVisualParams = PD.Values<typeof InteractionVisualParams>;
|
||||
|
||||
export function buildInteractionsShape(interactions: StructureInteractions, params: InteractionVisualParams, prev?: Mesh): Shape<Mesh> {
|
||||
const mesh = MeshBuilder.createState(interactions.elements.length * 128, 1024, prev);
|
||||
|
||||
mesh.currentGroup = -1;
|
||||
const tooltips = new Map<number, string>();
|
||||
|
||||
const visible = new Set(params.kinds);
|
||||
const kindsToWords = new Map(InteractionKinds.map(k => [k, stringToWords(k)]));
|
||||
|
||||
const colors = new Map<number, Color>();
|
||||
|
||||
const bA = { sphere: Sphere3D.zero() };
|
||||
const bB = { sphere: Sphere3D.zero() };
|
||||
|
||||
const pA = Vec3();
|
||||
const pB = Vec3();
|
||||
|
||||
const dir = Vec3();
|
||||
const capPos = Vec3();
|
||||
|
||||
const addLinkOptions: AddLinkOptions = {
|
||||
builderState: mesh,
|
||||
props: { ...DefaultLinkCylinderProps },
|
||||
};
|
||||
const addLinkParams: AddLinkParams = {
|
||||
a: pA,
|
||||
b: pB,
|
||||
group: 0,
|
||||
linkStub: false,
|
||||
linkStyle: LinkStyle.Solid,
|
||||
linkRadius: 0,
|
||||
};
|
||||
|
||||
for (const interaction of interactions.elements) {
|
||||
mesh.currentGroup++;
|
||||
if (!visible.has(interaction.info.kind)) continue;
|
||||
|
||||
let tooltip: string;
|
||||
if (interaction.info.kind === 'covalent') {
|
||||
if (interaction.info.degree === 'aromatic') tooltip = 'Aromatic';
|
||||
else if (interaction.info.degree === 1) tooltip = 'Single';
|
||||
else if (interaction.info.degree === 2) tooltip = 'Double';
|
||||
else if (interaction.info.degree === 3) tooltip = 'Triple';
|
||||
else if (interaction.info.degree === 4) tooltip = 'Quadruple';
|
||||
else tooltip = 'Covalent';
|
||||
} else {
|
||||
tooltip = kindsToWords.get(interaction.info.kind) ?? interaction.info.kind;
|
||||
}
|
||||
if (interaction.sourceSchema?.description) {
|
||||
tooltip += ` (${interaction.sourceSchema.description})`;
|
||||
}
|
||||
tooltips.set(mesh.currentGroup, tooltip);
|
||||
|
||||
const options = params.styles[interaction.info.kind];
|
||||
let style: 'dashed' | 'solid' = 'solid';
|
||||
if (interaction.info.kind !== 'covalent') {
|
||||
style = params.styles[interaction.info.kind].style;
|
||||
}
|
||||
|
||||
colors.set(mesh.currentGroup, params.styles[interaction.info.kind].color);
|
||||
|
||||
StructureElement.Loci.getBoundary(interaction.a, undefined, bA);
|
||||
StructureElement.Loci.getBoundary(interaction.b, undefined, bB);
|
||||
|
||||
Vec3.sub(dir, bB.sphere.center, bA.sphere.center);
|
||||
Vec3.normalize(dir, dir);
|
||||
|
||||
Vec3.copy(pA, bA.sphere.center);
|
||||
Vec3.copy(pB, bB.sphere.center);
|
||||
|
||||
if (interaction.info.kind === 'hydrogen-bond' || interaction.info.kind === 'weak-hydrogen-bond') {
|
||||
const hydrogenStyle = params.styles[interaction.info.kind];
|
||||
if (hydrogenStyle.showArrow && hydrogenStyle.arrowOffset > 0) {
|
||||
Vec3.scaleAndAdd(pB, pB, dir, -hydrogenStyle.arrowOffset);
|
||||
}
|
||||
|
||||
if (hydrogenStyle.showArrow) {
|
||||
const height = options.radius * 3;
|
||||
Vec3.scaleAndAdd(capPos, pB, dir, -height);
|
||||
cylinder(mesh, pA, capPos, options.radius, style);
|
||||
addSimpleCylinder(
|
||||
mesh,
|
||||
capPos,
|
||||
pB,
|
||||
{ radiusTop: 0, radiusBottom: height, topCap: false, bottomCap: true }
|
||||
);
|
||||
} else {
|
||||
cylinder(mesh, pA, pB, options.radius, style);
|
||||
}
|
||||
} else {
|
||||
if (interaction.info.kind !== 'covalent') {
|
||||
cylinder(mesh, pA, pB, options.radius, style);
|
||||
} else {
|
||||
addLinkParams.group = mesh.currentGroup;
|
||||
addLinkParams.linkRadius = options.radius;
|
||||
const degree = interaction.info.degree ?? 1;
|
||||
if (degree === 'aromatic') addLinkParams.linkStyle = LinkStyle.Aromatic;
|
||||
else if (degree === 2) addLinkParams.linkStyle = LinkStyle.Double;
|
||||
else if (degree === 3) addLinkParams.linkStyle = LinkStyle.Triple;
|
||||
else addLinkParams.linkStyle = LinkStyle.Solid;
|
||||
addLinkParams.a = pA;
|
||||
addLinkParams.b = pB;
|
||||
addLinkCylinderMesh(addLinkOptions, addLinkParams);
|
||||
addLinkParams.a = pB;
|
||||
addLinkParams.b = pA;
|
||||
addLinkCylinderMesh(addLinkOptions, addLinkParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Shape.create(
|
||||
'Interactions',
|
||||
interactions,
|
||||
MeshBuilder.getMesh(mesh),
|
||||
(g) => colors.get(g) ?? 0 as Color,
|
||||
(g) => 1,
|
||||
(g) => tooltips.get(g) ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function cylinder(mesh: MeshBuilder.State, a: Vec3, b: Vec3, radius: number, style: 'dashed' | 'solid') {
|
||||
const props: BasicCylinderProps = {
|
||||
radiusBottom: radius,
|
||||
radiusTop: radius,
|
||||
topCap: true,
|
||||
bottomCap: true,
|
||||
};
|
||||
|
||||
if (style === 'dashed') {
|
||||
const dist = Vec3.distance(a, b);
|
||||
const count = Math.ceil(dist / (2 * radius));
|
||||
addFixedCountDashedCylinder(mesh, a, b, 1.0, count, true, props);
|
||||
} else {
|
||||
if (style !== 'solid') {
|
||||
console.warn(`Unknown style '${style}', using 'solid' instead.`);
|
||||
}
|
||||
addSimpleCylinder(mesh, a, b, props);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021-24 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-25 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -38,10 +38,7 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
|
||||
private labelProvider = {
|
||||
label: (loci: Loci): string | undefined => {
|
||||
if (!this.params.showTooltip) return;
|
||||
return [
|
||||
plddtLabel(loci),
|
||||
qmeanLabel(loci),
|
||||
].filter(l => !!l).join('</br>');
|
||||
return metricLabels(loci)?.join('</br>');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,59 +103,77 @@ function plddtCategory(score: number) {
|
||||
return 'Very low';
|
||||
}
|
||||
|
||||
function plddtLabel(loci: Loci): string | undefined {
|
||||
return metricLabel(loci, 'pLDDT', (scoreAvg: number, countInfo: string) => `pLDDT Score ${countInfo}: ${scoreAvg.toFixed(2)} <small>(${plddtCategory(scoreAvg)})</small>`);
|
||||
function buildLabel(metric: QualityAssessment.Local, scoreAvg: number, countInfo: string) {
|
||||
let label = metric.name;
|
||||
if (metric.type !== metric.name) label += ` (${metric.type})`;
|
||||
if (countInfo) label += countInfo;
|
||||
label += `: ${scoreAvg.toFixed(2)}`;
|
||||
if (metric.kind === 'pLDDT') label += ` <small>(${plddtCategory(scoreAvg)})</small>`;
|
||||
return label;
|
||||
}
|
||||
|
||||
function qmeanLabel(loci: Loci): string | undefined {
|
||||
return metricLabel(loci, 'qmean', (scoreAvg: number, countInfo: string) => `QMEAN Score ${countInfo}: ${scoreAvg.toFixed(2)}`);
|
||||
interface MetricAggregate {
|
||||
metric: QualityAssessment.Local,
|
||||
scoreSum: number,
|
||||
seenResidues: Set<number>,
|
||||
}
|
||||
|
||||
function metricLabel(loci: Loci, name: 'qmean' | 'pLDDT', label: (scoreAvg: number, countInfo: string) => string): string | undefined {
|
||||
if (loci.kind === 'element-loci') {
|
||||
if (loci.elements.length === 0) return;
|
||||
function metricLabels(loci: Loci): string[] | undefined {
|
||||
if (loci.kind !== 'element-loci') return;
|
||||
if (loci.elements.length === 0) return;
|
||||
|
||||
const seen = new Set<number>();
|
||||
const scoreSeen = new Set<number>();
|
||||
let scoreSum = 0;
|
||||
const seenMetrics: MetricAggregate[] = [];
|
||||
const aggregates = new Map<string, MetricAggregate>();
|
||||
|
||||
for (const { indices, unit } of loci.elements) {
|
||||
const metric = QualityAssessmentProvider.get(unit.model).value?.[name];
|
||||
if (!metric) continue;
|
||||
for (const { indices, unit } of loci.elements) {
|
||||
const metrics = QualityAssessmentProvider.get(unit.model).value?.local;
|
||||
if (!metrics) continue;
|
||||
|
||||
const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index;
|
||||
const { elements } = unit;
|
||||
const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index;
|
||||
const { elements } = unit;
|
||||
|
||||
for (const metric of metrics) {
|
||||
const key = `${metric.name}-${metric.type}`;
|
||||
let aggregate = aggregates.get(key);
|
||||
if (!aggregate) {
|
||||
aggregate = { metric, scoreSum: 0, seenResidues: new Set() };
|
||||
aggregates.set(key, aggregate);
|
||||
seenMetrics.push(aggregate);
|
||||
}
|
||||
|
||||
const values = metric.values;
|
||||
const { seenResidues } = aggregate;
|
||||
|
||||
OrderedSet.forEach(indices, idx => {
|
||||
const eI = elements[idx];
|
||||
const rI = residueIndex[eI];
|
||||
|
||||
const residueKey = cantorPairing(rI, unit.id);
|
||||
if (!seen.has(residueKey)) {
|
||||
const score = metric.get(residueIndex[eI]) ?? -1;
|
||||
if (score !== -1) {
|
||||
scoreSum += score;
|
||||
scoreSeen.add(residueKey);
|
||||
}
|
||||
seen.add(residueKey);
|
||||
}
|
||||
if (seenResidues.has(residueKey)) return;
|
||||
|
||||
const score = values.get(residueIndex[eI]);
|
||||
if (typeof score === 'undefined') return;
|
||||
|
||||
aggregate!.scoreSum += score;
|
||||
aggregate!.seenResidues.add(residueKey);
|
||||
});
|
||||
}
|
||||
|
||||
if (seen.size === 0) return;
|
||||
|
||||
const summary: string[] = [];
|
||||
|
||||
if (scoreSeen.size) {
|
||||
const countInfo = `<small>(${scoreSeen.size} ${scoreSeen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`;
|
||||
const scoreAvg = scoreSum / scoreSeen.size;
|
||||
summary.push(label(scoreAvg, countInfo));
|
||||
}
|
||||
|
||||
if (summary.length) {
|
||||
return summary.join('</br>');
|
||||
}
|
||||
}
|
||||
|
||||
if (seenMetrics.length === 0) return;
|
||||
|
||||
const labels: string[] = [];
|
||||
for (const { metric, scoreSum, seenResidues } of seenMetrics) {
|
||||
let countInfo = '';
|
||||
if (seenResidues.size > 1) {
|
||||
countInfo = ` <small>(${seenResidues.size} Residues avg.)</small>`;
|
||||
}
|
||||
const scoreAvg = scoreSum / seenResidues.size;
|
||||
const label = buildLabel(metric, scoreAvg, countInfo);
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Mandar Deshpande <mandar@ebi.ac.uk>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { QualityAssessment, QualityAssessmentProvider } from '../prop';
|
||||
@@ -15,6 +16,7 @@ import { Color } from '../../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
|
||||
import { TableLegend } from '../../../../mol-util/legend';
|
||||
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
|
||||
|
||||
const DefaultColor = Color(0xaaaaaa);
|
||||
const ConfidenceColors = {
|
||||
@@ -28,7 +30,9 @@ const ConfidenceColors = {
|
||||
const ConfidenceColorLegend = TableLegend(Object.entries(ConfidenceColors));
|
||||
|
||||
export function getPLDDTConfidenceColorThemeParams(ctx: ThemeDataContext) {
|
||||
return {};
|
||||
return {
|
||||
metricId: QualityAssessment.getLocalOptions(ctx.structure?.models[0], 'pLDDT'),
|
||||
};
|
||||
}
|
||||
export type PLDDTConfidenceColorThemeParams = ReturnType<typeof getPLDDTConfidenceColorThemeParams>
|
||||
|
||||
@@ -43,7 +47,8 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
if (!Unit.isAtomic(unit)) return DefaultColor;
|
||||
|
||||
const qualityAssessment = QualityAssessmentProvider.get(unit.model).value;
|
||||
let score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]);
|
||||
const metric = qualityAssessment?.localMap.get(props.metricId!)?.values ?? qualityAssessment?.pLDDT;
|
||||
let score = metric?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]);
|
||||
if (typeof score !== 'number') {
|
||||
score = unit.model.atomicConformation.B_iso_or_equiv.value(element);
|
||||
}
|
||||
@@ -87,7 +92,7 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
export const PLDDTConfidenceColorThemeProvider: ColorTheme.Provider<PLDDTConfidenceColorThemeParams, 'plddt-confidence'> = {
|
||||
name: 'plddt-confidence',
|
||||
label: 'pLDDT Confidence',
|
||||
category: ColorTheme.Category.Validation,
|
||||
category: ColorThemeCategory.Validation,
|
||||
factory: PLDDTConfidenceColorTheme,
|
||||
getParams: getPLDDTConfidenceColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(getPLDDTConfidenceColorThemeParams({})),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-25 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { QualityAssessment, QualityAssessmentProvider } from '../prop';
|
||||
@@ -12,11 +13,14 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
|
||||
import { Color, ColorScale } from '../../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
|
||||
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
|
||||
|
||||
const DefaultColor = Color(0xaaaaaa);
|
||||
|
||||
export function getQmeanScoreColorThemeParams(ctx: ThemeDataContext) {
|
||||
return {};
|
||||
return {
|
||||
metricId: QualityAssessment.getLocalOptions(ctx.structure?.models[0], 'qmean'),
|
||||
};
|
||||
}
|
||||
export type QmeanScoreColorThemeParams = ReturnType<typeof getQmeanScoreColorThemeParams>
|
||||
|
||||
@@ -37,7 +41,8 @@ export function QmeanScoreColorTheme(ctx: ThemeDataContext, props: PD.Values<Qme
|
||||
const { unit, element } = location;
|
||||
if (!Unit.isAtomic(unit)) return DefaultColor;
|
||||
const qualityAssessment = QualityAssessmentProvider.get(unit.model).value;
|
||||
const score = qualityAssessment?.qmean?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1;
|
||||
const metric = qualityAssessment?.localMap.get(props.metricId!)?.values ?? qualityAssessment?.qmean;
|
||||
const score = metric?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1;
|
||||
if (score < 0) {
|
||||
return DefaultColor;
|
||||
} else {
|
||||
@@ -71,7 +76,7 @@ export function QmeanScoreColorTheme(ctx: ThemeDataContext, props: PD.Values<Qme
|
||||
export const QmeanScoreColorThemeProvider: ColorTheme.Provider<QmeanScoreColorThemeParams, 'qmean-score'> = {
|
||||
name: 'qmean-score',
|
||||
label: 'QMEAN Score',
|
||||
category: ColorTheme.Category.Validation,
|
||||
category: ColorThemeCategory.Validation,
|
||||
factory: QmeanScoreColorTheme,
|
||||
getParams: getQmeanScoreColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(getQmeanScoreColorThemeParams({})),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2024-25 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -29,6 +29,18 @@ function drawMetricPNG(model: Model, metric: QualityAssessment.Pairwise, colorRa
|
||||
ctx.fillStyle = Color.toStyle(noDataColor);
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const colorCache = new Map<number, string>();
|
||||
const getColor = (t: number) => {
|
||||
const rounded = Math.round(t * 0xffff);
|
||||
if (colorCache.has(rounded)) {
|
||||
return colorCache.get(rounded)!;
|
||||
}
|
||||
const color = Color.interpolate(minColor, maxColor, rounded / 0xffff);
|
||||
const style = Color.toStyle(color);
|
||||
colorCache.set(rounded, style);
|
||||
return style;
|
||||
};
|
||||
|
||||
for (let rA = minResidueIndex; rA <= maxResidueIndex; rA++) {
|
||||
const row = values[rA];
|
||||
if (!row) continue;
|
||||
@@ -40,11 +52,12 @@ function drawMetricPNG(model: Model, metric: QualityAssessment.Pairwise, colorRa
|
||||
const x = rA - minResidueIndex;
|
||||
const y = rB - minResidueIndex;
|
||||
const t = (value - minMetric) / valueRange;
|
||||
|
||||
const color = Color.interpolate(minColor, maxColor, t);
|
||||
ctx.fillStyle = Color.toStyle(color);
|
||||
ctx.fillStyle = getColor(t);
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
ctx.fillRect(y, x, 1, 1);
|
||||
|
||||
if (typeof values[rB]?.[rA] !== 'number') {
|
||||
ctx.fillRect(y, x, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,17 +18,39 @@ import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomi
|
||||
import { CustomPropSymbol } from '../../../mol-script/language/symbol';
|
||||
import { Type } from '../../../mol-script/language/type';
|
||||
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { ParamDefinition, ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
export { QualityAssessment };
|
||||
|
||||
interface QualityAssessment {
|
||||
localMetrics: Map<string, Map<ResidueIndex, number>>
|
||||
local: QualityAssessment.Local[]
|
||||
/** id -> metric info */
|
||||
localMap: Map<number, QualityAssessment.Local>
|
||||
|
||||
/** default pLDDT metric */
|
||||
pLDDT?: Map<ResidueIndex, number>
|
||||
/** default qmean metric */
|
||||
qmean?: Map<ResidueIndex, number>
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* NOTE: Keeping this around in case someone is using it
|
||||
* TODO: Remove in Mol* 5.0
|
||||
*/
|
||||
localMetrics: Map<string, Map<ResidueIndex, number>>
|
||||
}
|
||||
|
||||
namespace QualityAssessment {
|
||||
export interface Local {
|
||||
id: number
|
||||
kind?: 'pLDDT' | 'qmean';
|
||||
type: mmCIF_Schema['ma_qa_metric']['type']['T']
|
||||
name: string
|
||||
domain?: [number, number]
|
||||
valueRange: [number, number]
|
||||
values: Map<ResidueIndex, number>
|
||||
}
|
||||
|
||||
export interface Pairwise {
|
||||
id: number
|
||||
name: string
|
||||
@@ -40,6 +62,8 @@ namespace QualityAssessment {
|
||||
|
||||
const Empty = {
|
||||
value: {
|
||||
local: [],
|
||||
localMap: new Map<number, Local>(),
|
||||
localMetrics: new Map(),
|
||||
} satisfies QualityAssessment
|
||||
};
|
||||
@@ -52,9 +76,11 @@ namespace QualityAssessment {
|
||||
db.ma_qa_metric_local.ordinal_id.isDefined
|
||||
);
|
||||
if (localMetricName && hasLocalMetric) {
|
||||
const nameQuery = localMetricName.toLowerCase();
|
||||
for (let i = 0, il = db.ma_qa_metric._rowCount; i < il; i++) {
|
||||
if (db.ma_qa_metric.mode.value(i) !== 'local') continue;
|
||||
if (localMetricName === db.ma_qa_metric.name.value(i)) return true;
|
||||
const name = db.ma_qa_metric.name.value(i).toLowerCase();
|
||||
if (name.includes(nameQuery)) return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
@@ -62,27 +88,65 @@ namespace QualityAssessment {
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalOptions(model: Model | undefined, kind: 'pLDDT' | 'qmean') {
|
||||
if (!model) return ParamDefinition.Select(undefined, [], { label: 'Metric', isHidden: true });
|
||||
const local = QualityAssessmentProvider.get(model).value?.local;
|
||||
if (!local) return ParamDefinition.Select(undefined, [], { label: 'Metric', isHidden: true });
|
||||
const options = local.filter(m => m.kind === kind).map(m => [m.id, `${m.name} (${m.type})`] as [number, string]);
|
||||
return ParamDefinition.Select(options[0]?.[0], options, { label: 'Metric ' });
|
||||
}
|
||||
|
||||
export async function obtain(ctx: CustomProperty.Context, model: Model, props: QualityAssessmentProps): Promise<CustomProperty.Data<QualityAssessment>> {
|
||||
if (!model || !MmcifFormat.is(model.sourceData)) return Empty;
|
||||
const { ma_qa_metric, ma_qa_metric_local } = model.sourceData.data.db;
|
||||
const { model_id, label_asym_id, label_seq_id, metric_id, metric_value } = ma_qa_metric_local;
|
||||
const { index } = model.atomicHierarchy;
|
||||
|
||||
const locals: Local[] = [];
|
||||
const localMetrics = new Map<number, Local>();
|
||||
|
||||
// for simplicity we assume names in ma_qa_metric for mode 'local' are unique
|
||||
const localMetrics = new Map<string, Map<ResidueIndex, number>>();
|
||||
const localMetricValues = new Map<string, Map<ResidueIndex, number>>();
|
||||
const localNames = new Map<number, string>();
|
||||
|
||||
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
|
||||
if (ma_qa_metric.mode.value(i) !== 'local') continue;
|
||||
|
||||
const id = ma_qa_metric.id.value(i);
|
||||
const name = ma_qa_metric.name.value(i);
|
||||
if (localMetrics.has(name)) {
|
||||
const type = ma_qa_metric.type.value(i);
|
||||
const values: Map<ResidueIndex, number> = new Map();
|
||||
const ispPLDDType = type.toLowerCase().includes('plddt');
|
||||
const has01Range = type.replace(/\s/g, '').includes('[0,1]');
|
||||
|
||||
let domain: [number, number] | undefined;
|
||||
if (has01Range) {
|
||||
domain = [0, 1];
|
||||
} else if (ispPLDDType) {
|
||||
domain = [0, 100];
|
||||
}
|
||||
|
||||
let kind: 'pLDDT' | 'qmean' | undefined;
|
||||
|
||||
const nameLower = name.toLowerCase();
|
||||
if (nameLower.includes('plddt')) kind = 'pLDDT';
|
||||
else if (nameLower.includes('qmean')) kind = 'qmean';
|
||||
|
||||
if (!kind && ispPLDDType) kind = 'pLDDT';
|
||||
if (!kind && has01Range) kind = 'qmean';
|
||||
|
||||
const metric: Local = { id, kind, type, name, domain, valueRange: [Number.MAX_VALUE, -Number.MAX_VALUE], values };
|
||||
|
||||
localMetrics.set(id, metric);
|
||||
locals.push(metric);
|
||||
|
||||
if (localMetricValues.has(name)) {
|
||||
console.warn(`local ma_qa_metric with name '${name}' already added`);
|
||||
continue;
|
||||
}
|
||||
|
||||
localMetrics.set(name, new Map());
|
||||
localNames.set(ma_qa_metric.id.value(i), name);
|
||||
localMetricValues.set(name, values);
|
||||
localNames.set(id, name);
|
||||
}
|
||||
|
||||
const residueKey: AtomicIndex.ResidueLabelKey = {
|
||||
@@ -104,16 +168,25 @@ namespace QualityAssessment {
|
||||
|
||||
const rI = index.findResidueLabel(residueKey);
|
||||
if (rI >= 0) {
|
||||
const name = localNames.get(metric_id.value(i))!;
|
||||
localMetrics.get(name)!.set(rI, metric_value.value(i));
|
||||
const entry = localMetrics.get(metric_id.value(i));
|
||||
if (!entry) continue;
|
||||
|
||||
const value = metric_value.value(i);
|
||||
const range = entry.valueRange;
|
||||
if (value < range[0]) range[0] = value;
|
||||
if (value > range[1]) range[1] = value;
|
||||
entry.values.set(rI, value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: {
|
||||
localMetrics,
|
||||
pLDDT: localMetrics.get('pLDDT'),
|
||||
qmean: localMetrics.get('qmean'),
|
||||
local: Array.from(localMetrics.values()),
|
||||
localMap: localMetrics,
|
||||
pLDDT: locals.find(m => m.kind === 'pLDDT')?.values,
|
||||
qmean: locals.find(m => m.kind === 'qmean')?.values,
|
||||
|
||||
localMetrics: localMetricValues,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { MVSData } from '../mvs-data';
|
||||
|
||||
|
||||
describe('MVSData', () => {
|
||||
it('MVSData functions work', async () => {
|
||||
it.skip('MVSData functions work', async () => {
|
||||
const data = fs.readFileSync('examples/mvs/1cbs.mvsj', { encoding: 'utf8' });
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
expect(mvsData).toBeTruthy();
|
||||
@@ -26,7 +26,7 @@ describe('MVSData', () => {
|
||||
expect(prettyString.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('MVSData builder works', async () => {
|
||||
it.skip('MVSData builder works', async () => {
|
||||
const builder = MVSData.createBuilder();
|
||||
expect(builder).toBeTruthy();
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { Bond, StructureElement } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import type { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import type { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { decodeColor } from '../helpers/utils';
|
||||
@@ -73,7 +73,7 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
|
||||
export const MVSAnnotationColorThemeProvider: ColorTheme.Provider<MVSAnnotationColorThemeParams, 'mvs-annotation'> = {
|
||||
name: 'mvs-annotation',
|
||||
label: 'MVS Annotation',
|
||||
category: ColorTheme.Category.Misc,
|
||||
category: 'Miscellaneous', // ColorTheme.Category.Misc can cause webpack build error due to import ordering
|
||||
factory: MVSAnnotationColorTheme,
|
||||
getParams: ctx => MVSAnnotationColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(MVSAnnotationColorThemeParams),
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { Bond, Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
@@ -139,7 +140,7 @@ export function makeMultilayerColorThemeProvider(colorThemeRegistry: ColorTheme.
|
||||
return {
|
||||
name: MultilayerColorThemeName,
|
||||
label: 'MVS Multi-layer',
|
||||
category: ColorTheme.Category.Misc,
|
||||
category: ColorThemeCategory.Misc,
|
||||
factory: (ctx, props) => makeMultilayerColorTheme(ctx, props, colorThemeRegistry),
|
||||
getParams: (ctx: ThemeDataContext) => makeMultilayerColorThemeParams(colorThemeRegistry, ctx),
|
||||
defaultValues: DefaultMultilayerColorThemeProps,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2024-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
@@ -8,12 +8,17 @@
|
||||
import { Lines } from '../../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
|
||||
import { addFixedCountDashedCylinder, addSimpleCylinder, BasicCylinderProps } from '../../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { Text } from '../../../mol-geo/geometry/text/text';
|
||||
import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
|
||||
import { Box, BoxCage } from '../../../mol-geo/primitive/box';
|
||||
import { Circle } from '../../../mol-geo/primitive/circle';
|
||||
import { Primitive } from '../../../mol-geo/primitive/primitive';
|
||||
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { radToDeg } from '../../../mol-math/misc';
|
||||
import { Shape } from '../../../mol-model/shape';
|
||||
import { Structure, StructureElement, StructureSelection } from '../../../mol-model/structure';
|
||||
import { StructureQueryHelper } from '../../../mol-plugin-state/helpers/structure-query';
|
||||
@@ -278,6 +283,15 @@ const Builders: Record<PrimitiveParams['kind'], PrimitiveBuilder> = {
|
||||
},
|
||||
resolveRefs: resolveLineRefs,
|
||||
},
|
||||
arrow: {
|
||||
builders: {
|
||||
mesh: addArrowMesh,
|
||||
},
|
||||
resolveRefs: (params: PrimitiveParams<'arrow'>, refs: Set<string>) => {
|
||||
addRef(params.start, refs);
|
||||
if (params.end) addRef(params.end, refs);
|
||||
},
|
||||
},
|
||||
label: {
|
||||
builders: {
|
||||
label: addPrimitiveLabel,
|
||||
@@ -291,6 +305,45 @@ const Builders: Record<PrimitiveParams['kind'], PrimitiveBuilder> = {
|
||||
},
|
||||
resolveRefs: resolveLineRefs,
|
||||
},
|
||||
angle_measurement: {
|
||||
builders: {
|
||||
mesh: addAngleMesh,
|
||||
label: addAngleLabel,
|
||||
},
|
||||
resolveRefs: (params: PrimitiveParams<'angle_measurement'>, refs: Set<string>) => {
|
||||
addRef(params.a, refs);
|
||||
addRef(params.b, refs);
|
||||
addRef(params.c, refs);
|
||||
},
|
||||
},
|
||||
ellipse: {
|
||||
builders: {
|
||||
mesh: addEllipseMesh,
|
||||
},
|
||||
resolveRefs: (params: PrimitiveParams<'ellipse'>, refs: Set<string>) => {
|
||||
addRef(params.center, refs);
|
||||
if (params.major_axis_endpoint) addRef(params.major_axis_endpoint, refs);
|
||||
if (params.minor_axis_endpoint) addRef(params.minor_axis_endpoint, refs);
|
||||
},
|
||||
},
|
||||
ellipsoid: {
|
||||
builders: {
|
||||
mesh: addEllipsoidMesh,
|
||||
},
|
||||
resolveRefs: (params: PrimitiveParams<'ellipsoid'>, refs: Set<string>) => {
|
||||
addRef(params.center, refs);
|
||||
if (params.major_axis_endpoint) addRef(params.major_axis_endpoint, refs);
|
||||
if (params.minor_axis_endpoint) addRef(params.minor_axis_endpoint, refs);
|
||||
},
|
||||
},
|
||||
box: {
|
||||
builders: {
|
||||
mesh: addBoxMesh,
|
||||
},
|
||||
resolveRefs: (params: PrimitiveParams<'box'>, refs: Set<string>) => {
|
||||
addRef(params.center, refs);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -602,6 +655,86 @@ function addTubeMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBui
|
||||
}
|
||||
}
|
||||
|
||||
const ArrowState = {
|
||||
start: Vec3.zero(),
|
||||
end: Vec3.zero(),
|
||||
dir: Vec3.zero(),
|
||||
startCap: Vec3.zero(),
|
||||
endCap: Vec3.zero(),
|
||||
};
|
||||
|
||||
function addArrowMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'arrow'>) {
|
||||
resolveBasePosition(context, params.start, ArrowState.start);
|
||||
if (params.end) {
|
||||
resolveBasePosition(context, params.end, ArrowState.end);
|
||||
}
|
||||
|
||||
if (params.direction) {
|
||||
Vec3.add(ArrowState.end, ArrowState.start, params.direction as any as Vec3);
|
||||
}
|
||||
|
||||
Vec3.sub(ArrowState.dir, ArrowState.end, ArrowState.start);
|
||||
Vec3.normalize(ArrowState.dir, ArrowState.dir);
|
||||
|
||||
if (params.length) {
|
||||
Vec3.scaleAndAdd(ArrowState.end, ArrowState.start, ArrowState.dir, params.length);
|
||||
}
|
||||
|
||||
const length = Vec3.distance(ArrowState.start, ArrowState.end);
|
||||
if (length < 1e-3) return;
|
||||
|
||||
const tubeRadius = params.tube_radius;
|
||||
const tubeProps: BasicCylinderProps = {
|
||||
radiusBottom: tubeRadius,
|
||||
radiusTop: tubeRadius,
|
||||
topCap: !params.show_end_cap,
|
||||
bottomCap: !params.show_start_cap,
|
||||
};
|
||||
|
||||
mesh.currentGroup = groups.allocateSingle(node);
|
||||
groups.updateColor(mesh.currentGroup, params.color);
|
||||
groups.updateTooltip(mesh.currentGroup, params.tooltip);
|
||||
|
||||
const startRadius = params.start_cap_radius ?? tubeRadius;
|
||||
if (params.show_start_cap) {
|
||||
Vec3.scaleAndAdd(ArrowState.startCap, ArrowState.start, ArrowState.dir, startRadius);
|
||||
addSimpleCylinder(mesh, ArrowState.startCap, ArrowState.start, {
|
||||
radiusBottom: startRadius,
|
||||
radiusTop: 0,
|
||||
topCap: false,
|
||||
bottomCap: true,
|
||||
radialSegments: 12,
|
||||
});
|
||||
} else {
|
||||
Vec3.copy(ArrowState.startCap, ArrowState.start);
|
||||
}
|
||||
|
||||
const endRadius = params.end_cap_radius ?? tubeRadius;
|
||||
if (params.show_end_cap) {
|
||||
Vec3.scaleAndAdd(ArrowState.endCap, ArrowState.end, ArrowState.dir, -endRadius);
|
||||
addSimpleCylinder(mesh, ArrowState.endCap, ArrowState.end, {
|
||||
radiusBottom: endRadius,
|
||||
radiusTop: 0,
|
||||
topCap: false,
|
||||
bottomCap: true,
|
||||
radialSegments: 12,
|
||||
});
|
||||
} else {
|
||||
Vec3.copy(ArrowState.endCap, ArrowState.end);
|
||||
}
|
||||
|
||||
if (params.show_tube) {
|
||||
if (params.tube_dash_length) {
|
||||
const dist = Vec3.distance(ArrowState.startCap, ArrowState.endCap);
|
||||
const count = Math.ceil(dist / (2 * params.tube_dash_length));
|
||||
addFixedCountDashedCylinder(mesh, ArrowState.startCap, ArrowState.endCap, 1.0, count, true, tubeProps);
|
||||
} else {
|
||||
addSimpleCylinder(mesh, ArrowState.startCap, ArrowState.endCap, tubeProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getDistanceLabel(context: PrimitiveBuilderContext, params: PrimitiveParams<'distance_measurement'>) {
|
||||
resolveBasePosition(context, params.start, lStart);
|
||||
resolveBasePosition(context, params.end, lEnd);
|
||||
@@ -646,17 +779,332 @@ function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderS
|
||||
labels.add(label, labelPos[0], labelPos[1], labelPos[2], 1.05 * (params.radius), 1, group);
|
||||
}
|
||||
|
||||
|
||||
const AngleState = {
|
||||
a: Vec3(),
|
||||
b: Vec3(),
|
||||
c: Vec3(),
|
||||
ba: Vec3(),
|
||||
bc: Vec3(),
|
||||
labelPos: Vec3(),
|
||||
radius: 0,
|
||||
};
|
||||
|
||||
function syncAngleState(context: PrimitiveBuilderContext, params: PrimitiveParams<'angle_measurement'>) {
|
||||
resolveBasePosition(context, params.a, AngleState.a);
|
||||
resolveBasePosition(context, params.b, AngleState.b);
|
||||
resolveBasePosition(context, params.c, AngleState.c);
|
||||
Vec3.sub(AngleState.ba, AngleState.a, AngleState.b);
|
||||
Vec3.sub(AngleState.bc, AngleState.c, AngleState.b);
|
||||
const value = radToDeg(Vec3.angle(AngleState.ba, AngleState.bc));
|
||||
|
||||
const angle = `${round(value, 2)}\u00B0`;
|
||||
const label = typeof params.label_template === 'string' ? params.label_template.replace('{{angle}}', angle) : angle;
|
||||
|
||||
if (typeof params.section_radius === 'number') {
|
||||
AngleState.radius = params.section_radius;
|
||||
} else {
|
||||
AngleState.radius = Math.min(Vec3.magnitude(AngleState.ba), Vec3.magnitude(AngleState.bc));
|
||||
if (typeof params.section_radius_scale === 'number') {
|
||||
AngleState.radius *= params.section_radius_scale;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function addAngleMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'angle_measurement'>) {
|
||||
const label = syncAngleState(context, params);
|
||||
const { groups, mesh } = state;
|
||||
|
||||
if (params.show_vector) {
|
||||
const radius = 0.01;
|
||||
const cylinderProps: BasicCylinderProps = {
|
||||
radiusBottom: radius,
|
||||
radiusTop: radius,
|
||||
topCap: true,
|
||||
bottomCap: true,
|
||||
};
|
||||
|
||||
mesh.currentGroup = groups.allocateSingle(node);
|
||||
groups.updateColor(mesh.currentGroup, params.vector_color);
|
||||
groups.updateTooltip(mesh.currentGroup, label);
|
||||
|
||||
let count = Math.ceil(Vec3.magnitude(AngleState.ba) / (2 * radius));
|
||||
addFixedCountDashedCylinder(mesh, AngleState.a, AngleState.b, 1.0, count, true, cylinderProps);
|
||||
count = Math.ceil(Vec3.magnitude(AngleState.bc) / (2 * radius));
|
||||
addFixedCountDashedCylinder(mesh, AngleState.b, AngleState.c, 1.0, count, true, cylinderProps);
|
||||
}
|
||||
|
||||
if (params.show_section) {
|
||||
const angle = Vec3.angle(AngleState.ba, AngleState.bc);
|
||||
Vec3.normalize(AngleState.ba, AngleState.ba);
|
||||
Vec3.normalize(AngleState.bc, AngleState.bc);
|
||||
Vec3.scale(AngleState.ba, AngleState.ba, AngleState.radius);
|
||||
Vec3.scale(AngleState.bc, AngleState.bc, AngleState.radius);
|
||||
|
||||
addEllipseMesh(context, state, node, {
|
||||
kind: 'ellipse',
|
||||
as_circle: true,
|
||||
center: AngleState.b as any,
|
||||
major_axis_endpoint: null,
|
||||
major_axis: AngleState.ba as any,
|
||||
minor_axis_endpoint: null,
|
||||
minor_axis: AngleState.bc as any,
|
||||
radius_major: AngleState.radius,
|
||||
radius_minor: AngleState.radius,
|
||||
theta_start: 0,
|
||||
theta_end: angle,
|
||||
color: params.section_color,
|
||||
tooltip: label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addAngleLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'angle_measurement'>) {
|
||||
const { labels, groups } = state;
|
||||
const label = syncAngleState(context, params);
|
||||
|
||||
Vec3.normalize(AngleState.ba, AngleState.ba);
|
||||
Vec3.normalize(AngleState.bc, AngleState.bc);
|
||||
Vec3.scale(AngleState.ba, AngleState.ba, AngleState.radius);
|
||||
Vec3.scale(AngleState.bc, AngleState.bc, AngleState.radius);
|
||||
|
||||
let size: number | undefined;
|
||||
if (typeof params.label_size === 'number') {
|
||||
size = params.label_size;
|
||||
} else {
|
||||
size = Math.max(AngleState.radius * (params.label_auto_size_scale), params.label_auto_size_min);
|
||||
}
|
||||
|
||||
Vec3.add(AngleState.labelPos, AngleState.ba, AngleState.bc);
|
||||
Vec3.normalize(AngleState.labelPos, AngleState.labelPos);
|
||||
Vec3.scale(AngleState.labelPos, AngleState.labelPos, AngleState.radius);
|
||||
Vec3.add(AngleState.labelPos, AngleState.labelPos, AngleState.b);
|
||||
|
||||
const group = groups.allocateSingle(node);
|
||||
groups.updateColor(group, params.label_color);
|
||||
groups.updateSize(group, size);
|
||||
|
||||
labels.add(label, AngleState.labelPos[0], AngleState.labelPos[1], AngleState.labelPos[2], 1, 1, group);
|
||||
}
|
||||
|
||||
function resolveLabelRefs(params: PrimitiveParams<'label'>, refs: Set<string>) {
|
||||
addRef(params.position, refs);
|
||||
}
|
||||
|
||||
const PrimitiveLabelState = {
|
||||
position: Vec3.zero(),
|
||||
sphere: Sphere3D.zero(),
|
||||
};
|
||||
|
||||
function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'label'>) {
|
||||
const { labels, groups } = state;
|
||||
resolveBasePosition(context, params.position, labelPos);
|
||||
resolvePosition(context, params.position, PrimitiveLabelState.position, PrimitiveLabelState.sphere, undefined);
|
||||
|
||||
const group = groups.allocateSingle(node);
|
||||
groups.updateColor(group, params.label_color);
|
||||
groups.updateSize(group, params.label_size);
|
||||
|
||||
labels.add(params.text, labelPos[0], labelPos[1], labelPos[2], params.label_offset, 1, group);
|
||||
const offset = PrimitiveLabelState.sphere.radius + params.label_offset;
|
||||
labels.add(params.text, PrimitiveLabelState.position[0], PrimitiveLabelState.position[1], PrimitiveLabelState.position[2], offset, 1, group);
|
||||
}
|
||||
|
||||
const circleCache = new Map<string, Primitive>();
|
||||
|
||||
function getCircle(options: { thetaStart?: number, thetaEnd?: number }) {
|
||||
const key = JSON.stringify(options);
|
||||
if (circleCache.has(key)) return circleCache.get(key)!;
|
||||
const thetaLength = (options.thetaEnd ?? 2 * Math.PI) - (options.thetaStart ?? 0);
|
||||
if (Math.abs(thetaLength) < 1e-3) return null;
|
||||
|
||||
const circle = Circle({
|
||||
radius: 1,
|
||||
thetaStart: options.thetaStart ?? 0,
|
||||
thetaLength,
|
||||
segments: Math.ceil(2 * Math.PI / thetaLength * 64),
|
||||
});
|
||||
circleCache.set(key, circle);
|
||||
return circle;
|
||||
}
|
||||
|
||||
const EllipseState = {
|
||||
centerPos: Vec3.zero(),
|
||||
majorPos: Vec3.zero(),
|
||||
minorPos: Vec3.zero(),
|
||||
majorAxis: Vec3.zero(),
|
||||
minorAxis: Vec3.zero(),
|
||||
scale: Vec3.zero(),
|
||||
normal: Vec3.zero(),
|
||||
scaleXform: Mat4.identity(),
|
||||
rotationXform: Mat4.identity(),
|
||||
translationXform: Mat4.identity(),
|
||||
xform: Mat4.identity(),
|
||||
};
|
||||
|
||||
|
||||
function addEllipseMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'ellipse'>) {
|
||||
// Unit circle in the XZ plane (Y up)
|
||||
// X = minor axis, Y = normal, Z = major axis
|
||||
|
||||
const circle = getCircle({ thetaStart: params.theta_start, thetaEnd: params.theta_end });
|
||||
if (!circle) return;
|
||||
|
||||
resolvePosition(context, params.center, EllipseState.centerPos, undefined, undefined);
|
||||
|
||||
if (params.major_axis_endpoint) {
|
||||
resolvePosition(context, params.major_axis_endpoint, EllipseState.majorPos, undefined, undefined);
|
||||
Vec3.sub(EllipseState.majorAxis, EllipseState.majorPos, EllipseState.centerPos);
|
||||
} else {
|
||||
Vec3.copy(EllipseState.majorAxis, params.major_axis as any as Vec3);
|
||||
}
|
||||
|
||||
if (params.minor_axis_endpoint) {
|
||||
resolvePosition(context, params.minor_axis_endpoint, EllipseState.minorPos, undefined, undefined);
|
||||
Vec3.sub(EllipseState.minorAxis, EllipseState.minorPos, EllipseState.centerPos);
|
||||
} else {
|
||||
Vec3.copy(EllipseState.minorAxis, params.minor_axis as any as Vec3);
|
||||
}
|
||||
|
||||
const { mesh, groups } = state;
|
||||
|
||||
// Translation
|
||||
Mat4.fromTranslation(EllipseState.translationXform, EllipseState.centerPos);
|
||||
|
||||
// Scale
|
||||
if (params.as_circle) {
|
||||
const r = params.radius_major ?? Vec3.magnitude(EllipseState.majorAxis);
|
||||
Vec3.set(EllipseState.scale, r, 1, r);
|
||||
} else {
|
||||
const major = params.radius_major ?? Vec3.magnitude(EllipseState.majorAxis);
|
||||
const minor = params.radius_minor ?? Vec3.magnitude(EllipseState.minorAxis);
|
||||
Vec3.set(EllipseState.scale, minor, 1, major);
|
||||
}
|
||||
Mat4.fromScaling(EllipseState.scaleXform, EllipseState.scale);
|
||||
|
||||
// Rotation
|
||||
Vec3.normalize(EllipseState.minorAxis, EllipseState.minorAxis);
|
||||
Vec3.normalize(EllipseState.majorAxis, EllipseState.majorAxis);
|
||||
Vec3.cross(EllipseState.normal, EllipseState.majorAxis, EllipseState.minorAxis);
|
||||
|
||||
Mat4.targetTo(EllipseState.rotationXform, Vec3.origin, EllipseState.majorAxis, EllipseState.normal);
|
||||
Mat4.mul(EllipseState.rotationXform, EllipseState.rotationXform, Mat4.rotY180);
|
||||
|
||||
// Final xform
|
||||
Mat4.mul3(EllipseState.xform, EllipseState.translationXform, EllipseState.rotationXform, EllipseState.scaleXform);
|
||||
|
||||
mesh.currentGroup = groups.allocateSingle(node);
|
||||
groups.updateColor(mesh.currentGroup, params.color);
|
||||
groups.updateTooltip(mesh.currentGroup, params.tooltip);
|
||||
|
||||
MeshBuilder.addPrimitive(mesh, EllipseState.xform, circle);
|
||||
MeshBuilder.addPrimitiveFlipped(mesh, EllipseState.xform, circle);
|
||||
}
|
||||
|
||||
const EllipsoidState = {
|
||||
centerPos: Vec3.zero(),
|
||||
majorPos: Vec3.zero(),
|
||||
minorPos: Vec3.zero(),
|
||||
majorAxis: Vec3.zero(),
|
||||
minorAxis: Vec3.zero(),
|
||||
sphere: Sphere3D.zero(),
|
||||
radius: Vec3.zero(),
|
||||
extent: Vec3.zero(),
|
||||
up: Vec3.zero(),
|
||||
};
|
||||
|
||||
|
||||
function addEllipsoidMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'ellipsoid'>) {
|
||||
resolvePosition(context, params.center, EllipsoidState.centerPos, EllipsoidState.sphere, undefined);
|
||||
|
||||
if (params.major_axis_endpoint) {
|
||||
resolvePosition(context, params.major_axis_endpoint, EllipsoidState.majorPos, undefined, undefined);
|
||||
Vec3.sub(EllipsoidState.majorAxis, EllipsoidState.majorPos, EllipsoidState.centerPos);
|
||||
} else if (params.major_axis) {
|
||||
Vec3.copy(EllipsoidState.majorAxis, params.major_axis as any as Vec3);
|
||||
} else {
|
||||
Vec3.copy(EllipsoidState.majorAxis, Vec3.unitX);
|
||||
}
|
||||
|
||||
if (params.minor_axis_endpoint) {
|
||||
resolvePosition(context, params.minor_axis_endpoint, EllipsoidState.minorPos, undefined, undefined);
|
||||
Vec3.sub(EllipsoidState.minorAxis, EllipsoidState.minorPos, EllipsoidState.centerPos);
|
||||
} else if (params.minor_axis) {
|
||||
Vec3.copy(EllipsoidState.minorAxis, params.minor_axis as any as Vec3);
|
||||
} else {
|
||||
Vec3.copy(EllipsoidState.minorAxis, Vec3.unitY);
|
||||
}
|
||||
|
||||
if (typeof params.radius === 'number') {
|
||||
Vec3.set(EllipsoidState.radius, params.radius, params.radius, params.radius);
|
||||
} else if (params.radius) {
|
||||
Vec3.copy(EllipsoidState.radius, params.radius as any as Vec3);
|
||||
} else {
|
||||
const r = EllipsoidState.sphere.radius;
|
||||
Vec3.set(EllipsoidState.radius, r, r, r);
|
||||
}
|
||||
|
||||
if (typeof params.radius_extent === 'number') {
|
||||
Vec3.set(EllipsoidState.extent, params.radius_extent, params.radius_extent, params.radius_extent);
|
||||
} else if (params.radius_extent) {
|
||||
Vec3.copy(EllipsoidState.extent, params.radius_extent as any as Vec3);
|
||||
} else {
|
||||
Vec3.set(EllipsoidState.extent, 0, 0, 0);
|
||||
}
|
||||
|
||||
Vec3.add(EllipsoidState.radius, EllipsoidState.radius, EllipsoidState.extent);
|
||||
|
||||
const { mesh, groups } = state;
|
||||
|
||||
mesh.currentGroup = groups.allocateSingle(node);
|
||||
groups.updateColor(mesh.currentGroup, params.color);
|
||||
groups.updateTooltip(mesh.currentGroup, params.tooltip);
|
||||
|
||||
Vec3.normalize(EllipsoidState.majorAxis, EllipsoidState.majorAxis);
|
||||
Vec3.normalize(EllipsoidState.minorAxis, EllipsoidState.minorAxis);
|
||||
Vec3.cross(EllipsoidState.up, EllipsoidState.majorAxis, EllipsoidState.minorAxis);
|
||||
|
||||
addEllipsoid(mesh, EllipsoidState.centerPos, EllipsoidState.up, EllipsoidState.minorAxis, EllipsoidState.radius, 3);
|
||||
}
|
||||
|
||||
|
||||
const BoxState = {
|
||||
center: Vec3.zero(),
|
||||
boundary: Box3D.zero(),
|
||||
size: Vec3.zero(),
|
||||
cage: BoxCage(),
|
||||
translationXform: Mat4.identity(),
|
||||
scaleXform: Mat4.identity(),
|
||||
xform: Mat4.identity(),
|
||||
};
|
||||
|
||||
function addBoxMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'box'>) {
|
||||
if (!params.show_edges && !params.show_faces) return;
|
||||
|
||||
resolvePosition(context, params.center, BoxState.center, undefined, BoxState.boundary);
|
||||
if (params.extent) {
|
||||
Box3D.expand(BoxState.boundary, BoxState.boundary, params.extent as unknown as Vec3);
|
||||
}
|
||||
|
||||
if (Box3D.volume(BoxState.boundary) < 1e-3) return;
|
||||
|
||||
const { mesh, groups } = state;
|
||||
|
||||
Mat4.fromScaling(BoxState.scaleXform, Box3D.size(BoxState.size, BoxState.boundary));
|
||||
Mat4.fromTranslation(BoxState.translationXform, BoxState.center);
|
||||
Mat4.mul(BoxState.xform, BoxState.translationXform, BoxState.scaleXform);
|
||||
|
||||
if (params.show_faces) {
|
||||
mesh.currentGroup = groups.allocateSingle(node);
|
||||
groups.updateColor(mesh.currentGroup, params.face_color);
|
||||
groups.updateTooltip(mesh.currentGroup, params.tooltip);
|
||||
MeshBuilder.addPrimitive(mesh, BoxState.xform, Box());
|
||||
}
|
||||
|
||||
if (params.show_edges) {
|
||||
mesh.currentGroup = groups.allocateSingle(node);
|
||||
groups.updateColor(mesh.currentGroup, params.edge_color);
|
||||
groups.updateTooltip(mesh.currentGroup, params.tooltip);
|
||||
MeshBuilder.addCage(mesh, BoxState.xform, BoxCage(), params.edge_radius, 2, 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Table } from '../../../mol-data/db';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex } from '../../../mol-model/structure';
|
||||
import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex, StructureElement } from '../../../mol-model/structure';
|
||||
import { Expression } from '../../../mol-script/language/expression';
|
||||
import { arrayExtend, filterInPlace, range } from '../../../mol-util/array';
|
||||
import { AtomRanges } from './atom-ranges';
|
||||
@@ -256,71 +256,19 @@ function matchesRange<T>(requiredMin: T | undefined | null, requiredMax: T | und
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Convert an annotation row into a MolScript expression */
|
||||
export function rowToExpression(row: MVSAnnotationRow): Expression {
|
||||
const { and } = MS.core.logic;
|
||||
const { eq, gre: gte, lte } = MS.core.rel;
|
||||
const { macromolecular } = MS.struct.atomProperty;
|
||||
const propTests: Partial<Record<string, Expression>> = {};
|
||||
|
||||
if (isDefined(row.label_entity_id)) {
|
||||
propTests['entity-test'] = eq([macromolecular.label_entity_id(), row.label_entity_id]);
|
||||
}
|
||||
|
||||
const chainTests: Expression[] = [];
|
||||
if (isDefined(row.label_asym_id)) chainTests.push(eq([macromolecular.label_asym_id(), row.label_asym_id]));
|
||||
if (isDefined(row.auth_asym_id)) chainTests.push(eq([macromolecular.auth_asym_id(), row.auth_asym_id]));
|
||||
|
||||
if (chainTests.length === 1) {
|
||||
propTests['chain-test'] = chainTests[0];
|
||||
} else if (chainTests.length > 1) {
|
||||
propTests['chain-test'] = and(chainTests);
|
||||
}
|
||||
|
||||
const residueTests: Expression[] = [];
|
||||
if (isDefined(row.label_seq_id)) residueTests.push(eq([macromolecular.label_seq_id(), row.label_seq_id]));
|
||||
if (isDefined(row.auth_seq_id)) residueTests.push(eq([macromolecular.auth_seq_id(), row.auth_seq_id]));
|
||||
if (isDefined(row.pdbx_PDB_ins_code)) residueTests.push(eq([macromolecular.pdbx_PDB_ins_code(), row.pdbx_PDB_ins_code]));
|
||||
if (isDefined(row.beg_label_seq_id)) residueTests.push(gte([macromolecular.label_seq_id(), row.beg_label_seq_id]));
|
||||
if (isDefined(row.end_label_seq_id)) residueTests.push(lte([macromolecular.label_seq_id(), row.end_label_seq_id]));
|
||||
if (isDefined(row.beg_auth_seq_id)) residueTests.push(gte([macromolecular.auth_seq_id(), row.beg_auth_seq_id]));
|
||||
if (isDefined(row.end_auth_seq_id)) residueTests.push(lte([macromolecular.auth_seq_id(), row.end_auth_seq_id]));
|
||||
if (residueTests.length === 1) {
|
||||
propTests['residue-test'] = residueTests[0];
|
||||
} else if (residueTests.length > 1) {
|
||||
propTests['residue-test'] = and(residueTests);
|
||||
}
|
||||
|
||||
const atomTests: Expression[] = [];
|
||||
if (isDefined(row.atom_id)) atomTests.push(eq([macromolecular.id(), row.atom_id]));
|
||||
if (isDefined(row.atom_index)) atomTests.push(eq([MS.struct.atomProperty.core.sourceIndex(), row.atom_index]));
|
||||
if (isDefined(row.label_atom_id)) atomTests.push(eq([macromolecular.label_atom_id(), row.label_atom_id]));
|
||||
if (isDefined(row.auth_atom_id)) atomTests.push(eq([macromolecular.auth_atom_id(), row.auth_atom_id]));
|
||||
if (isDefined(row.type_symbol)) atomTests.push(eq([MS.struct.atomProperty.core.elementSymbol(), row.type_symbol.toUpperCase()]));
|
||||
if (atomTests.length === 1) {
|
||||
propTests['atom-test'] = atomTests[0];
|
||||
} else if (atomTests.length > 1) {
|
||||
propTests['atom-test'] = and(atomTests);
|
||||
}
|
||||
|
||||
return MS.struct.generator.atomGroups(propTests);
|
||||
return StructureElement.Schema.toExpression(row);
|
||||
}
|
||||
|
||||
/** Convert multiple annotation rows into a MolScript expression.
|
||||
* (with union semantics, i.e. an atom qualifies if it qualifies for at least one of the rows) */
|
||||
export function rowsToExpression(rows: readonly MVSAnnotationRow[]): Expression {
|
||||
if (rows.length === 1) return rowToExpression(rows[0]);
|
||||
return unionExpression(rows.map(rowToExpression));
|
||||
return StructureElement.Schema.toExpression({
|
||||
items: rows as StructureElement.SchemaItem[]
|
||||
});
|
||||
}
|
||||
|
||||
/** Create MolScript expression covering the set union of the given expressions */
|
||||
function unionExpression(expressions: Expression[]): Expression {
|
||||
return MS.struct.combinator.merge(expressions.map(e => MS.struct.modifier.union([e])));
|
||||
}
|
||||
|
||||
|
||||
/** Data structure for an array divided into contiguous groups */
|
||||
interface GroupedArray<T> {
|
||||
/** Number of groups */
|
||||
|
||||
17
src/extensions/mvs/load-extensions/is-hidden-custom-state.ts
Normal file
17
src/extensions/mvs/load-extensions/is-hidden-custom-state.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MolstarLoadingExtension } from '../load';
|
||||
|
||||
export const IsHiddenCustomStateExtension: MolstarLoadingExtension<{}> = {
|
||||
id: 'ww-pdb/is-hidden-custom-state',
|
||||
description: 'Allow updating initial visibility of nodes',
|
||||
createExtensionContext: () => ({}),
|
||||
action: (updateTarget, node) => {
|
||||
if (!node.custom || !node.custom?.is_hidden) return;
|
||||
updateTarget.update.to(updateTarget.selector).updateState({ isHidden: true });
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -125,13 +125,16 @@ export interface UpdateTarget {
|
||||
readonly selector: StateObjectSelector,
|
||||
readonly targetManager: TargetManager,
|
||||
readonly mvsDependencyRefs: Set<string>,
|
||||
|
||||
readonly transformer?: StateTransformer,
|
||||
readonly transformParams?: any,
|
||||
}
|
||||
export const UpdateTarget = {
|
||||
/** Create a new update, with `selector` pointing to the root. */
|
||||
create(plugin: PluginContext, replaceExisting: boolean): UpdateTarget {
|
||||
const update = plugin.build();
|
||||
const msTarget = update.toRoot().selector;
|
||||
return { update, selector: msTarget, targetManager: new TargetManager(plugin, replaceExisting), mvsDependencyRefs: new Set() };
|
||||
const msTarget = update.toRoot();
|
||||
return { update, selector: msTarget.selector, targetManager: new TargetManager(plugin, replaceExisting), mvsDependencyRefs: new Set() };
|
||||
},
|
||||
/** Add a child node to `target.selector`, return a new `UpdateTarget` pointing to the new child. */
|
||||
apply<A extends StateObject, B extends StateObject, P extends {}>(target: UpdateTarget, transformer: StateTransformer<A, B, P>, params?: Partial<P>, options?: Partial<StateTransform.Options>): UpdateTarget {
|
||||
@@ -141,8 +144,8 @@ export const UpdateTarget = {
|
||||
refSuffix += `:${reprType}`;
|
||||
}
|
||||
const ref = target.targetManager.getChildRef(target.selector, refSuffix);
|
||||
const msResult = target.update.to(target.selector).apply(transformer, params, { ...options, ref }).selector;
|
||||
const result: UpdateTarget = { ...target, selector: msResult, mvsDependencyRefs: new Set() };
|
||||
const apply = target.update.to(target.selector).apply(transformer, params, { ...options, ref });
|
||||
const result: UpdateTarget = { ...target, selector: apply.selector, mvsDependencyRefs: new Set(), transformer, transformParams: params };
|
||||
target.targetManager.allTargets.push(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mat3, Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { StructureComponentParams } from '../../mol-plugin-state/helpers/structure-component';
|
||||
import { StructureFromModel, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { arrayDistinct } from '../../mol-util/array';
|
||||
import { canonicalJsonString } from '../../mol-util/json';
|
||||
@@ -27,6 +29,7 @@ import { MolstarLoadingContext } from './load';
|
||||
import { Subtree, getChildren } from './tree/generic/tree-schema';
|
||||
import { dfs, formatObject } from './tree/generic/tree-utils';
|
||||
import { MolstarKind, MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree } from './tree/molstar/molstar-tree';
|
||||
import { DefaultColor } from './tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri'] satisfies MolstarKind[]);
|
||||
@@ -35,10 +38,6 @@ export type AnnotationFromUriKind = ElementOfSet<typeof AnnotationFromUriKinds>
|
||||
export const AnnotationFromSourceKinds = new Set(['color_from_source', 'component_from_source', 'label_from_source', 'tooltip_from_source'] satisfies MolstarKind[]);
|
||||
export type AnnotationFromSourceKind = ElementOfSet<typeof AnnotationFromSourceKinds>
|
||||
|
||||
/** Color to be used e.g. for representations without 'color' node */
|
||||
export const DefaultColor = 'white';
|
||||
|
||||
|
||||
/** Return a 4x4 matrix representing a rotation followed by a translation */
|
||||
export function transformFromRotationTranslation(rotation: number[] | null | undefined, translation: number[] | null | undefined): Mat4 {
|
||||
if (rotation && rotation.length !== 9) throw new Error(`'rotation' param for 'transform' node must be array of 9 elements, found ${rotation}`);
|
||||
@@ -285,19 +284,30 @@ export function componentFromXProps(node: MolstarNode<'component_from_uri' | 'co
|
||||
/** Create props for `StructureRepresentation3D` transformer from a representation node. */
|
||||
export function representationProps(node: MolstarSubtree<'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
const alpha = alphaForNode(node);
|
||||
switch (node.params.type) {
|
||||
const params = node.params;
|
||||
switch (params.type) {
|
||||
case 'cartoon':
|
||||
return {
|
||||
type: { name: 'cartoon', params: { alpha } },
|
||||
type: { name: 'cartoon', params: { alpha, tubularHelices: params.tubular_helices } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'ball_and_stick':
|
||||
return {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.5, sizeAspectRatio: 0.5, alpha } },
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: (params.size_factor ?? 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
};
|
||||
case 'spacefill':
|
||||
return {
|
||||
type: { name: 'spacefill', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
case 'carbohydrate':
|
||||
return {
|
||||
type: { name: 'carbohydrate', params: { alpha, sizeFactor: params.size_factor ?? 1 } },
|
||||
};
|
||||
case 'surface':
|
||||
return {
|
||||
type: { name: 'molecular-surface', params: { alpha } },
|
||||
sizeTheme: { name: 'physical', params: { scale: 1 } },
|
||||
type: { name: 'molecular-surface', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
@@ -305,7 +315,7 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
|
||||
}
|
||||
|
||||
/** Create value for `type.params.alpha` prop for `StructureRepresentation3D` transformer from a representation node based on 'opacity' nodes in its subtree. */
|
||||
export function alphaForNode(node: MolstarSubtree<'representation'>): number {
|
||||
export function alphaForNode(node: MolstarSubtree<'representation' | 'volume_representation'>): number {
|
||||
const children = getChildren(node).filter(c => c.kind === 'opacity');
|
||||
if (children.length > 0) {
|
||||
return children[children.length - 1].params.opacity;
|
||||
@@ -313,8 +323,14 @@ export function alphaForNode(node: MolstarSubtree<'representation'>): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function hasMolStarUseDefaultColoring(node: MolstarNode): boolean {
|
||||
if (!node.custom) return false;
|
||||
return 'molstar_use_default_coloring' in node.custom || 'molstar_color_theme_name' in node.custom;
|
||||
}
|
||||
|
||||
/** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */
|
||||
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] {
|
||||
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] | undefined {
|
||||
if (node?.kind === 'representation') {
|
||||
const children = getChildren(node).filter(c => c.kind === 'color' || c.kind === 'color_from_uri' || c.kind === 'color_from_source') as MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>[];
|
||||
if (children.length === 0) {
|
||||
@@ -322,12 +338,23 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
name: 'uniform',
|
||||
params: { value: decodeColor(DefaultColor) },
|
||||
};
|
||||
} else if (children.length === 1 && hasMolStarUseDefaultColoring(children[0])) {
|
||||
if (children[0].custom?.molstar_use_default_coloring) return undefined;
|
||||
const custom = children[0].custom;
|
||||
return {
|
||||
name: custom?.molstar_color_theme_name ?? undefined,
|
||||
params: custom?.molstar_color_theme_params ?? {},
|
||||
};
|
||||
} else if (children.length === 1 && appliesColorToWholeRepr(children[0])) {
|
||||
return colorThemeForNode(children[0], context);
|
||||
} else {
|
||||
const layers: MultilayerColorThemeProps['layers'] = children.map(
|
||||
c => ({ theme: colorThemeForNode(c, context), selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) })
|
||||
);
|
||||
c => {
|
||||
const theme = colorThemeForNode(c, context);
|
||||
if (!theme) return undefined;
|
||||
return { theme, selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) };
|
||||
}
|
||||
).filter(t => !!t);
|
||||
return {
|
||||
name: MultilayerColorThemeName,
|
||||
params: { layers },
|
||||
@@ -389,3 +416,36 @@ export function makeNearestReprMap(root: MolstarTree) {
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Create props for `VolumeRepresentation3D` transformer from a representation node. */
|
||||
export function volumeRepresentationProps(node: MolstarSubtree<'volume_representation'>): Partial<StateTransformer.Params<VolumeRepresentation3D>> {
|
||||
const alpha = alphaForNode(node);
|
||||
const params = node.params;
|
||||
switch (params.type) {
|
||||
case 'isosurface':
|
||||
const isoValue = typeof params.absolute_isovalue === 'number' ? Volume.IsoValue.absolute(params.absolute_isovalue) : Volume.IsoValue.relative(params.relative_isovalue ?? 0);
|
||||
const visuals: ('wireframe' | 'solid')[] = [];
|
||||
if (params.show_wireframe) visuals.push('wireframe');
|
||||
if (params.show_faces) visuals.push('solid');
|
||||
return {
|
||||
type: { name: 'isosurface', params: { alpha, isoValue, visuals } },
|
||||
};
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
}
|
||||
|
||||
/** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */
|
||||
export function volumeColorThemeForNode(node: MolstarSubtree<'volume_representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<VolumeRepresentation3D>['colorTheme'] | undefined {
|
||||
if (node?.kind !== 'volume_representation') return undefined;
|
||||
|
||||
const children = getChildren(node).filter(c => c.kind === 'color') as MolstarNode<'color'>[];
|
||||
if (children.length === 0) {
|
||||
return {
|
||||
name: 'uniform',
|
||||
params: { value: decodeColor(DefaultColor) },
|
||||
};
|
||||
} if (children.length === 1) {
|
||||
return colorThemeForNode(children[0], context);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -7,9 +7,11 @@
|
||||
*/
|
||||
|
||||
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
|
||||
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { Download, ParseCcp4, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { VolumeFromCcp4, VolumeFromDensityServerCif } from '../../mol-plugin-state/transforms/volume';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
@@ -22,9 +24,10 @@ import { CustomLabelProps, CustomLabelRepresentationProvider } from './component
|
||||
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
|
||||
import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-prop';
|
||||
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData } from './components/primitives';
|
||||
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
|
||||
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
|
||||
import { LoadingActions, LoadingExtension, loadTree, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps } from './load-helpers';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, SnapshotMetadata } from './mvs-data';
|
||||
import { validateTree } from './tree/generic/tree-schema';
|
||||
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
|
||||
@@ -32,13 +35,24 @@ import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTre
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
export interface MVSLoadOptions {
|
||||
replaceExisting?: boolean,
|
||||
keepCamera?: boolean,
|
||||
keepSnapshotCamera?: boolean,
|
||||
extensions?: MolstarLoadingExtension<any>[],
|
||||
sanityChecks?: boolean,
|
||||
sourceUrl?: string,
|
||||
doNotReportErrors?: boolean
|
||||
}
|
||||
|
||||
/** Load a MolViewSpec (MVS) tree into the Mol* plugin.
|
||||
* If `options.replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state.
|
||||
* If `options.keepCamera`, ignore any camera positioning from the MVS state and keep the current camera position instead.
|
||||
* If `options.keepSnapshotCamera`, ignore any camera positioning when generating snapshots.
|
||||
* If `options.sanityChecks`, run some sanity checks and print potential issues to the console.
|
||||
* If `options.extensions` is provided, apply specified set of MVS-loading extensions (not a part of standard MVS specification); default: apply all builtin extensions; use `extensions: []` to avoid applying builtin extensions.
|
||||
* `options.sourceUrl` serves as the base for resolving relative URLs/URIs and may itself be relative to the window URL. */
|
||||
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[], sanityChecks?: boolean, sourceUrl?: string, doNotReportErrors?: boolean } = {}) {
|
||||
export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
plugin.errorContext.clear('mvs');
|
||||
try {
|
||||
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
|
||||
@@ -113,13 +127,15 @@ async function loadMolstarTree(plugin: PluginContext, tree: MolstarTree, options
|
||||
}
|
||||
}
|
||||
|
||||
function molstarTreeToEntry(plugin: PluginContext, tree: MolstarTree, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }, options?: { replaceExisting?: boolean, keepCamera?: boolean }) {
|
||||
function molstarTreeToEntry(plugin: PluginContext, tree: MolstarTree, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }, options?: { replaceExisting?: boolean, keepCamera?: boolean, keepSnapshotCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
const context = MolstarLoadingContext.create();
|
||||
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, options);
|
||||
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { ...options, extensions: options?.extensions ?? BuiltinLoadingExtensions });
|
||||
snapshot.canvas3d = {
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas) : undefined,
|
||||
};
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
|
||||
if (!options?.keepSnapshotCamera) {
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
|
||||
}
|
||||
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
|
||||
|
||||
const entryParams: PluginStateSnapshotManager.EntryParams = {
|
||||
@@ -172,6 +188,8 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
} else if (format === 'pdb') {
|
||||
return updateParent;
|
||||
} else if (format === 'map') {
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
@@ -272,6 +290,22 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
colorTheme: colorThemeForNode(node, context),
|
||||
});
|
||||
},
|
||||
volume(updateParent: UpdateTarget, node: MolstarNode<'volume'>): UpdateTarget | undefined {
|
||||
if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Ccp4)) {
|
||||
return UpdateTarget.apply(updateParent, VolumeFromCcp4, {});
|
||||
} else if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Cif)) {
|
||||
return UpdateTarget.apply(updateParent, VolumeFromDensityServerCif, { blockHeader: node.params.channel_id || undefined });
|
||||
} else {
|
||||
console.error(`Unsupported volume format`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
volume_representation(updateParent: UpdateTarget, node: MolstarNode<'volume_representation'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
return UpdateTarget.apply(updateParent, VolumeRepresentation3D, {
|
||||
...volumeRepresentationProps(node),
|
||||
colorTheme: volumeColorThemeForNode(node, context),
|
||||
});
|
||||
},
|
||||
color: undefined, // No action needed, already loaded in `representation`
|
||||
color_from_uri: undefined, // No action needed, already loaded in `representation`
|
||||
color_from_source: undefined, // No action needed, already loaded in `representation`
|
||||
@@ -321,4 +355,5 @@ export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<Molsta
|
||||
|
||||
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
|
||||
NonCovalentInteractionsExtension,
|
||||
IsHiddenCustomStateExtension,
|
||||
];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
@@ -17,6 +18,7 @@ export const ParseFormatMvsToMolstar = {
|
||||
mmcif: { format: 'cif', is_binary: false },
|
||||
bcif: { format: 'cif', is_binary: true },
|
||||
pdb: { format: 'pdb', is_binary: false },
|
||||
map: { format: 'map', is_binary: true },
|
||||
} satisfies { [p in ParseFormatT]: { format: MolstarParseFormatT, is_binary: boolean } };
|
||||
|
||||
|
||||
@@ -71,6 +73,7 @@ const StructureFormatExtensions: Record<ParseFormatT, (FileExtension | '*')[]> =
|
||||
mmcif: ['.cif', '.mmif'],
|
||||
bcif: ['.bcif'],
|
||||
pdb: ['.pdb', '.ent'],
|
||||
map: ['.map', '.ccp4', '.mrc', '.mrcs'],
|
||||
};
|
||||
|
||||
/** Run some sanity check on a MVSTree. Return a list of potential problems (`undefined` if there are none) */
|
||||
|
||||
@@ -2,17 +2,29 @@
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MVSData } from '../../../mvs-data';
|
||||
import { builderDemo } from '../mvs-builder';
|
||||
import { builderDemo, createMVSBuilder } from '../mvs-builder';
|
||||
|
||||
|
||||
describe('mvs-builder', () => {
|
||||
it('mvs-builder demo works', async () => {
|
||||
it('mvs-builder demo works', () => {
|
||||
const mvsData = builderDemo();
|
||||
expect(typeof mvsData.metadata.version).toEqual('string');
|
||||
expect(typeof mvsData.metadata.timestamp).toEqual('string');
|
||||
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('volume builder works', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
.download({ url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/box/-22.367,-33.367,-21.634/-7.106,-10.042,-0.937?detail=3' })
|
||||
.parse({ format: 'bcif' })
|
||||
.volume({ channel_id: '2FO-FC' })
|
||||
.representation({ type: 'isosurface', absolute_isovalue: 1, show_wireframe: true, show_faces: false });
|
||||
const state = builder.getState();
|
||||
expect(state).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { deepClone, pickObjectKeys } from '../../../../mol-util/object';
|
||||
@@ -151,6 +152,10 @@ export class Parse extends _Base<'parse'> {
|
||||
ref: params.ref,
|
||||
}));
|
||||
}
|
||||
/** Add a 'volume' node representing raw volume data */
|
||||
volume(params: MVSNodeParams<'volume'> & CustomAndRef = {}): Volume {
|
||||
return new Volume(this._root, this.addChild('volume', params));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,10 +253,37 @@ export class Representation extends _Base<'representation'> {
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
|
||||
export class Volume extends _Base<'volume'> implements FocusMixin {
|
||||
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
|
||||
representation(params: Partial<MVSNodeParams<'volume_representation'>> & CustomAndRef = {}): VolumeRepresentation {
|
||||
const fullParams: MVSNodeParams<'volume_representation'> = { ...params, type: params.type ?? 'isosurface' };
|
||||
return new VolumeRepresentation(this._root, this.addChild('volume_representation', fullParams));
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'volume_representation' node */
|
||||
export class VolumeRepresentation extends _Base<'volume_representation'> implements FocusMixin {
|
||||
/** Add a 'color' node and return builder pointing back to the representation node. 'color' node instructs to apply color to a visual representation. */
|
||||
color(params: MVSNodeParams<'color'> & CustomAndRef): VolumeRepresentation {
|
||||
this.addChild('color', params);
|
||||
return this;
|
||||
}
|
||||
/** Add an 'opacity' node and return builder pointing back to the representation node. 'opacity' node instructs to customize opacity/transparency of a visual representation. */
|
||||
opacity(params: MVSNodeParams<'opacity'> & CustomAndRef): VolumeRepresentation {
|
||||
this.addChild('opacity', params);
|
||||
return this;
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
}
|
||||
|
||||
|
||||
type MVSPrimitiveSubparams<TKind extends MVSNodeParams<'primitive'>['kind']> = Omit<Extract<MVSNodeParams<'primitive'>, { kind: TKind }>, 'kind'>;
|
||||
|
||||
/** MVS builder pointing to a 'primitives' node */
|
||||
class Primitives extends _Base<'primitives'> implements FocusMixin {
|
||||
export class Primitives extends _Base<'primitives'> implements FocusMixin {
|
||||
/** Construct custom meshes/shapes in a low-level fashion by providing vertices and indices. */
|
||||
mesh(params: MVSPrimitiveSubparams<'mesh'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'mesh', ...params });
|
||||
@@ -267,6 +299,11 @@ class Primitives extends _Base<'primitives'> implements FocusMixin {
|
||||
this.addChild('primitive', { kind: 'tube', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines an arrow. */
|
||||
arrow(params: MVSPrimitiveSubparams<'arrow'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'arrow', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a tube, connecting a start and an end point, with label containing distance between start and end. */
|
||||
distance(params: MVSPrimitiveSubparams<'distance_measurement'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'distance_measurement', ...params });
|
||||
@@ -277,6 +314,21 @@ class Primitives extends _Base<'primitives'> implements FocusMixin {
|
||||
this.addChild('primitive', { kind: 'label', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines an ellipse. */
|
||||
ellipse(params: MVSPrimitiveSubparams<'ellipse'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'ellipse', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines an ellipsoid */
|
||||
ellipsoid(params: MVSPrimitiveSubparams<'ellipsoid'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'ellipsoid', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a box. */
|
||||
box(params: MVSPrimitiveSubparams<'box'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'box', ...params });
|
||||
return this;
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str } from '../generic/field-schema';
|
||||
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str, union } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
import { ColorT, FloatList, IntList, PrimitivePositionT } from './param-types';
|
||||
import { ColorT, FloatList, IntList, PrimitivePositionT, Vector3 } from './param-types';
|
||||
|
||||
|
||||
const _TubeBase = {
|
||||
@@ -75,6 +75,39 @@ const TubeParams = {
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
};
|
||||
|
||||
const ArrowParams = {
|
||||
/** Start point of the tube. */
|
||||
start: RequiredField(PrimitivePositionT, 'Start point of the arrow.'),
|
||||
/** End point of the tube. */
|
||||
end: OptionalField(nullable(PrimitivePositionT), null, 'End point of the arrow.'),
|
||||
/** If specified, the endpoint is computed as start + direction. */
|
||||
direction: OptionalField(nullable(Vector3), null, 'If specified, the endpoint is computed as start + direction.'),
|
||||
/** Length of the arrow. If unset, the distance between start and end is used. */
|
||||
length: OptionalField(nullable(float), null, 'Length of the arrow. If unset, the distance between start and end is used.'),
|
||||
/** Draw a cap at the start of the arrow. */
|
||||
show_start_cap: OptionalField(bool, false, 'Draw a cap at the start of the arrow.'),
|
||||
/** Length of the start cap. */
|
||||
start_cap_length: OptionalField(float, 0.1, 'Length of the start cap.'),
|
||||
/** Radius of the start cap. */
|
||||
start_cap_radius: OptionalField(float, 0.1, 'Radius of the start cap.'),
|
||||
/** Draw an arrow at the end of the arrow. */
|
||||
show_end_cap: OptionalField(bool, false, 'Draw a cap at the end of the arrow.'),
|
||||
/** Height of the arrow at the end. */
|
||||
end_cap_length: OptionalField(float, 0.1, 'Length of the end cap.'),
|
||||
/** Radius of the arrow at the end. */
|
||||
end_cap_radius: OptionalField(float, 0.1, 'Radius of the end cap.'),
|
||||
/** Draw a tube connecting the start and end points. */
|
||||
show_tube: OptionalField(bool, true, 'Draw a tube connecting the start and end points.'),
|
||||
/** Tube radius (in Angstroms). */
|
||||
tube_radius: OptionalField(float, 0.05, 'Tube radius (in Angstroms).'),
|
||||
/** Length of each dash and gap between dashes. If not specified (null), draw full line. */
|
||||
tube_dash_length: OptionalField(nullable(float), null, 'Length of each dash and gap between dashes. If not specified (null), draw full line.'),
|
||||
/** Color of the tube. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the tube. If not specified, uses the parent primitives group `color`.'),
|
||||
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the arrow. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
};
|
||||
|
||||
const DistanceMeasurementParams = {
|
||||
..._TubeBase,
|
||||
/** Template used to construct the label. Use {{distance}} as placeholder for the distance. */
|
||||
@@ -89,6 +122,37 @@ const DistanceMeasurementParams = {
|
||||
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
|
||||
};
|
||||
|
||||
const AngleMeasurementParams = {
|
||||
/** Point A. */
|
||||
a: RequiredField(PrimitivePositionT, 'Point A.'),
|
||||
/** Point B. */
|
||||
b: RequiredField(PrimitivePositionT, 'Point B.'),
|
||||
/** Point C. */
|
||||
c: RequiredField(PrimitivePositionT, 'Point C.'),
|
||||
/** Template used to construct the label. Use {{angle}} as placeholder for the angle in radians. */
|
||||
label_template: OptionalField(str, '{{angle}}', 'Template used to construct the label. Use {{angle}} as placeholder for the angle in radians.'),
|
||||
/** Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min). */
|
||||
label_size: OptionalField(nullable(float), null, 'Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min).'),
|
||||
/** Scaling factor for relative size. */
|
||||
label_auto_size_scale: OptionalField(float, 0.33, 'Scaling factor for relative size.'),
|
||||
/** Minimum size for relative size. */
|
||||
label_auto_size_min: OptionalField(float, 0, 'Minimum size for relative size.'),
|
||||
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
|
||||
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
|
||||
/** Draw vectors between (a, b) and (b, c). */
|
||||
show_vector: OptionalField(bool, true, 'Draw vectors between (a, b) and (b, c).'),
|
||||
/** Color of the vectors. */
|
||||
vector_color: OptionalField(nullable(ColorT), null, 'Color of the vectors.'),
|
||||
/** Draw a filled circle section representing the angle. */
|
||||
show_section: OptionalField(bool, true, 'Draw a filled circle section representing the angle.'),
|
||||
/** Color of the angle section. If not specified, the primitives group color is used. */
|
||||
section_color: OptionalField(nullable(ColorT), null, 'Color of the angle section. If not specified, the primitives group color is used.'),
|
||||
/** Radius of the angle section. In angstroms. */
|
||||
section_radius: OptionalField(nullable(float), null, 'Radius of the angle section. In angstroms.'),
|
||||
/** Factor to scale the radius of the angle section. Ignored if section_radius is set. */
|
||||
section_radius_scale: OptionalField(float, 0.33, 'Factor to scale the radius of the angle section. Ignored if section_radius is set.'),
|
||||
};
|
||||
|
||||
const PrimitiveLabelParams = {
|
||||
/** Position of this label. */
|
||||
position: RequiredField(PrimitivePositionT, 'Position of this label.'),
|
||||
@@ -102,6 +166,73 @@ const PrimitiveLabelParams = {
|
||||
label_offset: OptionalField(float, 0, 'Camera-facing offset to prevent overlap with geometry.'),
|
||||
};
|
||||
|
||||
const EllipseParams = {
|
||||
/** Color of the primitive. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the ellipse. If not specified, uses the parent primitives group `color`.'),
|
||||
/** If true, ignores radius_minor/magnitude of the minor axis */
|
||||
as_circle: OptionalField(bool, false, 'If true, ignores radius_minor/magnitude of the minor axis.'),
|
||||
/** ellipse center. */
|
||||
center: RequiredField(PrimitivePositionT, 'The center of the ellipse.'),
|
||||
/** Major axis of this ellipse. */
|
||||
major_axis: OptionalField(nullable(Vector3), null, 'Major axis of this ellipse.'),
|
||||
/** Minor axis of this ellipse. */
|
||||
minor_axis: OptionalField(nullable(Vector3), null, 'Minor axis of this ellipse.'),
|
||||
/** Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center. */
|
||||
major_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center.'),
|
||||
/** Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center. */
|
||||
minor_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center.'),
|
||||
/** Radius of the major axis. If unset, the length of the major axis is used. */
|
||||
radius_major: OptionalField(nullable(float), null, 'Radius of the major axis. If unset, the length of the major axis is used.'),
|
||||
/** Radius of the minor axis. If unset, the length of the minor axis is used. */
|
||||
radius_minor: OptionalField(nullable(float), null, 'Radius of the minor axis. If unset, the length of the minor axis is used.'),
|
||||
/** Start of the arc. In radians */
|
||||
theta_start: OptionalField(float, 0, 'Start of the arc. In radians'),
|
||||
/** End of the arc. In radians */
|
||||
theta_end: OptionalField(float, 2 * Math.PI, 'End of the arc. In radians'),
|
||||
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
};
|
||||
|
||||
const EllipsoidParams = {
|
||||
/** Color of the primitive. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the ellipsoid. If not specified, uses the parent primitives group `color`.'),
|
||||
/** Ellipsoid center. */
|
||||
center: RequiredField(PrimitivePositionT, 'The center of the ellipsoid.'),
|
||||
/** Major axis of this ellipsoid. */
|
||||
major_axis: OptionalField(nullable(Vector3), null, 'Major axis of this ellipsoid.'),
|
||||
/** Minor axis of this ellipsoid. */
|
||||
minor_axis: OptionalField(nullable(Vector3), null, 'Minor axis of this ellipsoid.'),
|
||||
/** Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center. */
|
||||
major_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center.'),
|
||||
/** Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center. */
|
||||
minor_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center.'),
|
||||
/** Radii of the ellipsoid along each axis. */
|
||||
radius: OptionalField(nullable(union([Vector3, float])), null, 'Radii of the ellipsoid along each axis.'),
|
||||
/** Added to the radii of the ellipsoid along each axis. */
|
||||
radius_extent: OptionalField(nullable(union([Vector3, float])), null, 'Added to the radii of the ellipsoid along each axis.'),
|
||||
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
};
|
||||
|
||||
const BoxParams = {
|
||||
/** The center of the box. */
|
||||
center: RequiredField(PrimitivePositionT, 'The center of the box.'),
|
||||
/** The width, the height, and the depth of the box. Added to the bounding box determined by the center. */
|
||||
extent: OptionalField(nullable(Vector3), null, 'The width, the height, and the depth of the box. Added to the bounding box determined by the center.'),
|
||||
/** Determine whether to render the faces of the box. */
|
||||
show_faces: OptionalField(bool, true, 'Determine whether to render the faces of the box.'),
|
||||
/** Color of the box faces. */
|
||||
face_color: OptionalField(nullable(ColorT), null, 'Color of the box faces.'),
|
||||
/** Determine whether to render the edges of the box. */
|
||||
show_edges: OptionalField(bool, false, 'Determine whether to render the edges of the box.'),
|
||||
/** Radius of the box edges. In angstroms. */
|
||||
edge_radius: OptionalField(float, 0.1, 'Radius of the box edges. In angstroms.'),
|
||||
/** Color of the box edges. */
|
||||
edge_color: OptionalField(nullable(ColorT), null, 'Color of the edges.'),
|
||||
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
};
|
||||
|
||||
export const MVSPrimitiveParams = UnionParamsSchema(
|
||||
'kind',
|
||||
'Kind of geometrical primitive',
|
||||
@@ -109,7 +240,12 @@ export const MVSPrimitiveParams = UnionParamsSchema(
|
||||
'mesh': SimpleParamsSchema(MeshParams),
|
||||
'lines': SimpleParamsSchema(LinesParams),
|
||||
'tube': SimpleParamsSchema(TubeParams),
|
||||
'arrow': SimpleParamsSchema(ArrowParams),
|
||||
'distance_measurement': SimpleParamsSchema(DistanceMeasurementParams),
|
||||
'angle_measurement': SimpleParamsSchema(AngleMeasurementParams),
|
||||
'label': SimpleParamsSchema(PrimitiveLabelParams),
|
||||
'ellipse': SimpleParamsSchema(EllipseParams),
|
||||
'ellipsoid': SimpleParamsSchema(EllipsoidParams),
|
||||
'box': SimpleParamsSchema(BoxParams),
|
||||
},
|
||||
);
|
||||
|
||||
72
src/extensions/mvs/tree/mvs/mvs-tree-representations.ts
Normal file
72
src/extensions/mvs/tree/mvs/mvs-tree-representations.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, nullable, OptionalField } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
|
||||
const Cartoon = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Simplify corkscrew helices to tubes. */
|
||||
tubular_helices: OptionalField(bool, false, 'Simplify corkscrew helices to tubes.'),
|
||||
};
|
||||
|
||||
const BallAndStick = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls whether hydrogen atoms are drawn. */
|
||||
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
|
||||
};
|
||||
|
||||
const Spacefill = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls whether hydrogen atoms are drawn. */
|
||||
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
|
||||
};
|
||||
|
||||
const Carbohydrate = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
};
|
||||
|
||||
const Surface = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls whether hydrogen atoms are drawn. */
|
||||
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
|
||||
};
|
||||
|
||||
export const MVSRepresentationParams = UnionParamsSchema(
|
||||
'type',
|
||||
'Representation type',
|
||||
{
|
||||
cartoon: SimpleParamsSchema(Cartoon),
|
||||
ball_and_stick: SimpleParamsSchema(BallAndStick),
|
||||
spacefill: SimpleParamsSchema(Spacefill),
|
||||
carbohydrate: SimpleParamsSchema(Carbohydrate),
|
||||
surface: SimpleParamsSchema(Surface),
|
||||
},
|
||||
);
|
||||
|
||||
const VolumeIsoSurface = {
|
||||
/** Relative isovalue. */
|
||||
relative_isovalue: OptionalField(nullable(float), null, 'Relative isovalue.'),
|
||||
/** Absolute isovalue. Overrides `relative_isovalue`. */
|
||||
absolute_isovalue: OptionalField(nullable(float), null, 'Absolute isovalue. Overrides `relative_isovalue`.'),
|
||||
/** Show mesh wireframe. Defaults to false. */
|
||||
show_wireframe: OptionalField(bool, false, 'Show mesh wireframe. Defaults to false.'),
|
||||
/** Show mesh faces. Defaults to true. */
|
||||
show_faces: OptionalField(bool, true, 'Show mesh faces. Defaults to true.'),
|
||||
};
|
||||
|
||||
export const MVSVolumeRepresentationParams = UnionParamsSchema(
|
||||
'type',
|
||||
'Representation type',
|
||||
{
|
||||
'isosurface': SimpleParamsSchema(VolumeIsoSurface),
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -8,8 +8,9 @@
|
||||
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
|
||||
import { MVSRepresentationParams, MVSVolumeRepresentationParams } from './mvs-tree-representations';
|
||||
import { MVSPrimitiveParams } from './mvs-tree-primitives';
|
||||
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, RepresentationTypeT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
|
||||
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
|
||||
|
||||
|
||||
const _DataFromUriParams = {
|
||||
@@ -42,6 +43,9 @@ const _DataFromSourceParams = {
|
||||
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
};
|
||||
|
||||
/** Color to be used e.g. for representations without 'color' node */
|
||||
export const DefaultColor = 'white';
|
||||
|
||||
/** Schema for `MVSTree` (MolViewSpec tree) */
|
||||
export const MVSTreeSchema = TreeSchema({
|
||||
rootKind: 'root',
|
||||
@@ -142,18 +146,29 @@ export const MVSTreeSchema = TreeSchema({
|
||||
representation: {
|
||||
description: 'This node instructs to create a visual representation of a component.',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: MVSRepresentationParams,
|
||||
},
|
||||
/** This node instructs to create a volume from a parsed data resource. "Volume" refers to an internal representation of volumetric data without any visual representation. */
|
||||
volume: {
|
||||
description: 'This node instructs to create a volume from a parsed data resource. "Volume" refers to an internal representation of volumetric data without any visual representation.',
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Method of visual representation of the component. */
|
||||
type: RequiredField(RepresentationTypeT, 'Method of visual representation of the component.'),
|
||||
channel_id: OptionalField(nullable(str), null, 'Channel identifier (only applies when the input data contain multiple channels).'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to create a visual representation of a volume. */
|
||||
volume_representation: {
|
||||
description: 'This node instructs to create a visual representation of a volume.',
|
||||
parent: ['volume'],
|
||||
params: MVSVolumeRepresentationParams,
|
||||
},
|
||||
/** This node instructs to apply color to a visual representation. */
|
||||
color: {
|
||||
description: 'This node instructs to apply color to a visual representation.',
|
||||
parent: ['representation'],
|
||||
parent: ['representation', 'volume_representation'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
color: RequiredField(ColorT, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
color: OptionalField(ColorT, DefaultColor, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
/** Defines to what part of the representation this color should be applied. */
|
||||
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'all', 'Defines to what part of the representation this color should be applied.'),
|
||||
}),
|
||||
@@ -181,7 +196,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
/** This node instructs to apply opacity/transparency to a visual representation. */
|
||||
opacity: {
|
||||
description: 'This node instructs to apply opacity/transparency to a visual representation.',
|
||||
parent: ['representation'],
|
||||
parent: ['representation', 'volume_representation'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque. */
|
||||
opacity: RequiredField(float, 'Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque.'),
|
||||
@@ -248,7 +263,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
/** This node instructs to set the camera focus to a component (zoom in). */
|
||||
focus: {
|
||||
description: 'This node instructs to set the camera focus to a component (zoom in).',
|
||||
parent: ['root', 'component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri'],
|
||||
parent: ['root', 'component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri', 'volume', 'volume_representation'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Vector describing the direction of the view (camera position -> focused target). */
|
||||
direction: OptionalField(Vector3, [0, 0, -1], 'Vector describing the direction of the view (camera position -> focused target).'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -12,18 +12,18 @@ import { ColorNames } from '../../../../mol-util/color/names';
|
||||
|
||||
|
||||
/** `format` parameter values for `parse` node in MVS tree */
|
||||
export const ParseFormatT = literal('mmcif', 'bcif', 'pdb');
|
||||
export const ParseFormatT = literal('mmcif', 'bcif', 'pdb', 'map');
|
||||
export type ParseFormatT = ValueFor<typeof ParseFormatT>
|
||||
|
||||
/** `format` parameter values for `parse` node in Molstar tree */
|
||||
export const MolstarParseFormatT = literal('cif', 'pdb');
|
||||
export const MolstarParseFormatT = literal('cif', 'pdb', 'map');
|
||||
export type MolstarParseFormatT = ValueFor<typeof MolstarParseFormatT>
|
||||
|
||||
/** `kind` parameter values for `structure` node in MVS tree */
|
||||
export const StructureTypeT = literal('model', 'assembly', 'symmetry', 'symmetry_mates');
|
||||
|
||||
/** `selector` parameter values for `component` node in MVS tree */
|
||||
export const ComponentSelectorT = literal('all', 'polymer', 'protein', 'nucleic', 'branched', 'ligand', 'ion', 'water');
|
||||
export const ComponentSelectorT = literal('all', 'polymer', 'protein', 'nucleic', 'branched', 'ligand', 'ion', 'water', 'coarse');
|
||||
|
||||
/** `selector` parameter values for `component` node in MVS tree */
|
||||
export const ComponentExpressionT = iots.partial({
|
||||
@@ -45,9 +45,6 @@ export const ComponentExpressionT = iots.partial({
|
||||
});
|
||||
export type ComponentExpressionT = ValueFor<typeof ComponentExpressionT>
|
||||
|
||||
/** `type` parameter values for `representation` node in MVS tree */
|
||||
export const RepresentationTypeT = literal('ball_and_stick', 'cartoon', 'surface');
|
||||
|
||||
/** `schema` parameter values for `*_from_uri` and `*_from_source` nodes in MVS tree */
|
||||
export const SchemaT = literal('whole_structure', 'entity', 'chain', 'auth_chain', 'residue', 'auth_residue', 'residue_range', 'auth_residue_range', 'atom', 'auth_atom', 'all_atomic');
|
||||
|
||||
@@ -64,7 +61,7 @@ export const Matrix = list(float);
|
||||
/** Primitives-related types */
|
||||
export const PrimitiveComponentExpressionT = iots.partial({ structure_ref: str, expression_schema: SchemaT, expressions: list(ComponentExpressionT) });
|
||||
export type PrimitiveComponentExpressionT = ValueFor<typeof PrimitiveComponentExpressionT>
|
||||
export const PrimitivePositionT = iots.union([Vector3, ComponentExpressionT, list(PrimitiveComponentExpressionT)]);
|
||||
export const PrimitivePositionT = iots.union([Vector3, ComponentExpressionT, PrimitiveComponentExpressionT]);
|
||||
export type PrimitivePositionT = ValueFor<typeof PrimitivePositionT>
|
||||
|
||||
export const FloatList = list(float);
|
||||
@@ -93,6 +90,7 @@ export const ColorNamesT = literal(...Object.keys(ColorNames) as (keyof ColorNam
|
||||
|
||||
/** `color` parameter values for `color` node in MVS tree */
|
||||
export const ColorT = union([ColorNameT, HexColorT]);
|
||||
export type ColorT = ValueFor<typeof ColorT>
|
||||
|
||||
/** Type helpers */
|
||||
export function isVector3(x: any): x is Vector3 {
|
||||
|
||||
36
src/extensions/mvs/util.ts
Normal file
36
src/extensions/mvs/util.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector, StateTree } from '../../mol-state';
|
||||
|
||||
|
||||
/**
|
||||
* Queries all MolViewSpec references in the current state of the plugin.
|
||||
*/
|
||||
export function queryMVSRef(plugin: PluginContext, ref: string) {
|
||||
return plugin.state.data.selectQ(q => q.root.subtree().withTag(`mvs-ref:${ref}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mapping of all MolViewSpec references in the current state of the plugin.
|
||||
*/
|
||||
export function createMVSRefMap(plugin: PluginContext) {
|
||||
const tree = plugin.state.data.tree;
|
||||
const mapping = new Map<string, StateObjectSelector[]>();
|
||||
StateTree.doPreOrder(tree, tree.root, { mapping, plugin }, (n, _, s) => {
|
||||
if (!n.tags) return;
|
||||
for (const tag of n.tags) {
|
||||
if (!tag.startsWith('mvs-ref:')) continue;
|
||||
const mvsRef = tag.substring(8);
|
||||
const selector = new StateObjectSelector(n.ref, s.plugin.state.data);
|
||||
if (s.mapping.has(mvsRef)) s.mapping.get(mvsRef)!.push(selector);
|
||||
else s.mapping.set(mvsRef, [selector]);
|
||||
}
|
||||
});
|
||||
|
||||
return mapping;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { Color } from '../../../mol-util/color';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
|
||||
const ValidationColors = [
|
||||
Color.fromRgb(170, 170, 170), // not applicable
|
||||
@@ -90,7 +91,7 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
|
||||
export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params, 'pdbe-structure-quality-report'> = {
|
||||
name: 'pdbe-structure-quality-report',
|
||||
label: 'Structure Quality Report',
|
||||
category: ColorTheme.Category.Validation,
|
||||
category: ColorThemeCategory.Validation,
|
||||
factory: StructureQualityReportColorTheme,
|
||||
getParams: ctx => {
|
||||
const issueTypes = StructureQualityReport.getIssueTypes(ctx.structure);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-mod
|
||||
import { Location } from '../../../../mol-model/location';
|
||||
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
|
||||
import { ValidationReportProvider, ValidationReport } from '../prop';
|
||||
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC);
|
||||
|
||||
@@ -70,7 +71,7 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
|
||||
export const DensityFitColorThemeProvider: ColorTheme.Provider<{}, ValidationReport.Tag.DensityFit> = {
|
||||
name: ValidationReport.Tag.DensityFit,
|
||||
label: 'Density Fit',
|
||||
category: ColorTheme.Category.Validation,
|
||||
category: ColorThemeCategory.Validation,
|
||||
factory: DensityFitColorTheme,
|
||||
getParams: () => ({}),
|
||||
defaultValues: PD.getDefaultValues({}),
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ValidationReportProvider, ValidationReport } from '../prop';
|
||||
import { TableLegend } from '../../../../mol-util/legend';
|
||||
import { PolymerType } from '../../../../mol-model/structure/model/types';
|
||||
import { SetUtils } from '../../../../mol-util/set';
|
||||
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
|
||||
|
||||
const DefaultColor = Color(0x909090);
|
||||
|
||||
@@ -108,7 +109,7 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQualityColorThemeParams, ValidationReport.Tag.GeometryQuality> = {
|
||||
name: ValidationReport.Tag.GeometryQuality,
|
||||
label: 'Geometry Quality',
|
||||
category: ColorTheme.Category.Validation,
|
||||
category: ColorThemeCategory.Validation,
|
||||
factory: GeometryQualityColorTheme,
|
||||
getParams: getGeometricQualityColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(getGeometricQualityColorThemeParams({})),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-mod
|
||||
import { Location } from '../../../../mol-model/location';
|
||||
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
|
||||
import { ValidationReportProvider, ValidationReport } from '../prop';
|
||||
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC);
|
||||
|
||||
@@ -61,7 +62,7 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
|
||||
export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}, ValidationReport.Tag.RandomCoilIndex> = {
|
||||
name: ValidationReport.Tag.RandomCoilIndex,
|
||||
label: 'Random Coil Index',
|
||||
category: ColorTheme.Category.Validation,
|
||||
category: ColorThemeCategory.Validation,
|
||||
factory: RandomCoilIndexColorTheme,
|
||||
getParams: () => ({}),
|
||||
defaultValues: PD.getDefaultValues({}),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { SbNcbrPartialChargesPropertyProvider } from './property';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
|
||||
const Colors = {
|
||||
Bond: Color(0xffffff),
|
||||
@@ -133,7 +134,7 @@ PartialChargesThemeParams,
|
||||
> = {
|
||||
label: 'SB NCBR Partial Charges',
|
||||
name: 'sb-ncbr-partial-charges',
|
||||
category: ColorTheme.Category.Atom,
|
||||
category: ColorThemeCategory.Atom,
|
||||
factory: PartialChargesColorTheme,
|
||||
getParams: getPartialChargesThemeParams,
|
||||
defaultValues: PD.getDefaultValues(PartialChargesThemeParams),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -1070,7 +1070,10 @@ namespace Canvas3D {
|
||||
if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
|
||||
if (props.pickPadding !== undefined) p.pickPadding = props.pickPadding;
|
||||
if (props.pickPadding !== undefined) {
|
||||
p.pickPadding = props.pickPadding;
|
||||
pickHelper.setPickPadding(p.pickPadding);
|
||||
}
|
||||
if (props.userInteractionReleaseMs !== undefined) p.userInteractionReleaseMs = props.userInteractionReleaseMs;
|
||||
if (props.viewport !== undefined) {
|
||||
const doNotUpdate = p.viewport === props.viewport ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -247,6 +247,18 @@ export class PickHelper {
|
||||
|
||||
setViewport(x: number, y: number, width: number, height: number) {
|
||||
Viewport.set(this.viewport, x, y, width, height);
|
||||
this.update();
|
||||
}
|
||||
|
||||
setPickPadding(pickPadding: number) {
|
||||
if (this.pickPadding !== pickPadding) {
|
||||
this.pickPadding = pickPadding;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
const { x, y, width, height } = this.viewport;
|
||||
|
||||
this.pickRatio = this.pickPass.pickRatio;
|
||||
this.pickX = Math.ceil(x * this.pickRatio);
|
||||
@@ -263,7 +275,8 @@ export class PickHelper {
|
||||
this.setupBuffers();
|
||||
}
|
||||
|
||||
this.spiral = spiral2d(Math.round(this.pickRatio * this.pickPadding));
|
||||
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private syncBuffers() {
|
||||
@@ -324,6 +337,10 @@ export class PickHelper {
|
||||
}
|
||||
|
||||
private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
|
||||
if (this.pickRatio !== this.pickPass.pickRatio) {
|
||||
this.update();
|
||||
}
|
||||
|
||||
const { webgl, pickRatio } = this;
|
||||
if (webgl.isContextLost) return;
|
||||
|
||||
@@ -392,7 +409,7 @@ export class PickHelper {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, readonly pickPadding = 1) {
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, private pickPadding = 1) {
|
||||
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -130,6 +130,7 @@ export namespace BaseGeometry {
|
||||
uClipObjectPosition: ValueCell.create(clip.objects.position),
|
||||
uClipObjectRotation: ValueCell.create(clip.objects.rotation),
|
||||
uClipObjectScale: ValueCell.create(clip.objects.scale),
|
||||
uClipObjectTransform: ValueCell.create(clip.objects.transform),
|
||||
|
||||
instanceGranularity: ValueCell.create(props.instanceGranularity),
|
||||
uLod: ValueCell.create(Vec4.create(props.lod[0], props.lod[1], props.lod[2], 0)),
|
||||
@@ -152,6 +153,7 @@ export namespace BaseGeometry {
|
||||
ValueCell.update(values.uClipObjectPosition, clip.objects.position);
|
||||
ValueCell.update(values.uClipObjectRotation, clip.objects.rotation);
|
||||
ValueCell.update(values.uClipObjectScale, clip.objects.scale);
|
||||
ValueCell.update(values.uClipObjectTransform, clip.objects.transform);
|
||||
|
||||
ValueCell.updateIfChanged(values.instanceGranularity, props.instanceGranularity);
|
||||
ValueCell.update(values.uLod, Vec4.set(values.uLod.ref.value, props.lod[0], props.lod[1], props.lod[2], 0));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -24,6 +24,8 @@ export type ColorData = {
|
||||
uColor: ValueCell<Vec3>,
|
||||
tColor: ValueCell<TextureImage<Uint8Array>>,
|
||||
tColorGrid: ValueCell<Texture>,
|
||||
uPaletteDomain: ValueCell<Vec2>,
|
||||
uPaletteDefault: ValueCell<Vec3>,
|
||||
tPalette: ValueCell<TextureImage<Uint8Array>>,
|
||||
uColorTexDim: ValueCell<Vec2>,
|
||||
uColorGridDim: ValueCell<Vec3>,
|
||||
@@ -36,6 +38,9 @@ export function createColors(locationIt: LocationIterator, positionIt: LocationI
|
||||
const data = _createColors(locationIt, positionIt, colorTheme, colorData);
|
||||
if (colorTheme.palette) {
|
||||
ValueCell.updateIfChanged(data.dUsePalette, true);
|
||||
const [min, max] = colorTheme.palette.domain || [0, 1];
|
||||
ValueCell.update(data.uPaletteDomain, Vec2.set(data.uPaletteDomain.ref.value, min, max));
|
||||
ValueCell.update(data.uPaletteDefault, Color.toVec3Normalized(data.uPaletteDefault.ref.value, colorTheme.palette.defaultColor ?? Color(0xCCCCCC)));
|
||||
updatePaletteTexture(colorTheme.palette, data.tPalette);
|
||||
} else {
|
||||
ValueCell.updateIfChanged(data.dUsePalette, false);
|
||||
@@ -103,6 +108,8 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
|
||||
uColor: ValueCell.create(Color.toVec3Normalized(Vec3(), value)),
|
||||
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
tColorGrid: ValueCell.create(createNullTexture()),
|
||||
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
|
||||
uPaletteDefault: ValueCell.create(Vec3()),
|
||||
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
|
||||
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
@@ -131,6 +138,8 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color
|
||||
uColor: ValueCell.create(Vec3()),
|
||||
tColor: ValueCell.create(colors),
|
||||
tColorGrid: ValueCell.create(createNullTexture()),
|
||||
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
|
||||
uPaletteDefault: ValueCell.create(Vec3()),
|
||||
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(colors.width, colors.height)),
|
||||
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
@@ -233,6 +242,8 @@ export function createGridColor(grid: ColorVolume, type: ColorType, colorData?:
|
||||
uColor: ValueCell.create(Vec3()),
|
||||
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
tColorGrid: ValueCell.create(colors),
|
||||
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
|
||||
uPaletteDefault: ValueCell.create(Vec3()),
|
||||
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(width, height)),
|
||||
uColorGridDim: ValueCell.create(Vec3.clone(dimension)),
|
||||
@@ -255,6 +266,8 @@ function createDirectColor(colorData?: ColorData): ColorData {
|
||||
uColor: ValueCell.create(Vec3()),
|
||||
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
tColorGrid: ValueCell.create(createNullTexture()),
|
||||
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
|
||||
uPaletteDefault: ValueCell.create(Vec3()),
|
||||
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
|
||||
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -214,7 +214,7 @@ export namespace DirectVolume {
|
||||
const { bboxSize, bboxMin, bboxMax, gridDimension, transform: gridTransform } = directVolume;
|
||||
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
const positionIt = Utils.createPositionIterator(directVolume, transform);
|
||||
const positionIt = createPositionIterator(directVolume, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -23,6 +23,7 @@ import { RenderObjectValues } from '../../mol-gl/render-object';
|
||||
import { TextureMesh } from './texture-mesh/texture-mesh';
|
||||
import { Image } from './image/image';
|
||||
import { Cylinders } from './cylinders/cylinders';
|
||||
import { arrayMaxPackedIntToRGB } from '../../mol-util/number-packing';
|
||||
|
||||
export type GeometryKind = 'mesh' | 'points' | 'spheres' | 'cylinders' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
|
||||
|
||||
@@ -105,7 +106,7 @@ export namespace Geometry {
|
||||
case 'direct-volume':
|
||||
return 1;
|
||||
case 'image':
|
||||
return arrayMax(geometry.groupTexture.ref.value.array) + 1;
|
||||
return arrayMaxPackedIntToRGB(geometry.groupTexture.ref.value.array, 4) + 1;
|
||||
case 'texture-mesh':
|
||||
return geometry.groupCount;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -9,7 +9,7 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { calculateTransformBoundingSphere, createTextureImage, TextureImage } from '../../../mol-gl/renderable/util';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Vec2, Vec4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Vec2, Vec4, Vec3, Quat, Mat4 } from '../../../mol-math/linear-algebra';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
@@ -59,16 +59,37 @@ interface Image {
|
||||
readonly imageTextureDim: ValueCell<Vec2>,
|
||||
readonly cornerBuffer: ValueCell<Float32Array>,
|
||||
readonly groupTexture: ValueCell<TextureImage<Uint8Array>>,
|
||||
readonly valueTexture: ValueCell<TextureImage<Float32Array>>,
|
||||
|
||||
readonly trimType: ValueCell<number>,
|
||||
readonly trimCenter: ValueCell<Vec3>,
|
||||
readonly trimRotation: ValueCell<Quat>,
|
||||
readonly trimScale: ValueCell<Vec3>,
|
||||
readonly trimTransform: ValueCell<Mat4>,
|
||||
|
||||
readonly isoLevel: ValueCell<number>,
|
||||
|
||||
/** Bounding sphere of the image */
|
||||
boundingSphere: Sphere3D
|
||||
}
|
||||
|
||||
namespace Image {
|
||||
export function create(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, image?: Image): Image {
|
||||
export type Trim = {
|
||||
type: 0 | 1 | 2 | 3 | 4 | 5,
|
||||
center: Vec3,
|
||||
rotation: Quat,
|
||||
scale: Vec3,
|
||||
transform: Mat4,
|
||||
}
|
||||
|
||||
export function createEmptyTrim(): Trim {
|
||||
return { type: 0, center: Vec3(), rotation: Quat(), scale: Vec3(), transform: Mat4() };
|
||||
}
|
||||
|
||||
export function create(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, valueTexture: TextureImage<Float32Array>, trim: Trim, isoLevel: number, image?: Image): Image {
|
||||
return image ?
|
||||
update(imageTexture, corners, groupTexture, image) :
|
||||
fromData(imageTexture, corners, groupTexture);
|
||||
update(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image) :
|
||||
fromData(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel);
|
||||
}
|
||||
|
||||
function hashCode(image: Image) {
|
||||
@@ -77,7 +98,7 @@ namespace Image {
|
||||
]);
|
||||
}
|
||||
|
||||
function fromData(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>): Image {
|
||||
function fromData(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, valueTexture: TextureImage<Float32Array>, trim: Trim, isoLevel: number): Image {
|
||||
const boundingSphere = Sphere3D();
|
||||
let currentHash = -1;
|
||||
|
||||
@@ -90,6 +111,13 @@ namespace Image {
|
||||
imageTextureDim: ValueCell.create(Vec2.create(width, height)),
|
||||
cornerBuffer: ValueCell.create(corners),
|
||||
groupTexture: ValueCell.create(groupTexture),
|
||||
valueTexture: ValueCell.create(valueTexture),
|
||||
trimType: ValueCell.create(trim.type),
|
||||
trimCenter: ValueCell.create(trim.center),
|
||||
trimRotation: ValueCell.create(trim.rotation),
|
||||
trimScale: ValueCell.create(trim.scale),
|
||||
trimTransform: ValueCell.create(trim.transform),
|
||||
isoLevel: ValueCell.create(isoLevel),
|
||||
get boundingSphere() {
|
||||
const newHash = hashCode(image);
|
||||
if (newHash !== currentHash) {
|
||||
@@ -103,7 +131,7 @@ namespace Image {
|
||||
return image;
|
||||
}
|
||||
|
||||
function update(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, image: Image): Image {
|
||||
function update(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, valueTexture: TextureImage<Float32Array>, trim: Trim, isoLevel: number, image: Image): Image {
|
||||
const width = imageTexture.width;
|
||||
const height = imageTexture.height;
|
||||
|
||||
@@ -111,6 +139,15 @@ namespace Image {
|
||||
ValueCell.update(image.imageTextureDim, Vec2.set(image.imageTextureDim.ref.value, width, height));
|
||||
ValueCell.update(image.cornerBuffer, corners);
|
||||
ValueCell.update(image.groupTexture, groupTexture);
|
||||
ValueCell.update(image.valueTexture, valueTexture);
|
||||
|
||||
ValueCell.updateIfChanged(image.trimType, trim.type);
|
||||
ValueCell.update(image.trimCenter, Vec3.copy(image.trimCenter.ref.value, trim.center));
|
||||
ValueCell.update(image.trimRotation, Quat.copy(image.trimRotation.ref.value, trim.rotation));
|
||||
ValueCell.update(image.trimScale, Vec3.copy(image.trimScale.ref.value, trim.scale));
|
||||
ValueCell.update(image.trimTransform, Mat4.copy(image.trimTransform.ref.value, trim.transform));
|
||||
|
||||
ValueCell.updateIfChanged(image.isoLevel, isoLevel);
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -118,7 +155,9 @@ namespace Image {
|
||||
const imageTexture = createTextureImage(0, 4, Uint8Array);
|
||||
const corners = image ? image.cornerBuffer.ref.value : new Float32Array(8 * 3);
|
||||
const groupTexture = createTextureImage(0, 4, Uint8Array);
|
||||
return create(imageTexture, corners, groupTexture, image);
|
||||
const valueTexture = createTextureImage(0, 1, Float32Array);
|
||||
const trim = createEmptyTrim();
|
||||
return create(imageTexture, corners, groupTexture, valueTexture, trim, -1, image);
|
||||
}
|
||||
|
||||
export const Params = {
|
||||
@@ -136,12 +175,16 @@ namespace Image {
|
||||
updateBoundingSphere,
|
||||
createRenderableState,
|
||||
updateRenderableState,
|
||||
createPositionIterator: () => LocationIterator(1, 1, 1, () => NullLocation)
|
||||
createPositionIterator
|
||||
};
|
||||
|
||||
function createPositionIterator(_image: Image, _transform: TransformData): LocationIterator {
|
||||
return LocationIterator(1, 1, 1, () => NullLocation);
|
||||
}
|
||||
|
||||
function createValues(image: Image, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): ImageValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
const positionIt = Utils.createPositionIterator(image, transform);
|
||||
const positionIt = createPositionIterator(image, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
@@ -186,6 +229,15 @@ namespace Image {
|
||||
uImageTexDim: image.imageTextureDim,
|
||||
tImageTex: image.imageTexture,
|
||||
tGroupTex: image.groupTexture,
|
||||
tValueTex: image.valueTexture,
|
||||
|
||||
uTrimType: image.trimType,
|
||||
uTrimCenter: image.trimCenter,
|
||||
uTrimRotation: image.trimRotation,
|
||||
uTrimScale: image.trimScale,
|
||||
uTrimTransform: image.trimTransform,
|
||||
|
||||
uIsoLevel: image.isoLevel,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
25
src/mol-geo/geometry/mesh/builder/plane.ts
Normal file
25
src/mol-geo/geometry/mesh/builder/plane.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Vec3, Mat4 } from '../../../../mol-math/linear-algebra';
|
||||
import { SegmentedPlane } from '../../../primitive/plane';
|
||||
import { MeshBuilder } from '../mesh-builder';
|
||||
|
||||
const tmpPlaneMat = Mat4.identity();
|
||||
const tmpVec = Vec3();
|
||||
|
||||
function setPlaneMat(m: Mat4, center: Vec3, dirMajor: Vec3, dirMinor: Vec3, scale: Vec3) {
|
||||
Vec3.add(tmpVec, center, dirMajor);
|
||||
Mat4.targetTo(m, center, tmpVec, dirMinor);
|
||||
Mat4.setTranslation(m, center);
|
||||
Mat4.mul(m, m, Mat4.rotY90);
|
||||
return Mat4.scale(m, m, scale);
|
||||
}
|
||||
|
||||
export function addPlane(state: MeshBuilder.State, center: Vec3, dirMajor: Vec3, dirMinor: Vec3, scale: Vec3, widthSegments: number, heightSegments: number) {
|
||||
const plane = SegmentedPlane(widthSegments, heightSegments);
|
||||
MeshBuilder.addPrimitive(state, setPlaneMat(tmpPlaneMat, center, dirMajor, dirMinor, scale), plane);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -37,4 +37,53 @@ export function Plane(): Primitive {
|
||||
|
||||
export function PlaneCage(): Cage {
|
||||
return planeCage;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export function SegmentedPlane(widthSegments: number, heightSegments: number): Primitive {
|
||||
const widthSegments1 = widthSegments + 1;
|
||||
const heightSegments1 = heightSegments + 1;
|
||||
|
||||
const segmentWidth = 1 / widthSegments;
|
||||
const segmentHeight = 1 / heightSegments;
|
||||
|
||||
const vertices = new Float32Array(widthSegments1 * heightSegments1 * 3);
|
||||
const normals = new Float32Array(widthSegments1 * heightSegments1 * 3);
|
||||
const indices = new Uint32Array(widthSegments * heightSegments * 6);
|
||||
|
||||
let i = 0;
|
||||
for (let iy = 0; iy < heightSegments1; ++iy) {
|
||||
const y = iy * segmentHeight - 0.5;
|
||||
for (let ix = 0; ix < widthSegments1; ++ix) {
|
||||
const x = ix * segmentWidth - 0.5;
|
||||
vertices[i] = x;
|
||||
vertices[i + 1] = -y;
|
||||
vertices[i + 2] = 0;
|
||||
normals[i] = 0;
|
||||
normals[i + 1] = 0;
|
||||
normals[i + 2] = 1;
|
||||
i += 3;
|
||||
}
|
||||
}
|
||||
|
||||
let j = 0;
|
||||
for (let iy = 0; iy < heightSegments; ++iy) {
|
||||
for (let ix = 0; ix < widthSegments; ++ix) {
|
||||
const a = ix + widthSegments1 * iy;
|
||||
const b = ix + widthSegments1 * (iy + 1);
|
||||
const c = (ix + 1) + widthSegments1 * (iy + 1);
|
||||
const d = (ix + 1) + widthSegments1 * iy;
|
||||
|
||||
indices[j] = a;
|
||||
indices[j + 1] = b;
|
||||
indices[j + 2] = d;
|
||||
indices[j + 3] = b;
|
||||
indices[j + 4] = c;
|
||||
indices[j + 5] = d;
|
||||
j += 6;
|
||||
}
|
||||
}
|
||||
|
||||
return { vertices, normals, indices };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -23,6 +23,15 @@ export const ImageSchema = {
|
||||
uImageTexDim: UniformSpec('v2'),
|
||||
tImageTex: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
|
||||
tGroupTex: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
|
||||
tValueTex: TextureSpec('image-float32', 'alpha', 'float', 'linear'),
|
||||
|
||||
uTrimType: UniformSpec('i'),
|
||||
uTrimCenter: UniformSpec('v3'),
|
||||
uTrimRotation: UniformSpec('q'),
|
||||
uTrimScale: UniformSpec('v3'),
|
||||
uTrimTransform: UniformSpec('m4'),
|
||||
|
||||
uIsoLevel: UniformSpec('f'),
|
||||
|
||||
dInterpolation: DefineSpec('string', InterpolationTypeNames),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -203,6 +203,8 @@ export const ColorSchema = {
|
||||
uColorTexDim: UniformSpec('v2'),
|
||||
uColorGridDim: UniformSpec('v3'),
|
||||
uColorGridTransform: UniformSpec('v4'),
|
||||
uPaletteDomain: UniformSpec('v2'),
|
||||
uPaletteDefault: UniformSpec('v3'),
|
||||
tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
|
||||
tPalette: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
|
||||
tColorGrid: TextureSpec('texture', 'rgb', 'ubyte', 'linear'),
|
||||
@@ -323,6 +325,7 @@ export const BaseSchema = {
|
||||
uClipObjectPosition: UniformSpec('v3[]', 'material'),
|
||||
uClipObjectRotation: UniformSpec('v4[]', 'material'),
|
||||
uClipObjectScale: UniformSpec('v3[]', 'material'),
|
||||
uClipObjectTransform: UniformSpec('m4[]', 'material'),
|
||||
|
||||
aInstance: AttributeSpec('float32', 1, 1),
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user