Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Rose
3667092327 tweak cube clamp param 2022-11-08 22:28:47 -08:00
Alexander Rose
842824057b add param to clamp cube values 2022-11-08 22:17:23 -08:00
443 changed files with 9445 additions and 128774 deletions

View File

@@ -18,7 +18,7 @@
],
"brace-style": "off",
"comma-spacing": "off",
"space-infix-ops": "off",
"space-infix-ops": "error",
"comma-dangle": "off",
"eqeqeq": [
"error",
@@ -107,7 +107,6 @@
"1tbs", { "allowSingleLine": true }
],
"@typescript-eslint/comma-spacing": "error",
"@typescript-eslint/space-infix-ops": "error",
"@typescript-eslint/space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",

View File

@@ -1,5 +1,3 @@
name: Build
on:
push:
pull_request:
@@ -17,6 +15,6 @@ jobs:
- name: Lint
run: npm run lint
- name: Test
run: npm install --no-save "gl@^6.0.2" && xvfb-run --auto-servernum npm run jest
run: npm install --no-save "gl@^5.0.0" && xvfb-run --auto-servernum npm run jest
- name: Build
run: npm run build

18
.travis.yml Normal file
View File

@@ -0,0 +1,18 @@
language: node_js
os: linux
sudo: required
dist: trusty
before_install:
- sudo apt-get install -y mesa-utils
- sudo apt-get install -y xvfb
- sudo apt-get install -y libgl1-mesa-dri
- sudo apt-get install -y libglapi-mesa
- sudo apt-get install -y libosmesa6
- sudo apt-get install -y gcc-4.9
- sudo apt-get install -y libstdc++6
- sudo apt-get install -y libxi-dev
node_js:
- "12"
- "10"
before_script:
- export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start

View File

@@ -6,268 +6,18 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [v3.38.2] - 2023-07-24
- Don't rely solely on `chem_comp_atom` when detecting CCD files (#877)
- Actually support non-physical keys in `Bindings.Trigger.code`
## [v3.38.1] - 2023-07-22
- Fix pixel-scale not updated in SSAO pass
## [v3.38.0] - 2023-07-18
- Fix display issue with SIFTS mapping
- Support non-physical keys in `Bindings.Trigger.code`
- Update `getStateSnapshot` to only overwrite current snapshot if it was created automatically
- Fix distinct palette's `getSamples` infinite loop
- Add 'NH2', 'FOR', 'FMT' to `CommonProteinCaps`
- Add `opened` event to `PluginStateSnapshotManager`
- Properly switch-off fog
- Add `approximate` option for spheres rendering
- Reduce `Spheres` memory usage
- Derive mapping from VertexID
- Pull position and group from texture
- Add `Euler` math primitive
- Add stride option to element sphere & point visuals
- Add `disabledExtensions` field to default viewer's options
- Add `LRUCache.remove`
- Add 'Chain Instance' and 'Uniform' options for 'Carbon Color' param (in Color Theme: Element Symbol)
## [v3.37.1] - 2023-06-20
- Fix issues with wboit/dpoit in large scenes
- Fix lines, text, points rendering (broken in v3.37.0)
## [v3.37.0] - 2023-06-17
- Add `inverted` option to `xrayShaded` parameter
- Model-export extension: Add ability to set a file name for structures
- Add `contextHash` to `SizeTheme`
- Add mipmap-based blur for image backgrounds
## [v3.36.1] - 2023-06-11
- Allow parsing of CCD ligand files
- Add dedicated wwPDB CCD extension to align and visualize ideal & model CCD coordinates
- Make operators in `IndexPairBonds` a directed property
- Remove erroneous bounding-box overlap test in `Structure.eachUnitPair`
- Fix `EdgeBuilder.addNextEdge` for loop edges
- Optimize inter unit bond compute
- Ensure consistent state for volume representation (#210)
- Improve SSAO for thin geometry (e.g. lines)
- Add snapshot support for structure selections
- Add `nucleicProfile` parameter to cartoon representation
- Add `cartoon` theme with separate colorings for for mainchain and sidechain visuals
## [v3.35.0] - 2023-05-14
- Enable odd dash count (1,3,5)
- Add principal axes spec and fix edge cases
- Add a uniform color theme for NtC tube that still paints residue and segment dividers in a different color
- Mesh exporter improvements
- Support points & lines in glTF export
- Set alphaMode and doubleSided in glTF export
- Fix flipped cylinder caps
- Fix bond assignments `struct_conn` records referencing waters
- Add StructConn extension providing functions for inspecting struct_conns
- Fix `PluginState.setSnapshot` triggering unnecessary state updates
- Fix an edge case in the `mol-state`'s `State` when trying to apply a transform to an existing Null object
- Add `SbNcbrPartialCharges` extension for coloring and labeling atoms and residues by partial atomic charges
- uses custom mmcif categories `_sb_ncbr_partial_atomic_charges_meta` and `_sb_ncbr_partial_atomic_charges` (more info in [README.md](./src/extensions/sb-ncbr/README.md))
- Parse HEADER record when reading PDB file
- Support `ignoreHydrogens` in interactions representation
- Add hydroxyproline (HYP) commonly present in collagen molecules to the list of amino acids
- Fix assemblies for Archive PDB files (do not generate unique `label_asym_id` if `REMARK 350` is present)
- Add additional functions to `core.math` in `mol-script`
- `cantorPairing`, `sortedCantorPairing`, `invertCantorPairing`,
- `trunc`, `sign`
## [v3.34.0] - 2023-04-16
- Avoid `renderMarkingDepth` for fully transparent renderables
- Remove `camera.far` doubling workaround
- Add `ModifiersKeys.areNone` helper function
- Do not render NtC tube segments unless all required atoms are present in the structure
- Fix rendering issues caused by VAO reuse
- Add "Zoom All", "Orient Axes", "Reset Axes" buttons to the "Reset Camera" button
- Improve trackball move-state handling when key bindings use modifiers
- Fix rendering with very small viewport and SSAO enabled
- Fix `.getAllLoci` for structure representations with `structure.child`
- Fix `readAllLinesAsync` refering to dom length property
- Make mol-util/file-info node compatible
- Add `eachLocation` to representation/visual interface
## [v3.33.0] - 2023-04-02
- Handle resizes of viewer element even when window remains the same size
- Throttle canvas resize events
- Selection toggle buttons hidden if selection mode is off
- Camera focus loci bindings allow reset on click-away to be overridden
- Input/controls improvements
- Move or fly around the scene using keys
- Pointer lock to look around scene
- Toggle spin/rock animation using keys
- Apply bumpiness as lightness variation with `ignoreLight`
- Remove `JSX` reference from `loci-labels.ts`
- Fix overpaint/transparency/substance smoothing not updated when geometry changes
- Fix camera project/unproject when using offset viewport
- Add support for loading all blocks from a mmcif file as a trajectory
- Add `Frustum3D` and `Plane3D` math primitives
- Include `occupancy` and `B_iso_or_equiv` when creating `Conformation` from `Model`
- Remove LazyImports (introduced in v3.31.1)
## [v3.32.0] - 2023-03-20
- Avoid rendering of fully transparent renderables
- Add occlusion color parameter
- Fix issue with outlines and orthographic camera
- Reduce over-blurring occlusion at larger view distances
- Fix occlusion artefact with non-canvas viewport and pixel-ratio > 1
- Update nodejs-shims conditionals to handle polyfilled document object in NodeJS environment.
- Ensure marking edges are at least one pixel wide
- Add exposure parameter to renderer
- Only trigger marking when mouse is directly over canvas
- Fix blurry occlusion in screenshots
- [Breaking] Add `setFSModule` to `mol-util/data-source` instead of trying to trick WebPack
## [v3.31.4] - 2023-02-24
- Allow link cylinder/line `dashCount` set to '0'
- Stop animation loop when disposing `PluginContext` (thanks @gfrn for identifying the issue)
## [v3.31.3] - 2023-02-22
- Fix impostor bond visuals not correctly updating on `sizeFactor` changes
- Fix degenerate case in PCA
- Fix near clipping avoidance in impostor shaders
- Update `fs` import in `data-source.ts`
## [v3.31.2] - 2023-02-12
- Fix exit code of volume pack executable (pack.ts). Now exits with non-0 status when an error happens
- Remove pca transform from components ui focus (too distracting)
- Fix artefacts with opaque outlines behind transparent objects
- Fix polymer trace visual not updating
- Fix use of `WEBGL_provoking_vertex`
## [v3.31.1] - 2023-02-05
- Improve Component camera focus based on the PCA of the structure and the following rules:
- The first residue should be in first quadrant if there is only one chain
- The average position of the residues of the first chain should be in the first quadrant if there is more than one chain
- Add `HeadlessPluginContext` and `HeadlessScreenshotHelper` to be used in Node.js
- Add example `image-renderer`
- Fix wrong offset when rendering text with orthographic projection
- Update camera/handle helper when `devicePixelRatio` changes
- Add various options to customize the axes camera-helper
- Fix issue with texture-mesh color smoothing when changing themes
- Add fast boundary helper and corresponding unit trait
- Add Observable for Canvas3D commits
## [v3.30.0] - 2023-01-29
- Improve `Dnatco` extension
- Factor out common code in `Dnatco` extension
- Add `NtC tube` visual. Applicable for structures with NtC annotation
- [Breaking] Rename `DnatcoConfalPyramids` to `DnatcoNtCs`
- Improve boundary calculation performance
- Add option to create & include images in state snapshots
- Fix SSAO artefacts with high bias values
- Fix SSAO resolution scale parameter handling
- Improve outlines, visually more stable at different view distances
## [v3.29.0] - 2023-01-15
- `meshes` extension: Fixed a bug in mesh visualization (show backfaces when opacity < 1)
- Add color quick select control to Volume controls
- Fix `dropFiles` bug
- Fix some cyclic imports and reduce the use of const enums. This should make it easier to use the library with the `isolatedModules: true` TS config.
- Fix `dropFiles` bug (#679)
- Add `input type='color'` picker to `CombinedColorControl`
- Set `ParameterMappingControl` disabled when state is updating
- Performance tweaks
- Update clip `defines` only when changed
- Check for identity in structure/unit areEqual methods
- Avoid cloning of structure representation parameters
- Make SymmetryOperator.createMapping monomorphic
- Improve bonding-sphere calculation
- Defer Scene properties calculation (markerAverage, opacityAverage, hasOpaque)
- Improve checks in in UnitsRepresentation setVisualState
- Add StructureElement.Loci.forEachLocation
- Add RepresentationRegistry.clear and ThemeRegistry.clear
- Add generic Loci support for overpaint, substance, clipping themes
- Add `.getCenter` and `.center` to `Camera`
- Add support to dim unmarked groups
- Add support for marker edge strength
## [v3.28.0] - 2022-12-20
- Show histogram in direct volume control point settings
- Add `solidInterior` parameter to sphere/cylinder impostors
- [Breaking] Tweak `ignoreHydrogens` non-polar handling (introduced in 3.27.0)
- Add `meshes` and `volumes-and-segmentations` extensions
- See https://molstarvolseg.ncbr.muni.cz/ for more info
- Fix missing support for info in `ParamDefinition.Converted`
- Add support for multi-visual volume representations
- Improve volume isosurface bounding-sphere
- Add basic volume segmentation support to core
- Add `Volume.Segment` model
- Add `Segmentation` custom volume property
- Add `SegmentRepresentation` representation
- Add `volume-segment` color theme
- Fix GPU marching cubes failing for large meshes with webgl2 (due to use of float16)
## [v3.27.0] - 2022-12-15
- Add an `includeTransparent` parameter to hide/show outlines of components that are transparent
- Fix 'once' for animations of systems with many frames
- Better guard against issue (black fringes) with bumpiness in impostors
- Improve impostor shaders
- Fix sphere near-clipping with orthographic projection
- Fix cylinder near-clipping
- Add interior cylinder caps
- Add per-pixel object clipping
- Fix `QualityAssessment` assignment bug for structures with different auth vs label sequence numbering
- Refresh `ApplyActionControl`'s param definition when toggling expanded state
- Fix `struct_conn` bond assignment for ions
- Ability to show only polar hydrogens
## [v3.26.0] - 2022-12-04
- Support for ``powerPreference`` webgl attribute. Add ``PluginConfig.General.PowerPreference`` and ``power-preference`` Viewer GET param.
- Excluded common protein caps `NME` and `ACE` from the ligand selection query
- Add screen-space shadow post-processing effect
- Add "Structure Molecular Surface" visual
- Add `external-volume` theme (coloring of arbitrary geometries by user-selected volume)
## [v3.25.1] - 2022-11-20
- Fix edge-case in `Structure.eachUnitPair` with single-element units
- Fix 'auto' structure-quality for coarse models
## [v3.25.0] - 2022-11-16
- Fix handling of gzipped assets (reverts #615)
## [v3.24.0] - 2022-11-13
- Make `PluginContext.initContainer` checkered canvas background optional
- Store URL of downloaded assets to detect zip/gzip based on extension (#615)
- Add optional `operator.key`; can be referenced in `IndexPairBonds`
- Add overpaint/transparency/substance theme strength to representations
- Fix viewport color for transparent background
## [v3.23.0] - 2022-10-19
- Add `PluginContext.initContainer/mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI
- Add `PluginContext.canvas3dInitialized`
- `createPluginUI` now resolves after the 3d canvas has been initialized
- Change EM Volume Streaming default from `Whole Structure` to `Auto`
- Change EM Volume Streaming default from `Whote Structure` to `Auto`
## [v3.22.0] - 2022-10-17
- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality`
- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality`
## [v3.21.0] - 2022-10-17

View File

@@ -1,6 +1,6 @@
[![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](./LICENSE)
[![npm version](https://badge.fury.io/js/molstar.svg)](https://www.npmjs.com/package/molstar)
[![Build](https://github.com/molstar/molstar/actions/workflows/node.yml/badge.svg)](https://github.com/molstar/molstar/actions/workflows/node.yml)
[![Build Status](https://travis-ci.org/molstar/molstar.svg?branch=master)](https://travis-ci.org/molstar/molstar)
[![Gitter](https://badges.gitter.im/molstar/Lobby.svg)](https://gitter.im/molstar/Lobby)
# Mol*

View File

@@ -1,118 +0,0 @@
# wwPDB StructConn extension
The STRUCT_CONN category in the mmCIF file format contains details about the connections between portions of the structure. These can be hydrogen bonds, salt bridges, disulfide bridges and so on (see more at <https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v40.dic/Categories/struct_conn.html>).
**wwPDB StructConn extension** in Mol* provides functionality to retrieve and visualize these connections.
The extension exposes three functions, located in `src/extensions/wwpdb/struct-conn/index.ts`.
- `getStructConns` - to retrieve struct_conn records from a loaded structure
- `inspectStructConn` - to visualize a struct_conn
- `clearStructConnInspections` - to remove visulizations created by `inspectStructConn`
## Example 1
The following example is a minimal HTML using this functionality:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer</title>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<body style="margin: 0px;">
<div style="position: absolute; width: 100%; height: 10%; padding-block: 10px;">
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf1');">disulf1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf2');">disulf2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale1');">covale1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale2');">covale2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale3');">covale3</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale4');">covale4</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc1');">metalc1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc2');">metalc2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc3');">metalc3</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc4');">metalc4</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, '5elb');">CLEAR</button>
</div>
<div id="app" style="position: absolute; top: 10%; width: 100%; height: 90%;"></div>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
var molstarViewer;
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
molstarViewer = viewer;
viewer.loadPdb('5elb');
});
</script>
</body>
</html>
```
The PDB ID (`'5elb'`) can be replaced be `undefined`, in which case the functions will apply to the first loaded structure.
## Example 2
This is a more elaborated example, which automatically loads `5elb` (or any PDB entry given in the URL after `?pdb=`), retrieves the list of struct_conns, and creates a button for each struct_conn.
Be aware that some of the struct_conns may be present in the deposited model but not in the preferred assembly (default view). The presented example will raise a dialog window with error message in such cases, e.g. `disulf6` in entry `5elb`.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer - StructConn Extension Demo</title>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<style>
body { margin: 0px; }
#app { position: absolute; width: 85%; height: 100%; }
#controls { position: absolute; right: 0; width: 15%; height: 100%; display: flex; flex-direction: column; overflow-y: scroll; }
h1 { text-align: center; margin: 12px; font-weight: bold; font-size: 120%; }
button { margin: 4px; margin-top: 0px; }
</style>
<body>
<div id="app"></div>
<div id="controls">
<h1 id="pdb-id">Loading...</h1>
<button onclick="clearInspections();">CLEAR</button>
</div>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
var pdbId = window.location.search.match(/[?&]pdb=(\w+)/i)?.[1]?.toLowerCase() ?? '5elb';
var molstarViewer;
function inspect(structConnId) {
if (molstarViewer?.plugin) {
molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, pdbId, structConnId).then(nSelectedAtoms => {
if (nSelectedAtoms < 2) alert('Some of the interacting atoms were not found :(\n(maybe not present in the viewed assembly)');
});
}
}
function clearInspections() {
if (molstarViewer?.plugin) {
molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, pdbId);
}
}
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
molstarViewer = viewer;
return viewer.loadPdb(pdbId);
}).then(() => {
const structConns = molstar.PluginExtensions.wwPDBStructConn.getStructConns(molstarViewer.plugin, pdbId);
const controls = document.getElementById('controls');
for (const structConnId in structConns) {
const button = document.createElement('button');
button.innerText = structConnId;
button.addEventListener('click', () => inspect(structConnId));
controls.appendChild(button);
};
document.getElementById('pdb-id').innerHTML = pdbId;
});
</script>
</body>
</html>
```

View File

@@ -26,7 +26,6 @@
* Non-standard residues
* Protein (1BRR, 5Z6Y)
* DNA (5D3G)
* Collagen (6JEC)
* Multiple models with different sets of ligands or missing ligands (1J6T, 1VRC, 2ICY, 1O2F)
* Long linear sugar chain (4HG6)
* Anisotropic B-factors/Ellipsoids (1EJG)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11483
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "3.38.2",
"version": "3.23.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -13,7 +13,7 @@
"scripts": {
"lint": "eslint .",
"lint-fix": "eslint . --fix",
"test": "npm install --no-save \"gl@^6.0.2\" && npm run lint && jest",
"test": "npm install --no-save \"gl@^5.0.0\" && npm run lint && jest",
"jest": "jest",
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
"clean": "node ./scripts/clean.js",
@@ -92,90 +92,76 @@
"Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
"Adam Midlik <midlik@gmail.com>",
"Koya Sakuma <koya.sakuma.work@gmail.com>",
"Gianluca Tomasello <giagitom@gmail.com>",
"Ke Ma <mark.ma@rcsb.org>",
"Jason Pattle <jpattle@exscientia.co.uk>",
"David Williams <dwilliams@nobiastx.com>",
"Zhenyu Zhang <jump2cn@gmail.com>",
"Russell Parker <russell@benchling.com>",
"Dominik Tichy <tichydominik451@gmail.com>",
"Yana Rose <yana.v.rose@gmail.com>"
"Gianluca Tomasello <giagitom@gmail.com>"
],
"license": "MIT",
"devDependencies": {
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^4.0.1",
"@graphql-codegen/time": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/add": "^3.2.1",
"@graphql-codegen/cli": "^2.13.7",
"@graphql-codegen/time": "^3.2.1",
"@graphql-codegen/typescript": "^2.7.4",
"@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
"@graphql-codegen/typescript-graphql-request": "^5.0.0",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@types/cors": "^2.8.13",
"@types/gl": "^6.0.2",
"@types/jpeg-js": "^0.3.7",
"@types/pngjs": "^6.0.1",
"@types/jest": "^29.5.2",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@graphql-codegen/typescript-graphql-request": "^4.5.6",
"@graphql-codegen/typescript-operations": "^2.5.4",
"@types/cors": "^2.8.12",
"@types/gl": "^4.1.1",
"@types/jest": "^29.1.2",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"benchmark": "^2.1.4",
"concurrently": "^8.2.0",
"cpx2": "^5.0.0",
"concurrently": "^7.4.0",
"cpx2": "^4.2.0",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.8.1",
"eslint": "^8.44.0",
"css-loader": "^6.7.1",
"eslint": "^8.25.0",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^6.2.0",
"fs-extra": "^11.1.1",
"graphql": "^16.7.1",
"fs-extra": "^10.1.0",
"graphql": "^16.6.0",
"http-server": "^14.1.1",
"jest": "^29.6.1",
"mini-css-extract-plugin": "^2.7.6",
"jest": "^29.2.0",
"mini-css-extract-plugin": "^2.6.1",
"path-browserify": "^1.0.1",
"raw-loader": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.63.6",
"sass-loader": "^13.3.2",
"simple-git": "^3.19.1",
"sass": "^1.55.0",
"sass-loader": "^13.1.0",
"simple-git": "^3.14.1",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.3",
"ts-jest": "^29.1.1",
"typescript": "^5.1.6",
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4"
"style-loader": "^3.3.1",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"dependencies": {
"@types/argparse": "^2.0.10",
"@types/benchmark": "^2.1.2",
"@types/compression": "1.7.2",
"@types/express": "^4.17.17",
"@types/node": "^16.18.38",
"@types/node-fetch": "^2.6.4",
"@types/express": "^4.17.14",
"@types/node": "^16.11.66",
"@types/node-fetch": "^2.6.2",
"@types/swagger-ui-dist": "3.30.1",
"argparse": "^2.0.1",
"body-parser": "^1.20.2",
"body-parser": "^1.20.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.18.2",
"h264-mp4-encoder": "^1.0.12",
"immer": "^9.0.21",
"immutable": "^4.3.0",
"node-fetch": "^2.6.12",
"rxjs": "^7.8.1",
"swagger-ui-dist": "^5.1.0",
"tslib": "^2.6.0",
"util.promisify": "^1.1.2",
"immer": "^9.0.15",
"immutable": "^4.1.0",
"node-fetch": "^2.6.7",
"rxjs": "^7.5.7",
"swagger-ui-dist": "^4.14.3",
"tslib": "^2.4.0",
"util.promisify": "^1.1.1",
"xhr2": "^0.2.1"
},
"peerDependencies": {
"react": "^18.1.0 || ^17.0.2 || ^16.14.0",
"react-dom": "^18.1.0 || ^17.0.2 || ^16.14.0"
},
"optionalDependencies": {
"gl": "^6.0.2",
"jpeg-js": "^0.4.4",
"pngjs": "^6.0.0"
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -15,7 +15,7 @@ const deployDir = path.resolve(buildDir, 'deploy/');
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 analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics -->`;
function log(command, stdout, stderr) {
if (command) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -31,8 +31,7 @@ function shinyStyle(plugin: PluginContext) {
postprocessing: {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'off', params: {} },
shadow: { name: 'off', params: {} },
outline: { name: 'off', params: {} },
outline: { name: 'off', params: {} }
}
} });
}
@@ -45,21 +44,17 @@ function occlusionStyle(plugin: PluginContext) {
postprocessing: {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'on', params: {
blurKernelSize: 15,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
radius: 5,
samples: 32,
resolutionScale: 1,
color: Color(0x000000),
resolutionScale: 1
} },
outline: { name: 'on', params: {
scale: 1.0,
threshold: 0.33,
color: Color(0x0000),
includeTransparent: true,
} },
shadow: { name: 'off', params: {} },
} }
}
} });
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2022 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>
@@ -7,9 +7,8 @@
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { CellPack } from '../../extensions/cellpack';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
import { GeometryExport } from '../../extensions/geo-export';
import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior';
import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
@@ -48,22 +47,18 @@ import '../../mol-util/polyfill';
import { ObjectKeys } from '../../mol-util/type-helpers';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { Backgrounds } from '../../extensions/backgrounds';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
const CustomFormats = [
['g3d', G3dProvider] as const
];
export const ExtensionMap = {
'volseg': PluginSpec.Behavior(Volseg),
const Extensions = {
'backgrounds': PluginSpec.Behavior(Backgrounds),
'cellpack': PluginSpec.Behavior(CellPack),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
@@ -74,14 +69,11 @@ export const ExtensionMap = {
'geo-export': PluginSpec.Behavior(GeometryExport),
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
};
const DefaultViewerOptions = {
customFormats: CustomFormats as [string, DataFormatProvider][],
extensions: ObjectKeys(ExtensionMap),
disabledExtensions: [] as string[],
extensions: ObjectKeys(Extensions),
layoutIsExpanded: true,
layoutShowControls: true,
layoutShowRemoteState: true,
@@ -99,7 +91,6 @@ const DefaultViewerOptions = {
enableDpoit: PluginConfig.General.EnableDpoit.defaultValue,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -113,7 +104,6 @@ const DefaultViewerOptions = {
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
};
type ViewerOptions = typeof DefaultViewerOptions;
@@ -132,13 +122,11 @@ export class Viewer {
const o: ViewerOptions = { ...DefaultViewerOptions, ...definedOptions };
const defaultSpec = DefaultPluginUISpec();
const disabledExtension = new Set(o.disabledExtensions ?? []);
const spec: PluginUISpec = {
actions: defaultSpec.actions,
behaviors: [
...defaultSpec.behaviors,
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
...o.extensions.map(e => Extensions[e]),
],
animations: [...defaultSpec.animations || []],
customParamEditors: defaultSpec.customParamEditors,
@@ -175,7 +163,6 @@ export class Viewer {
[PluginConfig.General.EnableDpoit, o.enableDpoit],
[PluginConfig.General.PreferWebGl1, o.preferWebgl1],
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
[PluginConfig.General.PowerPreference, o.powerPreference],
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
@@ -190,7 +177,6 @@ export class Viewer {
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
]
};
@@ -511,14 +497,8 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
} else {
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
}
}
});
export const PluginExtensions = {
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
};

View File

@@ -63,12 +63,8 @@
var enableDpoit = getParam('enable-dpoit', '[^&]+').trim() === '1';
var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
molstar.Viewer.create('app', {
disabledExtensions: [], // anything from Object.keys(molstar.ExtensionMap)
layoutShowControls: !hideControls,
viewportShowExpand: false,
collapseLeftPanel: collapseLeftPanel,
@@ -84,7 +80,6 @@
enableDpoit: enableDpoit ? true : void 0,
preferWebgl1: preferWebgl1,
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
}).then(viewer => {
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);

View File

@@ -81,5 +81,5 @@ export const DefaultDataOptions: DataOptions = {
const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
const CCD_PATH = path.join(DATA_DIR, 'components.cif');
const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
const CCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/components.cif';
const PVCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';

View File

@@ -152,7 +152,7 @@ function getImportFrames(d: Data.CifFrame, imports: Imports) {
}
/** get field from given or linked category */
function getField(category: string, field: string, d: Data.CifFrame, imports: Imports, ctx: FrameData): Data.CifField | undefined {
function getField(category: string, field: string, d: Data.CifFrame, imports: Imports, ctx: FrameData): Data.CifField|undefined {
const { categories, links } = ctx;
const cat = d.categories[category];
if (cat) {

View File

@@ -25,7 +25,7 @@ async function readFile(path: string) {
}
}
async function parseCif(data: string | Uint8Array) {
async function parseCif(data: string|Uint8Array) {
const comp = CIF.parse(data);
const parsed = await comp.run(p => console.log(Progress.format(p)), 250);
if (parsed.isError) throw parsed;

View File

@@ -38,7 +38,7 @@ function print(volume: Volume) {
}
async function doMesh(volume: Volume, filename: string) {
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
// Export the mesh in OBJ format.

View File

@@ -25,7 +25,7 @@ import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
import { setDebugMode, setTimingMode, consoleStats } from '../../mol-util/debug';
import { setDebugMode, setTimingMode } from '../../mol-util/debug';
interface DemoInput {
moleculeSdf: string,
@@ -222,5 +222,4 @@ export class AlphaOrbitalsExample {
(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
(window as any).AlphaOrbitalsExample.setDebugMode = setDebugMode;
(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;
(window as any).AlphaOrbitalsExample.consoleStats = consoleStats;
(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;

View File

@@ -1,94 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*
* Example command-line application generating images of PDB structures
* Build: npm install --no-save gl jpeg-js pngjs // these packages are not listed in dependencies for performance reasons
* npm run build
* Run: node lib/commonjs/examples/image-renderer 1cbs ../outputs_1cbs/
*/
import { ArgumentParser } from 'argparse';
import fs from 'fs';
import path from 'path';
import gl from 'gl';
import pngjs from 'pngjs';
import jpegjs from 'jpeg-js';
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
import { DefaultPluginSpec } from '../../mol-plugin/spec';
import { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
import { setFSModule } from '../../mol-util/data-source';
setFSModule(fs);
interface Args {
pdbId: string,
outDirectory: string
}
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Example command-line application generating images of PDB structures' });
parser.add_argument('pdbId', { help: 'PDB identifier' });
parser.add_argument('outDirectory', { help: 'Directory for outputs' });
const args = parser.parse_args();
return { ...args };
}
async function main() {
const args = parseArguments();
const url = `https://www.ebi.ac.uk/pdbe/entry-files/download/${args.pdbId}.bcif`;
console.log('PDB ID:', args.pdbId);
console.log('Source URL:', url);
console.log('Outputs:', args.outDirectory);
// Create a headless plugin
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec(), { width: 800, height: 800 });
await plugin.init();
// Download and visualize data in the plugin
const update = plugin.build();
const structure = update.toRoot()
.apply(Download, { url, isBinary: true })
.apply(ParseCif)
.apply(TrajectoryFromMmCif)
.apply(ModelFromTrajectory)
.apply(StructureFromModel);
const polymer = structure.apply(StructureComponent, { type: { name: 'static', params: 'polymer' } });
const ligand = structure.apply(StructureComponent, { type: { name: 'static', params: 'ligand' } });
polymer.apply(StructureRepresentation3D, {
type: { name: 'cartoon', params: { alpha: 1 } },
colorTheme: { name: 'sequence-id', params: {} },
});
ligand.apply(StructureRepresentation3D, {
type: { name: 'ball-and-stick', params: { sizeFactor: 1 } },
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
sizeTheme: { name: 'physical', params: {} },
});
await update.commit();
// Export images
fs.mkdirSync(args.outDirectory, { recursive: true });
await plugin.saveImage(path.join(args.outDirectory, 'basic.png'));
await plugin.saveImage(path.join(args.outDirectory, 'basic.jpg'));
await plugin.saveImage(path.join(args.outDirectory, 'large.png'), { width: 1600, height: 1200 });
await plugin.saveImage(path.join(args.outDirectory, 'large.jpg'), { width: 1600, height: 1200 });
await plugin.saveImage(path.join(args.outDirectory, 'stylized.png'), undefined, STYLIZED_POSTPROCESSING);
await plugin.saveImage(path.join(args.outDirectory, 'stylized.jpg'), undefined, STYLIZED_POSTPROCESSING);
await plugin.saveImage(path.join(args.outDirectory, 'stylized-compressed-jpg.jpg'), undefined, STYLIZED_POSTPROCESSING, undefined, 10);
// Export state loadable in Mol* Viewer
await plugin.saveStateSnapshot(path.join(args.outDirectory, 'molstar-state.molj'));
// Cleanup
await plugin.clear();
plugin.dispose();
}
main();

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -24,31 +24,8 @@ const Canvas3DPresets = {
illustrative: {
canvas3d: <Preset>{
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
resolutionScale: 1,
color: Color(0x000000),
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.33,
color: Color(0x000000),
includeTransparent: true,
}
},
shadow: {
name: 'off',
params: {}
},
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000) } }
},
renderer: {
ambientIntensity: 1.0,
@@ -59,25 +36,8 @@ const Canvas3DPresets = {
occlusion: {
canvas3d: <Preset>{
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
resolutionScale: 1,
}
},
outline: {
name: 'off',
params: {}
},
shadow: {
name: 'off',
params: {}
},
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
outline: { name: 'off', params: {} }
},
renderer: {
ambientIntensity: 0.4,
@@ -90,8 +50,7 @@ const Canvas3DPresets = {
canvas3d: <Preset>{
postprocessing: {
occlusion: { name: 'off', params: {} },
outline: { name: 'off', params: {} },
shadow: { name: 'off', params: {} },
outline: { name: 'off', params: {} }
},
renderer: {
ambientIntensity: 0.4,

View File

@@ -50,7 +50,6 @@ export const Backgrounds = PluginBehavior.create<{ }>({
lightness: 0,
saturation: 0,
opacity: 1,
blur: 0,
coverage: 'viewport',
}
}

View File

@@ -52,7 +52,7 @@ export interface Compartment {
}
// Primitives discribing a compartment
export enum CompartmentPrimitiveType {
export const enum CompartmentPrimitiveType {
MetaBall = 0,
Sphere = 1,
Cube = 2,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ludovic Autin <ludovic.autin@gmail.com>
@@ -236,7 +236,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
structure = await getCurve(name, getCurveTransforms(ingredient), model);
} else {
if ((!results || results.length === 0)) return;
let bu: string | undefined = source.bu ? source.bu : undefined;
let bu: string|undefined = source.bu ? source.bu : undefined;
if (bu) {
if (bu === 'AU') {
bu = undefined;
@@ -585,7 +585,7 @@ export const LoadCellPackModel = StateAction.build({
... ctx.managers.structure.component.state.options,
visualQuality: 'custom',
ignoreLight: true,
hydrogens: 'hide-all',
showHydrogens: false,
});
ctx.canvas3d?.setProps({
multiSample: { mode: 'off' },
@@ -600,21 +600,10 @@ export const LoadCellPackModel = StateAction.build({
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 8,
bias: 1,
blurKernelSize: 15,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {
name: 'on',
params: {
bias: 0.6,
maxDistance: 80,
steps: 3,
tolerance: 1.0,
}
},
outline: {
@@ -623,7 +612,6 @@ export const LoadCellPackModel = StateAction.build({
scale: 1,
threshold: 0.33,
color: ColorNames.black,
includeTransparent: true,
}
}
}

View File

@@ -1,59 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ConfalPyramidsPreset } from './confal-pyramids/behavior';
import { ConfalPyramidsColorThemeProvider } from './confal-pyramids/color';
import { ConfalPyramidsProvider } from './confal-pyramids/property';
import { ConfalPyramidsRepresentationProvider } from './confal-pyramids/representation';
import { NtCTubePreset } from './ntc-tube/behavior';
import { NtCTubeColorThemeProvider } from './ntc-tube/color';
import { NtCTubeProvider } from './ntc-tube/property';
import { NtCTubeRepresentationProvider } from './ntc-tube/representation';
export const DnatcoNtCs = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
name: 'dnatco-ntcs',
category: 'custom-props',
display: {
name: 'DNATCO NtC Annotations',
description: 'DNATCO NtC Annotations',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
register(): void {
this.ctx.customModelProperties.register(ConfalPyramidsProvider, this.params.autoAttach);
this.ctx.customModelProperties.register(NtCTubeProvider, this.params.autoAttach);
this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.add(NtCTubeColorThemeProvider);
this.ctx.representation.structure.registry.add(NtCTubeRepresentationProvider);
this.ctx.builders.structure.representation.registerPreset(ConfalPyramidsPreset);
this.ctx.builders.structure.representation.registerPreset(NtCTubePreset);
}
unregister() {
this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
this.ctx.customModelProperties.unregister(NtCTubeProvider.descriptor.name);
this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.remove(NtCTubeRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(NtCTubeColorThemeProvider);
this.ctx.builders.structure.representation.unregisterPreset(ConfalPyramidsPreset);
this.ctx.builders.structure.representation.unregisterPreset(NtCTubePreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
showToolTip: PD.Boolean(true)
})
});

View File

@@ -1,219 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { Color, ColorMap } from '../../mol-util/color';
export const DefaultNtCClassColors = {
A: 0xFFC1C1,
B: 0xC8CFFF,
BII: 0x0059DA,
miB: 0x3BE8FB,
Z: 0x01F60E,
IC: 0xFA5CFB,
OPN: 0xE90000,
SYN: 0xFFFF01,
N: 0xF2F2F2,
};
export const ErrorColor = Color(0xFFA10A);
export const NtCColors = ColorMap({
NANT_Upr: DefaultNtCClassColors.N,
NANT_Lwr: DefaultNtCClassColors.N,
AA00_Upr: DefaultNtCClassColors.A,
AA00_Lwr: DefaultNtCClassColors.A,
AA02_Upr: DefaultNtCClassColors.A,
AA02_Lwr: DefaultNtCClassColors.A,
AA03_Upr: DefaultNtCClassColors.A,
AA03_Lwr: DefaultNtCClassColors.A,
AA04_Upr: DefaultNtCClassColors.A,
AA04_Lwr: DefaultNtCClassColors.A,
AA08_Upr: DefaultNtCClassColors.A,
AA08_Lwr: DefaultNtCClassColors.A,
AA09_Upr: DefaultNtCClassColors.A,
AA09_Lwr: DefaultNtCClassColors.A,
AA01_Upr: DefaultNtCClassColors.A,
AA01_Lwr: DefaultNtCClassColors.A,
AA05_Upr: DefaultNtCClassColors.A,
AA05_Lwr: DefaultNtCClassColors.A,
AA06_Upr: DefaultNtCClassColors.A,
AA06_Lwr: DefaultNtCClassColors.A,
AA10_Upr: DefaultNtCClassColors.A,
AA10_Lwr: DefaultNtCClassColors.A,
AA11_Upr: DefaultNtCClassColors.A,
AA11_Lwr: DefaultNtCClassColors.A,
AA07_Upr: DefaultNtCClassColors.A,
AA07_Lwr: DefaultNtCClassColors.A,
AA12_Upr: DefaultNtCClassColors.A,
AA12_Lwr: DefaultNtCClassColors.A,
AA13_Upr: DefaultNtCClassColors.A,
AA13_Lwr: DefaultNtCClassColors.A,
AB01_Upr: DefaultNtCClassColors.A,
AB01_Lwr: DefaultNtCClassColors.B,
AB02_Upr: DefaultNtCClassColors.A,
AB02_Lwr: DefaultNtCClassColors.B,
AB03_Upr: DefaultNtCClassColors.A,
AB03_Lwr: DefaultNtCClassColors.B,
AB04_Upr: DefaultNtCClassColors.A,
AB04_Lwr: DefaultNtCClassColors.B,
AB05_Upr: DefaultNtCClassColors.A,
AB05_Lwr: DefaultNtCClassColors.B,
BA01_Upr: DefaultNtCClassColors.B,
BA01_Lwr: DefaultNtCClassColors.A,
BA05_Upr: DefaultNtCClassColors.B,
BA05_Lwr: DefaultNtCClassColors.A,
BA09_Upr: DefaultNtCClassColors.B,
BA09_Lwr: DefaultNtCClassColors.A,
BA08_Upr: DefaultNtCClassColors.BII,
BA08_Lwr: DefaultNtCClassColors.A,
BA10_Upr: DefaultNtCClassColors.B,
BA10_Lwr: DefaultNtCClassColors.A,
BA13_Upr: DefaultNtCClassColors.BII,
BA13_Lwr: DefaultNtCClassColors.A,
BA16_Upr: DefaultNtCClassColors.BII,
BA16_Lwr: DefaultNtCClassColors.A,
BA17_Upr: DefaultNtCClassColors.BII,
BA17_Lwr: DefaultNtCClassColors.A,
BB00_Upr: DefaultNtCClassColors.B,
BB00_Lwr: DefaultNtCClassColors.B,
BB01_Upr: DefaultNtCClassColors.B,
BB01_Lwr: DefaultNtCClassColors.B,
BB17_Upr: DefaultNtCClassColors.B,
BB17_Lwr: DefaultNtCClassColors.B,
BB02_Upr: DefaultNtCClassColors.B,
BB02_Lwr: DefaultNtCClassColors.B,
BB03_Upr: DefaultNtCClassColors.B,
BB03_Lwr: DefaultNtCClassColors.B,
BB11_Upr: DefaultNtCClassColors.B,
BB11_Lwr: DefaultNtCClassColors.B,
BB16_Upr: DefaultNtCClassColors.B,
BB16_Lwr: DefaultNtCClassColors.B,
BB04_Upr: DefaultNtCClassColors.B,
BB04_Lwr: DefaultNtCClassColors.BII,
BB05_Upr: DefaultNtCClassColors.B,
BB05_Lwr: DefaultNtCClassColors.BII,
BB07_Upr: DefaultNtCClassColors.BII,
BB07_Lwr: DefaultNtCClassColors.BII,
BB08_Upr: DefaultNtCClassColors.BII,
BB08_Lwr: DefaultNtCClassColors.BII,
BB10_Upr: DefaultNtCClassColors.miB,
BB10_Lwr: DefaultNtCClassColors.miB,
BB12_Upr: DefaultNtCClassColors.miB,
BB12_Lwr: DefaultNtCClassColors.miB,
BB13_Upr: DefaultNtCClassColors.miB,
BB13_Lwr: DefaultNtCClassColors.miB,
BB14_Upr: DefaultNtCClassColors.miB,
BB14_Lwr: DefaultNtCClassColors.miB,
BB15_Upr: DefaultNtCClassColors.miB,
BB15_Lwr: DefaultNtCClassColors.miB,
BB20_Upr: DefaultNtCClassColors.miB,
BB20_Lwr: DefaultNtCClassColors.miB,
IC01_Upr: DefaultNtCClassColors.IC,
IC01_Lwr: DefaultNtCClassColors.IC,
IC02_Upr: DefaultNtCClassColors.IC,
IC02_Lwr: DefaultNtCClassColors.IC,
IC03_Upr: DefaultNtCClassColors.IC,
IC03_Lwr: DefaultNtCClassColors.IC,
IC04_Upr: DefaultNtCClassColors.IC,
IC04_Lwr: DefaultNtCClassColors.IC,
IC05_Upr: DefaultNtCClassColors.IC,
IC05_Lwr: DefaultNtCClassColors.IC,
IC06_Upr: DefaultNtCClassColors.IC,
IC06_Lwr: DefaultNtCClassColors.IC,
IC07_Upr: DefaultNtCClassColors.IC,
IC07_Lwr: DefaultNtCClassColors.IC,
OP01_Upr: DefaultNtCClassColors.OPN,
OP01_Lwr: DefaultNtCClassColors.OPN,
OP02_Upr: DefaultNtCClassColors.OPN,
OP02_Lwr: DefaultNtCClassColors.OPN,
OP03_Upr: DefaultNtCClassColors.OPN,
OP03_Lwr: DefaultNtCClassColors.OPN,
OP04_Upr: DefaultNtCClassColors.OPN,
OP04_Lwr: DefaultNtCClassColors.OPN,
OP05_Upr: DefaultNtCClassColors.OPN,
OP05_Lwr: DefaultNtCClassColors.OPN,
OP06_Upr: DefaultNtCClassColors.OPN,
OP06_Lwr: DefaultNtCClassColors.OPN,
OP07_Upr: DefaultNtCClassColors.OPN,
OP07_Lwr: DefaultNtCClassColors.OPN,
OP08_Upr: DefaultNtCClassColors.OPN,
OP08_Lwr: DefaultNtCClassColors.OPN,
OP09_Upr: DefaultNtCClassColors.OPN,
OP09_Lwr: DefaultNtCClassColors.OPN,
OP10_Upr: DefaultNtCClassColors.OPN,
OP10_Lwr: DefaultNtCClassColors.OPN,
OP11_Upr: DefaultNtCClassColors.OPN,
OP11_Lwr: DefaultNtCClassColors.OPN,
OP12_Upr: DefaultNtCClassColors.OPN,
OP12_Lwr: DefaultNtCClassColors.OPN,
OP13_Upr: DefaultNtCClassColors.OPN,
OP13_Lwr: DefaultNtCClassColors.OPN,
OP14_Upr: DefaultNtCClassColors.OPN,
OP14_Lwr: DefaultNtCClassColors.OPN,
OP15_Upr: DefaultNtCClassColors.OPN,
OP15_Lwr: DefaultNtCClassColors.OPN,
OP16_Upr: DefaultNtCClassColors.OPN,
OP16_Lwr: DefaultNtCClassColors.OPN,
OP17_Upr: DefaultNtCClassColors.OPN,
OP17_Lwr: DefaultNtCClassColors.OPN,
OP18_Upr: DefaultNtCClassColors.OPN,
OP18_Lwr: DefaultNtCClassColors.OPN,
OP19_Upr: DefaultNtCClassColors.OPN,
OP19_Lwr: DefaultNtCClassColors.OPN,
OP20_Upr: DefaultNtCClassColors.OPN,
OP20_Lwr: DefaultNtCClassColors.OPN,
OP21_Upr: DefaultNtCClassColors.OPN,
OP21_Lwr: DefaultNtCClassColors.OPN,
OP22_Upr: DefaultNtCClassColors.OPN,
OP22_Lwr: DefaultNtCClassColors.OPN,
OP23_Upr: DefaultNtCClassColors.OPN,
OP23_Lwr: DefaultNtCClassColors.OPN,
OP24_Upr: DefaultNtCClassColors.OPN,
OP24_Lwr: DefaultNtCClassColors.OPN,
OP25_Upr: DefaultNtCClassColors.OPN,
OP25_Lwr: DefaultNtCClassColors.OPN,
OP26_Upr: DefaultNtCClassColors.OPN,
OP26_Lwr: DefaultNtCClassColors.OPN,
OP27_Upr: DefaultNtCClassColors.OPN,
OP27_Lwr: DefaultNtCClassColors.OPN,
OP28_Upr: DefaultNtCClassColors.OPN,
OP28_Lwr: DefaultNtCClassColors.OPN,
OP29_Upr: DefaultNtCClassColors.OPN,
OP29_Lwr: DefaultNtCClassColors.OPN,
OP30_Upr: DefaultNtCClassColors.OPN,
OP30_Lwr: DefaultNtCClassColors.OPN,
OP31_Upr: DefaultNtCClassColors.OPN,
OP31_Lwr: DefaultNtCClassColors.OPN,
OPS1_Upr: DefaultNtCClassColors.OPN,
OPS1_Lwr: DefaultNtCClassColors.OPN,
OP1S_Upr: DefaultNtCClassColors.OPN,
OP1S_Lwr: DefaultNtCClassColors.SYN,
AAS1_Upr: DefaultNtCClassColors.SYN,
AAS1_Lwr: DefaultNtCClassColors.A,
AB1S_Upr: DefaultNtCClassColors.A,
AB1S_Lwr: DefaultNtCClassColors.SYN,
AB2S_Upr: DefaultNtCClassColors.A,
AB2S_Lwr: DefaultNtCClassColors.SYN,
BB1S_Upr: DefaultNtCClassColors.B,
BB1S_Lwr: DefaultNtCClassColors.SYN,
BB2S_Upr: DefaultNtCClassColors.B,
BB2S_Lwr: DefaultNtCClassColors.SYN,
BBS1_Upr: DefaultNtCClassColors.SYN,
BBS1_Lwr: DefaultNtCClassColors.B,
ZZ01_Upr: DefaultNtCClassColors.Z,
ZZ01_Lwr: DefaultNtCClassColors.Z,
ZZ02_Upr: DefaultNtCClassColors.Z,
ZZ02_Lwr: DefaultNtCClassColors.Z,
ZZ1S_Upr: DefaultNtCClassColors.Z,
ZZ1S_Lwr: DefaultNtCClassColors.SYN,
ZZ2S_Upr: DefaultNtCClassColors.Z,
ZZ2S_Lwr: DefaultNtCClassColors.SYN,
ZZS1_Upr: DefaultNtCClassColors.SYN,
ZZS1_Lwr: DefaultNtCClassColors.Z,
ZZS2_Upr: DefaultNtCClassColors.SYN,
ZZS2_Lwr: DefaultNtCClassColors.Z,
});

View File

@@ -6,22 +6,23 @@
*/
import { ConfalPyramidsColorThemeProvider } from './color';
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsRepresentationProvider } from './representation';
import { Dnatco } from '../property';
import { DnatcoTypes } from '../types';
import { ConfalPyramidsTypes } from './types';
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-confal-pyramids',
display: {
name: 'Confal Pyramids', group: 'Annotation',
description: 'Schematic depiction of conformer class and confal value.',
},
isApplicable(a) {
return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
return a.data.models.length >= 1 && a.data.models.some(m => ConfalPyramids.isApplicable(m));
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
@@ -47,7 +48,50 @@ export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
}
});
export function confalPyramidLabel(step: DnatcoTypes.Step) {
export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
name: 'dnatco-confal-pyramids-prop',
category: 'custom-props',
display: {
name: 'Confal Pyramids',
description: 'Schematic depiction of conformer class and confal value.',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
private provider = ConfalPyramidsProvider;
register(): void {
this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
this.ctx.builders.structure.representation.registerPreset(DnatcoConfalPyramidsPreset);
}
update(p: { autoAttach: boolean, showToolTip: boolean }) {
const updated = this.params.autoAttach !== p.autoAttach;
this.params.autoAttach = p.autoAttach;
this.params.showToolTip = p.showToolTip;
this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
return updated;
}
unregister() {
this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
this.ctx.builders.structure.representation.unregisterPreset(DnatcoConfalPyramidsPreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
showToolTip: PD.Boolean(true)
})
});
export function confalPyramidLabel(halfPyramid: ConfalPyramidsTypes.HalfPyramid) {
const { step } = halfPyramid;
return `
<b>${step.auth_asym_id_1}</b> |
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}

View File

@@ -5,10 +5,8 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ErrorColor, NtCColors } from '../color';
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsTypes as CPT } from './types';
import { Dnatco } from '../property';
import { Location } from '../../../mol-model/location';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorTheme } from '../../../mol-theme/color';
@@ -21,7 +19,215 @@ import { ObjectKeys } from '../../../mol-util/type-helpers';
const Description = 'Assigns colors to confal pyramids';
const PyramidsColors = ColorMap({ ...NtCColors });
const DefaultClassColors = {
A: 0xFFC1C1,
B: 0xC8CFFF,
BII: 0x0059DA,
miB: 0x3BE8FB,
Z: 0x01F60E,
IC: 0xFA5CFB,
OPN: 0xE90000,
SYN: 0xFFFF01,
N: 0xF2F2F2,
};
const ErrorColor = Color(0xFFA10A);
const PyramidsColors = ColorMap({
NANT_Upr: DefaultClassColors.N,
NANT_Lwr: DefaultClassColors.N,
AA00_Upr: DefaultClassColors.A,
AA00_Lwr: DefaultClassColors.A,
AA02_Upr: DefaultClassColors.A,
AA02_Lwr: DefaultClassColors.A,
AA03_Upr: DefaultClassColors.A,
AA03_Lwr: DefaultClassColors.A,
AA04_Upr: DefaultClassColors.A,
AA04_Lwr: DefaultClassColors.A,
AA08_Upr: DefaultClassColors.A,
AA08_Lwr: DefaultClassColors.A,
AA09_Upr: DefaultClassColors.A,
AA09_Lwr: DefaultClassColors.A,
AA01_Upr: DefaultClassColors.A,
AA01_Lwr: DefaultClassColors.A,
AA05_Upr: DefaultClassColors.A,
AA05_Lwr: DefaultClassColors.A,
AA06_Upr: DefaultClassColors.A,
AA06_Lwr: DefaultClassColors.A,
AA10_Upr: DefaultClassColors.A,
AA10_Lwr: DefaultClassColors.A,
AA11_Upr: DefaultClassColors.A,
AA11_Lwr: DefaultClassColors.A,
AA07_Upr: DefaultClassColors.A,
AA07_Lwr: DefaultClassColors.A,
AA12_Upr: DefaultClassColors.A,
AA12_Lwr: DefaultClassColors.A,
AA13_Upr: DefaultClassColors.A,
AA13_Lwr: DefaultClassColors.A,
AB01_Upr: DefaultClassColors.A,
AB01_Lwr: DefaultClassColors.B,
AB02_Upr: DefaultClassColors.A,
AB02_Lwr: DefaultClassColors.B,
AB03_Upr: DefaultClassColors.A,
AB03_Lwr: DefaultClassColors.B,
AB04_Upr: DefaultClassColors.A,
AB04_Lwr: DefaultClassColors.B,
AB05_Upr: DefaultClassColors.A,
AB05_Lwr: DefaultClassColors.B,
BA01_Upr: DefaultClassColors.B,
BA01_Lwr: DefaultClassColors.A,
BA05_Upr: DefaultClassColors.B,
BA05_Lwr: DefaultClassColors.A,
BA09_Upr: DefaultClassColors.B,
BA09_Lwr: DefaultClassColors.A,
BA08_Upr: DefaultClassColors.BII,
BA08_Lwr: DefaultClassColors.A,
BA10_Upr: DefaultClassColors.B,
BA10_Lwr: DefaultClassColors.A,
BA13_Upr: DefaultClassColors.BII,
BA13_Lwr: DefaultClassColors.A,
BA16_Upr: DefaultClassColors.BII,
BA16_Lwr: DefaultClassColors.A,
BA17_Upr: DefaultClassColors.BII,
BA17_Lwr: DefaultClassColors.A,
BB00_Upr: DefaultClassColors.B,
BB00_Lwr: DefaultClassColors.B,
BB01_Upr: DefaultClassColors.B,
BB01_Lwr: DefaultClassColors.B,
BB17_Upr: DefaultClassColors.B,
BB17_Lwr: DefaultClassColors.B,
BB02_Upr: DefaultClassColors.B,
BB02_Lwr: DefaultClassColors.B,
BB03_Upr: DefaultClassColors.B,
BB03_Lwr: DefaultClassColors.B,
BB11_Upr: DefaultClassColors.B,
BB11_Lwr: DefaultClassColors.B,
BB16_Upr: DefaultClassColors.B,
BB16_Lwr: DefaultClassColors.B,
BB04_Upr: DefaultClassColors.B,
BB04_Lwr: DefaultClassColors.BII,
BB05_Upr: DefaultClassColors.B,
BB05_Lwr: DefaultClassColors.BII,
BB07_Upr: DefaultClassColors.BII,
BB07_Lwr: DefaultClassColors.BII,
BB08_Upr: DefaultClassColors.BII,
BB08_Lwr: DefaultClassColors.BII,
BB10_Upr: DefaultClassColors.miB,
BB10_Lwr: DefaultClassColors.miB,
BB12_Upr: DefaultClassColors.miB,
BB12_Lwr: DefaultClassColors.miB,
BB13_Upr: DefaultClassColors.miB,
BB13_Lwr: DefaultClassColors.miB,
BB14_Upr: DefaultClassColors.miB,
BB14_Lwr: DefaultClassColors.miB,
BB15_Upr: DefaultClassColors.miB,
BB15_Lwr: DefaultClassColors.miB,
BB20_Upr: DefaultClassColors.miB,
BB20_Lwr: DefaultClassColors.miB,
IC01_Upr: DefaultClassColors.IC,
IC01_Lwr: DefaultClassColors.IC,
IC02_Upr: DefaultClassColors.IC,
IC02_Lwr: DefaultClassColors.IC,
IC03_Upr: DefaultClassColors.IC,
IC03_Lwr: DefaultClassColors.IC,
IC04_Upr: DefaultClassColors.IC,
IC04_Lwr: DefaultClassColors.IC,
IC05_Upr: DefaultClassColors.IC,
IC05_Lwr: DefaultClassColors.IC,
IC06_Upr: DefaultClassColors.IC,
IC06_Lwr: DefaultClassColors.IC,
IC07_Upr: DefaultClassColors.IC,
IC07_Lwr: DefaultClassColors.IC,
OP01_Upr: DefaultClassColors.OPN,
OP01_Lwr: DefaultClassColors.OPN,
OP02_Upr: DefaultClassColors.OPN,
OP02_Lwr: DefaultClassColors.OPN,
OP03_Upr: DefaultClassColors.OPN,
OP03_Lwr: DefaultClassColors.OPN,
OP04_Upr: DefaultClassColors.OPN,
OP04_Lwr: DefaultClassColors.OPN,
OP05_Upr: DefaultClassColors.OPN,
OP05_Lwr: DefaultClassColors.OPN,
OP06_Upr: DefaultClassColors.OPN,
OP06_Lwr: DefaultClassColors.OPN,
OP07_Upr: DefaultClassColors.OPN,
OP07_Lwr: DefaultClassColors.OPN,
OP08_Upr: DefaultClassColors.OPN,
OP08_Lwr: DefaultClassColors.OPN,
OP09_Upr: DefaultClassColors.OPN,
OP09_Lwr: DefaultClassColors.OPN,
OP10_Upr: DefaultClassColors.OPN,
OP10_Lwr: DefaultClassColors.OPN,
OP11_Upr: DefaultClassColors.OPN,
OP11_Lwr: DefaultClassColors.OPN,
OP12_Upr: DefaultClassColors.OPN,
OP12_Lwr: DefaultClassColors.OPN,
OP13_Upr: DefaultClassColors.OPN,
OP13_Lwr: DefaultClassColors.OPN,
OP14_Upr: DefaultClassColors.OPN,
OP14_Lwr: DefaultClassColors.OPN,
OP15_Upr: DefaultClassColors.OPN,
OP15_Lwr: DefaultClassColors.OPN,
OP16_Upr: DefaultClassColors.OPN,
OP16_Lwr: DefaultClassColors.OPN,
OP17_Upr: DefaultClassColors.OPN,
OP17_Lwr: DefaultClassColors.OPN,
OP18_Upr: DefaultClassColors.OPN,
OP18_Lwr: DefaultClassColors.OPN,
OP19_Upr: DefaultClassColors.OPN,
OP19_Lwr: DefaultClassColors.OPN,
OP20_Upr: DefaultClassColors.OPN,
OP20_Lwr: DefaultClassColors.OPN,
OP21_Upr: DefaultClassColors.OPN,
OP21_Lwr: DefaultClassColors.OPN,
OP22_Upr: DefaultClassColors.OPN,
OP22_Lwr: DefaultClassColors.OPN,
OP23_Upr: DefaultClassColors.OPN,
OP23_Lwr: DefaultClassColors.OPN,
OP24_Upr: DefaultClassColors.OPN,
OP24_Lwr: DefaultClassColors.OPN,
OP25_Upr: DefaultClassColors.OPN,
OP25_Lwr: DefaultClassColors.OPN,
OP26_Upr: DefaultClassColors.OPN,
OP26_Lwr: DefaultClassColors.OPN,
OP27_Upr: DefaultClassColors.OPN,
OP27_Lwr: DefaultClassColors.OPN,
OP28_Upr: DefaultClassColors.OPN,
OP28_Lwr: DefaultClassColors.OPN,
OP29_Upr: DefaultClassColors.OPN,
OP29_Lwr: DefaultClassColors.OPN,
OP30_Upr: DefaultClassColors.OPN,
OP30_Lwr: DefaultClassColors.OPN,
OP31_Upr: DefaultClassColors.OPN,
OP31_Lwr: DefaultClassColors.OPN,
OPS1_Upr: DefaultClassColors.OPN,
OPS1_Lwr: DefaultClassColors.OPN,
OP1S_Upr: DefaultClassColors.OPN,
OP1S_Lwr: DefaultClassColors.SYN,
AAS1_Upr: DefaultClassColors.SYN,
AAS1_Lwr: DefaultClassColors.A,
AB1S_Upr: DefaultClassColors.A,
AB1S_Lwr: DefaultClassColors.SYN,
AB2S_Upr: DefaultClassColors.A,
AB2S_Lwr: DefaultClassColors.SYN,
BB1S_Upr: DefaultClassColors.B,
BB1S_Lwr: DefaultClassColors.SYN,
BB2S_Upr: DefaultClassColors.B,
BB2S_Lwr: DefaultClassColors.SYN,
BBS1_Upr: DefaultClassColors.SYN,
BBS1_Lwr: DefaultClassColors.B,
ZZ01_Upr: DefaultClassColors.Z,
ZZ01_Lwr: DefaultClassColors.Z,
ZZ02_Upr: DefaultClassColors.Z,
ZZ02_Lwr: DefaultClassColors.Z,
ZZ1S_Upr: DefaultClassColors.Z,
ZZ1S_Lwr: DefaultClassColors.SYN,
ZZ2S_Upr: DefaultClassColors.Z,
ZZ2S_Lwr: DefaultClassColors.SYN,
ZZS1_Upr: DefaultClassColors.SYN,
ZZS1_Lwr: DefaultClassColors.Z,
ZZS2_Upr: DefaultClassColors.SYN,
ZZS2_Lwr: DefaultClassColors.Z,
});
type PyramidsColors = typeof PyramidsColors;
export const ConfalPyramidsColorThemeParams = {
@@ -66,7 +272,7 @@ export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramid
factory: ConfalPyramidsColorTheme,
getParams: getConfalPyramidsColorThemeParams,
defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && ConfalPyramidsProvider.ref(data.structure.models[0], false)

View File

@@ -5,18 +5,90 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { Dnatco, DnatcoParams, DnatcoSteps } from '../property';
import { ConfalPyramidsTypes as CPT } from './types';
import { Column, Table } from '../../../mol-data/db';
import { toTable } from '../../../mol-io/reader/cif/schema';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Model } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
export const ConfalPyramidsParams = { ...DnatcoParams };
export type ConfalPyramids = PropertyWrapper<CPT.Steps | undefined>;
export namespace ConfalPyramids {
export const Schema = {
ndb_struct_ntc_step: {
id: Column.Schema.int,
name: Column.Schema.str,
PDB_model_number: Column.Schema.int,
label_entity_id_1: Column.Schema.int,
label_asym_id_1: Column.Schema.str,
label_seq_id_1: Column.Schema.int,
label_comp_id_1: Column.Schema.str,
label_alt_id_1: Column.Schema.str,
label_entity_id_2: Column.Schema.int,
label_asym_id_2: Column.Schema.str,
label_seq_id_2: Column.Schema.int,
label_comp_id_2: Column.Schema.str,
label_alt_id_2: Column.Schema.str,
auth_asym_id_1: Column.Schema.str,
auth_seq_id_1: Column.Schema.int,
auth_asym_id_2: Column.Schema.str,
auth_seq_id_2: Column.Schema.int,
PDB_ins_code_1: Column.Schema.str,
PDB_ins_code_2: Column.Schema.str,
},
ndb_struct_ntc_step_summary: {
step_id: Column.Schema.int,
assigned_CANA: Column.Schema.str,
assigned_NtC: Column.Schema.str,
confal_score: Column.Schema.int,
euclidean_distance_NtC_ideal: Column.Schema.float,
cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
closest_CANA: Column.Schema.str,
closest_NtC: Column.Schema.str,
closest_step_golden: Column.Schema.str
}
};
export type Schema = typeof Schema;
export async function fromCif(ctx: CustomProperty.Context, model: Model, props: ConfalPyramidsProps): Promise<CustomProperty.Data<ConfalPyramids>> {
const info = PropertyWrapper.createInfo();
const data = getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const fromCif = createPyramidsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: fromCif } };
}
function getCifData(model: Model) {
if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
if (!hasNdbStructNtcCategories(model)) return undefined;
return {
steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
};
}
function hasNdbStructNtcCategories(model: Model): boolean {
if (!MmcifFormat.is(model.sourceData)) return false;
const names = (model.sourceData).data.frame.categoryNames;
return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
}
export function isApplicable(model?: Model): boolean {
return !!model && hasNdbStructNtcCategories(model);
}
}
export const ConfalPyramidsParams = {};
export type ConfalPyramidsParams = typeof ConfalPyramidsParams;
export type ConfalPyramidsProps = PD.Values<ConfalPyramidsParams>;
export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, DnatcoSteps> = CustomModelProperty.createProvider({
export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, ConfalPyramids> = CustomModelProperty.createProvider({
label: 'Confal Pyramids',
descriptor: CustomPropertyDescriptor({
name: 'confal_pyramids',
@@ -24,9 +96,94 @@ export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramids
type: 'static',
defaultParams: ConfalPyramidsParams,
getParams: (data: Model) => ConfalPyramidsParams,
isApplicable: (data: Model) => Dnatco.isApplicable(data),
isApplicable: (data: Model) => ConfalPyramids.isApplicable(data),
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ConfalPyramidsProps>) => {
const p = { ...PD.getDefaultValues(ConfalPyramidsParams), ...props };
return Dnatco.fromCif(ctx, data, p);
return ConfalPyramids.fromCif(ctx, data, p);
}
});
type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
function createPyramidsFromCif(
model: Model,
cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
stepsSummary: StepsSummaryTable
): CPT.Steps {
const steps = new Array<CPT.Step>();
const mapping = new Array<CPT.MappedChains>();
const {
id, PDB_model_number, name,
auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
_rowCount
} = cifSteps;
if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
for (let i = 0; i < _rowCount; i++) {
const {
NtC,
confal_score,
rmsd
} = getSummaryData(id.value(i), i, stepsSummary);
const modelNum = PDB_model_number.value(i);
const chainId = auth_asym_id_1.value(i);
const seqId = auth_seq_id_1.value(i);
const modelIdx = modelNum - 1;
if (mapping.length <= modelIdx || !mapping[modelIdx])
mapping[modelIdx] = new Map<string, CPT.MappedResidues>();
const step = {
PDB_model_number: modelNum,
name: name.value(i),
auth_asym_id_1: chainId,
auth_seq_id_1: seqId,
label_comp_id_1: label_comp_id_1.value(i),
label_alt_id_1: label_alt_id_1.value(i),
PDB_ins_code_1: PDB_ins_code_1.value(i),
auth_asym_id_2: auth_asym_id_2.value(i),
auth_seq_id_2: auth_seq_id_2.value(i),
label_comp_id_2: label_comp_id_2.value(i),
label_alt_id_2: label_alt_id_2.value(i),
PDB_ins_code_2: PDB_ins_code_2.value(i),
confal_score,
NtC,
rmsd,
};
steps.push(step);
const mappedChains = mapping[modelIdx];
const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
const stepsForResidue = residuesOnChain.get(seqId) ?? [];
stepsForResidue.push(steps.length - 1);
residuesOnChain.set(seqId, stepsForResidue);
mappedChains.set(chainId, residuesOnChain);
mapping[modelIdx] = mappedChains;
}
return { steps, mapping };
}
function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
const {
step_id,
confal_score,
assigned_NtC,
cartesian_rmsd_closest_NtC_representative,
} = stepsSummary;
// Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
for (let j = i; j < stepsSummary._rowCount; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
// Safety net for cases where the previous assumption is not met
for (let j = 0; j < i; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
throw new Error('Inconsistent mmCIF data');
}

View File

@@ -5,10 +5,9 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsIterator } from './util';
import { ConfalPyramidsTypes as CPT } from './types';
import { Dnatco } from '../property';
import { Interval } from '../../../mol-data/int';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
@@ -88,8 +87,6 @@ function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Str
const it = new ConfalPyramidsIterator(structure, unit);
while (it.hasNext) {
const allPoints = it.move();
if (!allPoints)
continue;
for (const points of allPoints) {
const { O3, P, OP1, OP2, O5, confalScore } = points;
@@ -153,7 +150,9 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro
if (halfPyramidsCount <= groupId) return EmptyLoci;
const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
return CPT.Loci(data.steps, [idx]);
const step = data.steps[idx];
return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]);
}
function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
@@ -198,7 +197,7 @@ export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvi
defaultValues: PD.getDefaultValues(ConfalPyramidsParams),
defaultColorTheme: { name: 'confal-pyramids' },
defaultSizeTheme: { name: 'uniform' },
isApplicable: (structure: Structure) => structure.models.some(m => Dnatco.isApplicable(m)),
isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true),
detach: (data) => ConfalPyramidsProvider.ref(data.model, false),

View File

@@ -5,29 +5,61 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from '../types';
import { DataLocation } from '../../../mol-model/location';
import { DataLoci } from '../../../mol-model/loci';
import { confalPyramidLabel } from './behavior';
export namespace ConfalPyramidsTypes {
export interface Location extends DataLocation<DnatcoTypes.HalfStep, {}> {}
export const DataTag = 'dnatco-confal-half-pyramid';
export function Location(step: DnatcoTypes.Step, isLower: boolean) {
return DataLocation(DnatcoTypes.DataTag, { step, isLower }, {});
export type Step = {
PDB_model_number: number,
name: string,
auth_asym_id_1: string,
auth_seq_id_1: number,
label_comp_id_1: string,
label_alt_id_1: string,
PDB_ins_code_1: string,
auth_asym_id_2: string,
auth_seq_id_2: number,
label_comp_id_2: string,
label_alt_id_2: string,
PDB_ins_code_2: string,
confal_score: number,
NtC: string,
rmsd: number,
}
export type MappedChains = Map<string, MappedResidues>;
export type MappedResidues = Map<number, number[]>;
export interface Steps {
steps: Array<Step>,
mapping: MappedChains[],
}
export interface HalfPyramid {
step: Step,
isLower: boolean,
}
export interface Location extends DataLocation<HalfPyramid, {}> {}
export function Location(step: Step, isLower: boolean) {
return DataLocation(DataTag, { step, isLower }, {});
}
export function isLocation(x: any): x is Location {
return !!x && x.kind === 'data-location' && x.tag === DnatcoTypes.DataTag;
return !!x && x.kind === 'data-location' && x.tag === DataTag;
}
export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
export interface Loci extends DataLoci<HalfPyramid, {}> {}
export function Loci(data: DnatcoTypes.Step[], elements: ReadonlyArray<number>): Loci {
return DataLoci(DnatcoTypes.DataTag, data, elements, undefined, () => elements[0] !== undefined ? confalPyramidLabel(data[elements[0]]) : '');
export function Loci(data: HalfPyramid, elements: ReadonlyArray<{}>): Loci {
return DataLoci(DataTag, data, elements, undefined, () => confalPyramidLabel(data));
}
export function isLoci(x: any): x is Loci {
return !!x && x.kind === 'data-loci' && x.tag === DnatcoTypes.DataTag;
return !!x && x.kind === 'data-loci' && x.tag === DataTag;
}
}

View File

@@ -1,15 +1,16 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramidsProvider } from './property';
import { DnatcoTypes } from '../types';
import { DnatcoUtil } from '../util';
import { ConfalPyramidsTypes as CPT } from './types';
import { Segmentation } from '../../../mol-data/int';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
type Residue = Segmentation.Segment<ResidueIndex>;
export type Pyramid = {
O3: ElementIndex,
@@ -21,17 +22,31 @@ export type Pyramid = {
stepIdx: number,
};
function getPyramid(
loc: StructureElement.Location,
one: DnatcoUtil.Residue, two: DnatcoUtil.Residue,
altIdOne: string, altIdTwo: string,
insCodeOne: string, insCodeTwo: string,
confalScore: number, stepIdx: number): Pyramid {
const O3 = DnatcoUtil.getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne, insCodeOne);
const P = DnatcoUtil.getAtomIndex(loc, two, ['P'], altIdTwo, insCodeTwo);
const OP1 = DnatcoUtil.getAtomIndex(loc, two, ['OP1'], altIdTwo, insCodeTwo);
const OP2 = DnatcoUtil.getAtomIndex(loc, two, ['OP2'], altIdTwo, insCodeTwo);
const O5 = DnatcoUtil.getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo, insCodeTwo);
const EmptyStepIndices = new Array<number>();
function copyResidue(r?: Residue) {
return r ? { index: r.index, start: r.start, end: r.end } : void 0;
}
function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex {
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = loc.unit.elements[eI];
const elName = StructureProperties.atom.label_atom_id(loc);
const elAltId = StructureProperties.atom.label_alt_id(loc);
if (names.includes(elName) && (elAltId === altId || elAltId.length === 0))
return loc.element;
}
return -1 as ElementIndex;
}
function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid {
const O3 = getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne);
const P = getAtomIndex(loc, two, ['P'], altIdTwo);
const OP1 = getAtomIndex(loc, two, ['OP1'], altIdTwo);
const OP2 = getAtomIndex(loc, two, ['OP2'], altIdTwo);
const O5 = getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo);
return { O3, P, OP1, OP2, O5, confalScore, stepIdx };
}
@@ -39,29 +54,39 @@ function getPyramid(
export class ConfalPyramidsIterator {
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
private residueOne?: DnatcoUtil.Residue;
private residueTwo: DnatcoUtil.Residue;
private data?: DnatcoTypes.Steps;
private residueOne?: Residue;
private residueTwo: Residue;
private data?: CPT.Steps;
private loc: StructureElement.Location;
private moveStep() {
this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
private getStepIndices(r: Residue) {
this.loc.element = this.loc.unit.elements[r.start];
// Check for discontinuity
if (this.residueTwo.index !== (this.residueOne!.index + 1))
return void 0;
const modelIdx = StructureProperties.unit.model_num(this.loc) - 1;
const chainId = StructureProperties.chain.auth_asym_id(this.loc);
const seqId = StructureProperties.residue.auth_seq_id(this.loc);
const chains = this.data!.mapping[modelIdx];
if (!chains) return EmptyStepIndices;
const residues = chains.get(chainId);
if (!residues) return EmptyStepIndices;
return residues.get(seqId) ?? EmptyStepIndices;
}
private moveStep() {
this.residueOne = copyResidue(this.residueTwo);
this.residueTwo = copyResidue(this.residueIt.move())!;
return this.toPyramids(this.residueOne!, this.residueTwo);
}
private toPyramids(one: DnatcoUtil.Residue, two: DnatcoUtil.Residue) {
const indices = DnatcoUtil.getStepIndices(this.data!, this.loc, one);
private toPyramids(one: Residue, two: Residue) {
const indices = this.getStepIndices(one);
const points = [];
for (const idx of indices) {
const step = this.data!.steps[idx];
points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.PDB_ins_code_1, step.PDB_ins_code_2, step.confal_score, idx));
points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx));
}
return points;
@@ -96,8 +121,6 @@ export class ConfalPyramidsIterator {
return this.moveStep();
} else {
this.residueIt.setSegment(this.chainIt.move());
if (this.residueIt.hasNext)
this.residueTwo = this.residueIt.move();
return this.moveStep();
}
}

View File

@@ -1,8 +1,8 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
export { DnatcoNtCs } from './behavior';
export { DnatcoConfalPyramids } from './confal-pyramids/behavior';

View File

@@ -1,57 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeColorThemeProvider } from './color';
import { NtCTubeProvider } from './property';
import { NtCTubeRepresentationProvider } from './representation';
import { DnatcoTypes } from '../types';
import { Dnatco } from '../property';
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
export const NtCTubePreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-ntc-tube',
display: {
name: 'NtC Tube', group: 'Annotation',
description: 'NtC Tube',
},
isApplicable(a) {
return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const model = structureCell?.obj?.data.model;
if (!structureCell || !model) return {};
await plugin.runTask(Task.create('NtC tube', async runtime => {
await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
}));
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
const tube = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'NtC Tube' });
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
let tubeRepr;
if (representations)
tubeRepr = builder.buildRepresentation(update, tube, { type: NtCTubeRepresentationProvider, typeParams, color: NtCTubeColorThemeProvider }, { tag: 'ntc-tube' });
await update.commit({ revertOnError: true });
return { components: { ...components, tube }, representations: { ...representations, tubeRepr } };
}
});
export function NtCTubeSegmentLabel(step: DnatcoTypes.Step) {
return `
<b>${step.auth_asym_id_1}</b> |
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
<i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
`;
}

View File

@@ -1,99 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ErrorColor, NtCColors } from '../color';
import { NtCTubeProvider } from './property';
import { NtCTubeTypes as NTT } from './types';
import { Dnatco } from '../property';
import { Location } from '../../../mol-model/location';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorTheme } from '../../../mol-theme/color';
import { ThemeDataContext } from '../../../mol-theme/theme';
import { Color, ColorMap } from '../../../mol-util/color';
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';
const Description = 'Assigns colors to NtC Tube segments';
const NtCTubeColors = ColorMap({
...NtCColors,
residueMarker: Color(0x222222),
stepBoundaryMarker: Color(0x656565),
});
type NtCTubeColors = typeof NtCTubeColors;
export const NtCTubeColorThemeParams = {
colors: PD.MappedStatic('default', {
'default': PD.EmptyGroup(),
'custom': PD.Group(getColorMapParams(NtCTubeColors)),
'uniform': PD.Color(Color(0xEEEEEE)),
}),
markResidueBoundaries: PD.Boolean(true),
markSegmentBoundaries: PD.Boolean(true),
};
export type NtCTubeColorThemeParams = typeof NtCTubeColorThemeParams;
export function getNtCTubeColorThemeParams(ctx: ThemeDataContext) {
return PD.clone(NtCTubeColorThemeParams);
}
export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTubeColorThemeParams>): ColorTheme<NtCTubeColorThemeParams> {
const colorMap = props.colors.name === 'default'
? NtCTubeColors
: props.colors.name === 'custom'
? props.colors.params
: ColorMap({
...Object.fromEntries(ObjectKeys(NtCTubeColors).map(item => [item, props.colors.params])),
residueMarker: NtCTubeColors.residueMarker,
stepBoundaryMarker: NtCTubeColors.stepBoundaryMarker
}) as NtCTubeColors;
function color(location: Location, isSecondary: boolean): Color {
if (NTT.isLocation(location)) {
const { data } = location;
const { step, kind } = data;
let key;
if (kind === 'upper')
key = step.NtC + '_Upr' as keyof NtCTubeColors;
else if (kind === 'lower')
key = step.NtC + '_Lwr' as keyof NtCTubeColors;
else if (kind === 'residue-boundary')
key = (!props.markResidueBoundaries ? step.NtC + '_Lwr' : 'residueMarker') as keyof NtCTubeColors;
else /* segment-boundary */
key = (!props.markSegmentBoundaries ? step.NtC + '_Lwr' : 'stepBoundaryMarker') as keyof NtCTubeColors;
return colorMap[key] ?? ErrorColor;
}
return ErrorColor;
}
return {
factory: NtCTubeColorTheme,
granularity: 'group',
color,
props,
description: Description,
legend: TableLegend(ObjectKeys(colorMap).map(k => [k.replace('_', ' '), colorMap[k]] as [string, Color]).concat([['Error', ErrorColor]])),
};
}
export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = {
name: 'ntc-tube',
label: 'NtC Tube',
category: ColorTheme.Category.Residue,
factory: NtCTubeColorTheme,
getParams: getNtCTubeColorThemeParams,
defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? NtCTubeProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && NtCTubeProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -1,44 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeTypes as NTT } from './types';
import { Dnatco, DnatcoParams } from '../property';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Model } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export const NtCTubeParams = { ...DnatcoParams };
export type NtCTubeParams = typeof NtCTubeParams;
export type NtCTubeProps = PD.Values<NtCTubeParams>;
export type NtCTubeData = PropertyWrapper<NTT.Data | undefined>;
async function fromCif(ctx: CustomProperty.Context, model: Model, props: NtCTubeProps): Promise<CustomProperty.Data<NtCTubeData>> {
const info = PropertyWrapper.createInfo();
const data = Dnatco.getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const steps = Dnatco.getStepsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: { data: steps } } };
}
export const NtCTubeProvider: CustomModelProperty.Provider<NtCTubeParams, NtCTubeData> = CustomModelProperty.createProvider({
label: 'NtC Tube',
descriptor: CustomPropertyDescriptor({
name: 'ntc-tube',
}),
type: 'static',
defaultParams: NtCTubeParams,
getParams: (data: Model) => NtCTubeParams,
isApplicable: (data: Model) => Dnatco.isApplicable(data),
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<NtCTubeProps>) => {
const p = { ...PD.getDefaultValues(NtCTubeParams), ...props };
return fromCif(ctx, data, p);
}
});

View File

@@ -1,454 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeProvider } from './property';
import { NtCTubeSegmentsIterator } from './util';
import { NtCTubeTypes as NTT } from './types';
import { Dnatco } from '../property';
import { DnatcoTypes } from '../types';
import { DnatcoUtil } from '../util';
import { Interval } from '../../../mol-data/int';
import { BaseGeometry, VisualQuality } from '../../../mol-geo/geometry/base';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { addFixedCountDashedCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
import { PickingId } from '../../../mol-geo/geometry/picking';
import { CylinderProps } from '../../../mol-geo/primitive/cylinder';
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { smoothstep } from '../../../mol-math/interpolate';
import { NullLocation } from '../../../mol-model/location';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
import { structureUnion } from '../../../mol-model/structure/query/utils/structure-set';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
import { createCurveSegmentState, CurveSegmentState } from '../../../mol-repr/structure/visual/util/polymer';
import { getStructureQuality, VisualUpdateState } from '../../../mol-repr/util';
import { VisualContext } from '../../../mol-repr/visual';
import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
const v3add = Vec3.add;
const v3copy = Vec3.copy;
const v3cross = Vec3.cross;
const v3fromArray = Vec3.fromArray;
const v3matchDirection = Vec3.matchDirection;
const v3normalize = Vec3.normalize;
const v3orthogonalize = Vec3.orthogonalize;
const v3scale = Vec3.scale;
const v3slerp = Vec3.slerp;
const v3spline = Vec3.spline;
const v3sub = Vec3.sub;
const v3toArray = Vec3.toArray;
const NtCTubeMeshParams = {
...UnitsMeshParams,
linearSegments: PD.Numeric(4, { min: 2, max: 8, step: 1 }, BaseGeometry.CustomQualityParamInfo),
radialSegments: PD.Numeric(22, { min: 4, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
residueMarkerWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
segmentBoundaryWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
};
type NtCTubeMeshParams = typeof NtCTubeMeshParams;
type QualityOptions = Exclude<VisualQuality, 'auto' | 'custom'>;
const LinearSegmentCount: Record<QualityOptions, number> = {
highest: 6,
higher: 6,
high: 4,
medium: 4,
low: 3,
lower: 3,
lowest: 2,
};
const RadialSegmentCount: Record<QualityOptions, number> = {
highest: 32,
higher: 26,
high: 22,
medium: 18,
low: 14,
lower: 10,
lowest: 6,
};
const _curvePoint = Vec3();
const _tanA = Vec3();
const _tanB = Vec3();
const _firstTangentVec = Vec3();
const _lastTangentVec = Vec3();
const _firstNormalVec = Vec3();
const _lastNormalVec = Vec3();
const _tmpNormal = Vec3();
const _tangentVec = Vec3();
const _normalVec = Vec3();
const _binormalVec = Vec3();
const _prevNormal = Vec3();
const _nextNormal = Vec3();
function interpolatePointsAndTangents(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, tRange: number[]) {
const { curvePoints, tangentVectors, linearSegments } = state;
const tension = 0.5;
const r = tRange[1] - tRange[0];
for (let j = 0; j <= linearSegments; ++j) {
const t = j * r / linearSegments + tRange[0];
v3spline(_curvePoint, p0, p1, p2, p3, t, tension);
v3spline(_tanA, p0, p1, p2, p3, t - 0.01, tension);
v3spline(_tanB, p0, p1, p2, p3, t + 0.01, tension);
v3toArray(_curvePoint, curvePoints, j * 3);
v3normalize(_tangentVec, v3sub(_tangentVec, _tanA, _tanB));
v3toArray(_tangentVec, tangentVectors, j * 3);
}
}
function interpolateNormals(state: CurveSegmentState, firstDirection: Vec3, lastDirection: Vec3) {
const { curvePoints, tangentVectors, normalVectors, binormalVectors } = state;
const n = curvePoints.length / 3;
v3fromArray(_firstTangentVec, tangentVectors, 0);
v3fromArray(_lastTangentVec, tangentVectors, (n - 1) * 3);
v3orthogonalize(_firstNormalVec, _firstTangentVec, firstDirection);
v3orthogonalize(_lastNormalVec, _lastTangentVec, lastDirection);
v3matchDirection(_lastNormalVec, _lastNormalVec, _firstNormalVec);
v3copy(_prevNormal, _firstNormalVec);
const n1 = n - 1;
for (let i = 0; i < n; ++i) {
const j = smoothstep(0, n1, i) * n1;
const t = i === 0 ? 0 : 1 / (n - j);
v3fromArray(_tangentVec, tangentVectors, i * 3);
v3orthogonalize(_normalVec, _tangentVec, v3slerp(_tmpNormal, _prevNormal, _lastNormalVec, t));
v3toArray(_normalVec, normalVectors, i * 3);
v3copy(_prevNormal, _normalVec);
v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
v3toArray(_binormalVec, binormalVectors, i * 3);
}
for (let i = 1; i < n1; ++i) {
v3fromArray(_prevNormal, normalVectors, (i - 1) * 3);
v3fromArray(_normalVec, normalVectors, i * 3);
v3fromArray(_nextNormal, normalVectors, (i + 1) * 3);
v3scale(_normalVec, v3add(_normalVec, _prevNormal, v3add(_normalVec, _nextNormal, _normalVec)), 1 / 3);
v3toArray(_normalVec, normalVectors, i * 3);
v3fromArray(_tangentVec, tangentVectors, i * 3);
v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
v3toArray(_binormalVec, binormalVectors, i * 3);
}
}
function interpolate(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, firstDir: Vec3, lastDir: Vec3, tRange = [0, 1]) {
interpolatePointsAndTangents(state, p0, p1, p2, p3, tRange);
interpolateNormals(state, firstDir, lastDir);
}
function createNtCTubeSegmentsIterator(structureGroup: StructureGroup): LocationIterator {
const { structure, group } = structureGroup;
const instanceCount = group.units.length;
const data = NtCTubeProvider.get(structure.model)?.value?.data;
if (!data) return LocationIterator(0, 1, 1, () => NullLocation);
const numBlocks = data.data.steps.length * 4;
const getLocation = (groupId: number, instanceId: number) => {
if (groupId > numBlocks) return NullLocation;
const stepIdx = Math.floor(groupId / 4);
const step = data.data.steps[stepIdx];
const r = groupId % 4;
const kind =
r === 0 ? 'upper' :
r === 1 ? 'lower' :
r === 2 ? 'residue-boundary' : 'segment-boundary';
return NTT.Location({ step, kind });
};
return LocationIterator(totalMeshGroupsCount(data.data.steps) + 1, instanceCount, 1, getLocation);
}
function segmentCount(structure: Structure, props: PD.Values<NtCTubeMeshParams>): { linear: number, radial: number } {
const quality = props.quality;
if (quality === 'custom')
return { linear: props.linearSegments, radial: props.radialSegments };
else if (quality === 'auto') {
const autoQuality = getStructureQuality(structure) as QualityOptions;
return { linear: LinearSegmentCount[autoQuality], radial: RadialSegmentCount[autoQuality] };
} else
return { linear: LinearSegmentCount[quality], radial: RadialSegmentCount[quality] };
}
function stepBoundingSphere(step: DnatcoTypes.Step, struLoci: StructureElement.Loci): Sphere3D | undefined {
const one = DnatcoUtil.residueToLoci(step.auth_asym_id_1, step.auth_seq_id_1, step.label_alt_id_1, step.PDB_ins_code_1, struLoci, 'auth');
const two = DnatcoUtil.residueToLoci(step.auth_asym_id_2, step.auth_seq_id_2, step.label_alt_id_2, step.PDB_ins_code_2, struLoci, 'auth');
if (StructureElement.Loci.is(one) && StructureElement.Loci.is(two)) {
const union = structureUnion(struLoci.structure, [StructureElement.Loci.toStructure(one), StructureElement.Loci.toStructure(two)]);
return union.boundary.sphere;
}
return void 0;
}
function totalMeshGroupsCount(steps: DnatcoTypes.Step[]) {
// Each segment has two blocks, Residue Boundary marker and a Segment Boundary marker
return steps.length * 4 - 1; // Subtract one because the last Segment Boundary marker is not drawn
}
function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<NtCTubeMeshParams>, mesh?: Mesh) {
if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
const prop = NtCTubeProvider.get(structure.model).value;
if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
const { data } = prop.data;
if (data.steps.length === 0) return Mesh.createEmpty(mesh);
const MarkerLinearSegmentCount = 2;
const segCount = segmentCount(structure, props);
const vertexCount = Math.floor((segCount.linear * 4 * data.steps.length / structure.model.atomicHierarchy.chains._rowCount) * segCount.radial);
const chunkSize = Math.floor(vertexCount / 3);
const diameter = 1.0 * theme.size.props.value;
const mb = MeshBuilder.createState(vertexCount, chunkSize, mesh);
const state = createCurveSegmentState(segCount.linear);
const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state;
for (let idx = 0; idx <= segCount.linear; idx++) {
widthValues[idx] = diameter;
heightValues[idx] = diameter;
}
const [normals, binormals] = [binormalVectors, normalVectors]; // Needed so that the tube is not drawn from inside out
const markerState = createCurveSegmentState(MarkerLinearSegmentCount);
const { curvePoints: mCurvePoints, normalVectors: mNormalVectors, binormalVectors: mBinormalVectors, widthValues: mWidthValues, heightValues: mHeightValues } = markerState;
for (let idx = 0; idx <= MarkerLinearSegmentCount; idx++) {
mWidthValues[idx] = diameter;
mHeightValues[idx] = diameter;
}
const [mNormals, mBinormals] = [mBinormalVectors, mNormalVectors];
const firstDir = Vec3();
const lastDir = Vec3();
const markerDir = Vec3();
const residueMarkerWidth = props.residueMarkerWidth / 2;
const it = new NtCTubeSegmentsIterator(structure, unit);
while (it.hasNext) {
const segment = it.move();
if (!segment)
continue;
const { p_1, p0, p1, p2, p3, p4, pP } = segment;
const FirstBlockId = segment.stepIdx * 4;
const SecondBlockId = FirstBlockId + 1;
const ResidueMarkerId = FirstBlockId + 2;
const SegmentBoundaryMarkerId = FirstBlockId + 3;
const { rmShift, rmPos } = calcResidueMarkerShift(p2, p3, pP);
if (segment.firstInChain) {
v3normalize(firstDir, v3sub(firstDir, p2, p1));
v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
} else {
v3copy(firstDir, lastDir);
v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
}
// C5' -> O3' block
interpolate(state, p0, p1, p2, p3, firstDir, lastDir);
mb.currentGroup = FirstBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, segment.firstInChain || segment.followsGap, false, 'rounded');
// O3' -> C5' block
v3copy(firstDir, lastDir);
v3normalize(markerDir, v3sub(markerDir, p3, rmPos));
v3normalize(lastDir, v3sub(lastDir, p4, p3));
// From O3' to the residue marker
interpolate(state, p1, p2, p3, p4, firstDir, markerDir, [0, rmShift - residueMarkerWidth]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
// Residue marker
interpolate(markerState, p1, p2, p3, p4, markerDir, markerDir, [rmShift - residueMarkerWidth, rmShift + residueMarkerWidth]);
mb.currentGroup = ResidueMarkerId;
addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
if (segment.capEnd) {
// From the residue marker to C5' of the end
interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, true, 'rounded');
} else {
// From the residue marker to C5' of the step boundary marker
interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1 - props.segmentBoundaryWidth]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
// Step boundary marker
interpolate(markerState, p1, p2, p3, p4, lastDir, lastDir, [1 - props.segmentBoundaryWidth, 1]);
mb.currentGroup = SegmentBoundaryMarkerId;
addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
}
if (segment.followsGap) {
const cylinderProps: CylinderProps = {
radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial,
};
mb.currentGroup = FirstBlockId;
addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, false, cylinderProps);
}
}
const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1.05);
const m = MeshBuilder.getMesh(mb);
m.setBoundingSphere(boundingSphere);
return m;
}
const _rmvCO = Vec3();
const _rmvPO = Vec3();
const _rmPos = Vec3();
const _HalfPi = Math.PI / 2;
function calcResidueMarkerShift(pO: Vec3, pC: Vec3, pP: Vec3): { rmShift: number, rmPos: Vec3 } {
v3sub(_rmvCO, pC, pO);
v3sub(_rmvPO, pP, pO);
// Project position of P atom on the O3' -> C5' vector
const beta = Vec3.angle(_rmvPO, _rmvCO);
const alpha = _HalfPi - Math.abs(beta);
const lengthMO = Math.cos(alpha) * Vec3.magnitude(_rmvPO);
const shift = lengthMO / Vec3.magnitude(_rmvCO);
v3scale(_rmvCO, _rmvCO, shift);
v3add(_rmPos, _rmvCO, pO);
return { rmShift: shift, rmPos: _rmPos };
}
function getNtCTubeSegmentLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
const { groupId, objectId, instanceId } = pickingId;
if (objectId !== id) return EmptyLoci;
const { structure } = structureGroup;
const unit = structureGroup.group.units[instanceId];
if (!Unit.isAtomic(unit)) return EmptyLoci;
const data = NtCTubeProvider.get(structure.model)?.value?.data ?? undefined;
if (!data) return EmptyLoci;
const MeshGroupsCount = totalMeshGroupsCount(data.data.steps);
if (groupId > MeshGroupsCount) return EmptyLoci;
const stepIdx = Math.floor(groupId / 4);
const bs = stepBoundingSphere(data.data.steps[stepIdx], Structure.toStructureElementLoci(structure));
/*
* NOTE 1) Each step is drawn with 4 mesh groups. We need to divide/multiply by 4 to convert between steps and mesh groups.
* NOTE 2) Molstar will create a mesh only for the asymmetric unit. When the entire biological assembly
* is displayed, Molstar just copies and transforms the mesh. This means that even though the mesh
* might be displayed multiple times, groupIds of the individual blocks in the mesh will be the same.
* If there are multiple copies of a mesh, Molstar needs to be able to tell which block belongs to which copy of the mesh.
* To do that, Molstar adds an offset to groupIds of the copied meshes. Offset is calculated as follows:
*
* offset = NumberOfBlocks * UnitIndex
*
* "NumberOfBlocks" is the number of valid Location objects got from LocationIterator *or* the greatest groupId set by
* the mesh generator - whichever is smaller.
*
* UnitIndex is the index of the Unit the mesh belongs to, starting from 0. (See "unitMap" in the Structure object).
* We can also get this index from the value "instanceId" of the "pickingId" object.
*
* If this offset is not applied, picking a piece of one of the copied meshes would actually pick that piece in the original mesh.
* This is particularly apparent with highlighting - hovering over items in a copied mesh incorrectly highlights those items in the source mesh.
*
* Molstar can take advantage of the fact that ElementLoci has a reference to the Unit object attached to it. Since we cannot attach ElementLoci
* to a step, we need to calculate the offseted groupId here and pass it as part of the DataLoci.
*/
const offsetGroupId = stepIdx * 4 + (MeshGroupsCount + 1) * instanceId;
return NTT.Loci(data.data.steps, [stepIdx], [offsetGroupId], bs);
}
function eachNtCTubeSegment(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
if (NTT.isLoci(loci)) {
const offsetGroupId = loci.elements[0];
return apply(Interval.ofBounds(offsetGroupId, offsetGroupId + 4));
}
return false;
}
function NtCTubeVisual(materialId: number): UnitsVisual<NtCTubeMeshParams> {
return UnitsMeshVisual<NtCTubeMeshParams>({
defaultProps: PD.getDefaultValues(NtCTubeMeshParams),
createGeometry: createNtCTubeMesh,
createLocationIterator: createNtCTubeSegmentsIterator,
getLoci: getNtCTubeSegmentLoci,
eachLocation: eachNtCTubeSegment,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NtCTubeMeshParams>, currentProps: PD.Values<NtCTubeMeshParams>) => {
state.createGeometry = (
newProps.quality !== currentProps.quality ||
newProps.residueMarkerWidth !== currentProps.residueMarkerWidth ||
newProps.segmentBoundaryWidth !== currentProps.segmentBoundaryWidth ||
newProps.doubleSided !== currentProps.doubleSided ||
newProps.alpha !== currentProps.alpha ||
newProps.linearSegments !== currentProps.linearSegments ||
newProps.radialSegments !== currentProps.radialSegments
);
}
}, materialId);
}
const NtCTubeVisuals = {
'ntc-tube-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeMeshParams>) => UnitsRepresentation('NtC Tube Mesh', ctx, getParams, NtCTubeVisual),
};
export const NtCTubeParams = {
...NtCTubeMeshParams
};
export type NtCTubeParams = typeof NtCTubeParams;
export function getNtCTubeParams(ctx: ThemeRegistryContext, structure: Structure) {
return PD.clone(NtCTubeParams);
}
export type NtCTubeRepresentation = StructureRepresentation<NtCTubeParams>;
export function NtCTubeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeParams>): NtCTubeRepresentation {
return Representation.createMulti('NtC Tube', ctx, getParams, StructureRepresentationStateBuilder, NtCTubeVisuals as unknown as Representation.Def<Structure, NtCTubeParams>);
}
export const NtCTubeRepresentationProvider = StructureRepresentationProvider({
name: 'ntc-tube',
label: 'NtC Tube',
description: 'Displays schematic representation of NtC conformers',
factory: NtCTubeRepresentation,
getParams: getNtCTubeParams,
defaultValues: PD.getDefaultValues(NtCTubeParams),
defaultColorTheme: { name: 'ntc-tube' },
defaultSizeTheme: { name: 'uniform', props: { value: 2.0 } },
isApplicable: (structure: Structure) => structure.models.every(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: async (ctx: CustomProperty.Context, structure: Structure) => structure.models.forEach(m => NtCTubeProvider.attach(ctx, m, void 0, true)),
detach: (data) => data.models.forEach(m => NtCTubeProvider.ref(m, false)),
},
});

View File

@@ -1,51 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeSegmentLabel } from './behavior';
import { DnatcoTypes } from '../types';
import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
import { DataLocation } from '../../../mol-model/location';
import { DataLoci } from '../../../mol-model/loci';
export namespace NtCTubeTypes {
const DataTag = 'dnatco-tube-segment-data';
const DummyTag = 'dnatco-tube-dummy';
export type Data = {
data: DnatcoTypes.Steps,
}
export type TubeBlock = {
step: DnatcoTypes.Step,
kind: 'upper' | 'lower' | 'residue-boundary' | 'segment-boundary';
}
export interface Location extends DataLocation<TubeBlock> {}
export function Location(payload: TubeBlock) {
return DataLocation(DataTag, payload, {});
}
export function isLocation(x: any): x is Location {
return !!x && x.kind === 'data-location' && x.tag === DataTag;
}
export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
export interface DummyLoci extends DataLoci<{}, number> {}
export function Loci(data: DnatcoTypes.Step[], stepIndices: number[], elements: number[], boundingSphere?: Sphere3D): Loci {
return DataLoci(DataTag, data, elements, boundingSphere ? () => boundingSphere : undefined, () => stepIndices[0] !== undefined ? NtCTubeSegmentLabel(data[stepIndices[0]]) : '');
}
export function DummyLoci(): DummyLoci {
return DataLoci(DummyTag, {}, [], undefined, () => '');
}
export function isLoci(x: any): x is Loci {
return !!x && x.kind === 'data-loci' && x.tag === DataTag;
}
}

View File

@@ -1,214 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeTypes as NTT } from './types';
import { NtCTubeProvider } from './property';
import { DnatcoUtil } from '../util';
import { Segmentation, SortedArray } from '../../../mol-data/int';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
function getAtomPosition(vec: Vec3, loc: StructureElement.Location, residue: DnatcoUtil.Residue, names: string[], altId: string, insCode: string) {
const eI = DnatcoUtil.getAtomIndex(loc, residue, names, altId, insCode);
if (eI !== -1) {
loc.unit.conformation.invariantPosition(eI, vec);
return true;
}
return false; // Atom not found
}
const p_1 = Vec3();
const p0 = Vec3();
const p1 = Vec3();
const p2 = Vec3();
const p3 = Vec3();
const p4 = Vec3();
const pP = Vec3();
const C5PrimeNames = ['C5\'', 'C5*'];
const O3PrimeNames = ['O3\'', 'O3*'];
const O5PrimeNames = ['O5\'', 'O5*'];
const PNames = ['P'];
function getPoints(
loc: StructureElement.Location,
r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue,
altId0: string, altId1: string, altId2: string,
insCode0: string, insCode1: string, insCode2: string,
) {
if (r0) {
if (!getAtomPosition(p_1, loc, r0, C5PrimeNames, altId0, insCode0))
return void 0;
if (!getAtomPosition(p0, loc, r0, O3PrimeNames, altId0, insCode0))
return void 0;
} else {
if (!getAtomPosition(p0, loc, r1, O5PrimeNames, altId1, insCode1))
return void 0;
}
if (!getAtomPosition(p1, loc, r1, C5PrimeNames, altId1, insCode1))
return void 0;
if (!getAtomPosition(p2, loc, r1, O3PrimeNames, altId1, insCode1))
return void 0;
if (!getAtomPosition(p3, loc, r2, C5PrimeNames, altId2, insCode2))
return void 0;
if (!getAtomPosition(p4, loc, r2, O3PrimeNames, altId2, insCode2))
return void 0;
if (!getAtomPosition(pP, loc, r2, PNames, altId2, insCode2))
return void 0;
return { p_1, p0, p1, p2, p3, p4, pP };
}
function hasGapElements(r: DnatcoUtil.Residue, unit: Unit) {
for (let xI = r.start; xI < r.end; xI++) {
const eI = unit.elements[xI];
if (SortedArray.has(unit.gapElements, eI)) {
return true;
}
}
return false;
}
export type NtCTubeSegment = {
p_1: Vec3,
p0: Vec3,
p1: Vec3,
p2: Vec3,
p3: Vec3,
p4: Vec3,
pP: Vec3,
stepIdx: number,
followsGap: boolean,
firstInChain: boolean,
capEnd: boolean,
}
export class NtCTubeSegmentsIterator {
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
/* Second residue of the previous step, may be undefined
* if we are at the beginning of a chain or right after a discontinuity */
private residuePrev?: DnatcoUtil.Residue;
/* First residue of the current step */
private residueOne?: DnatcoUtil.Residue;
/* Second residue of the current step */
private residueTwo: DnatcoUtil.Residue;
/* First residue of the next step, may be undefined
* if we are at the end of a chain.
* Undefined value indicates that the iterator has reached the end.*/
private residueNext?: DnatcoUtil.Residue;
private data?: NTT.Data;
private altIdOne = '';
private insCodeOne = '';
private loc: StructureElement.Location;
private moveStep() {
if (!this.residueNext)
return void 0;
/* Assume discontinuity of the ResidueIndex of the residue that would become residue one (= first residue of the corresponding step)
* does not equal to ResidueIndex of what would be residue two (= second residue of the corresponding step). */
if (this.residueTwo.index + 1 === this.residueNext.index) {
this.residuePrev = DnatcoUtil.copyResidue(this.residueOne);
this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
this.residueTwo = DnatcoUtil.copyResidue(this.residueNext)!;
this.residueNext = this.residueIt.hasNext ? DnatcoUtil.copyResidue(this.residueIt.move())! : void 0;
} else {
if (!this.residueIt.hasNext) {
this.residueNext = void 0;
return void 0;
}
// There is discontinuity, act as if we were at the beginning of a chain
this.residuePrev = void 0;
this.residueOne = DnatcoUtil.copyResidue(this.residueNext);
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
this.residueNext = this.residueIt.hasNext ? DnatcoUtil.copyResidue(this.residueIt.move())! : void 0;
}
return this.toSegment(this.residuePrev, this.residueOne!, this.residueTwo, this.residueNext);
}
private prime() {
if (this.residueIt.hasNext)
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
if (this.residueIt.hasNext)
this.residueNext = this.residueIt.move();
}
private toSegment(r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue, r3: DnatcoUtil.Residue | undefined): NtCTubeSegment | undefined {
const indices = DnatcoUtil.getStepIndices(this.data!.data, this.loc, r1);
if (indices.length === 0)
return void 0;
const stepIdx = indices[0];
const step = this.data!.data.steps[stepIdx];
const altIdPrev = this.altIdOne;
const insCodePrev = this.insCodeOne;
this.altIdOne = step.label_alt_id_1;
this.insCodeOne = step.PDB_ins_code_1;
const altIdTwo = step.label_alt_id_2;
const insCodeTwo = step.PDB_ins_code_2;
const followsGap = !!r0 && hasGapElements(r0, this.loc.unit) && hasGapElements(r1, this.loc.unit);
const precedesDiscontinuity = r3 ? r3.index !== r2.index + 1 : false;
const points = getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo);
if (!points)
return void 0;
return {
...points,
stepIdx,
followsGap,
firstInChain: !r0,
capEnd: !this.residueNext || precedesDiscontinuity || hasGapElements(r2, this.loc.unit),
};
}
constructor(structure: Structure, unit: Unit.Atomic) {
this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
const prop = NtCTubeProvider.get(unit.model).value;
this.data = prop?.data;
if (this.chainIt.hasNext) {
this.residueIt.setSegment(this.chainIt.move());
this.prime();
}
this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
}
get hasNext() {
if (!this.data)
return false;
return !!this.residueNext
? true
: this.chainIt.hasNext;
}
move() {
if (!!this.residueNext) {
return this.moveStep();
} else {
this.residuePrev = void 0; // Assume discontinuity when we switch chains
this.residueNext = void 0;
this.residueIt.setSegment(this.chainIt.move());
this.prime();
return this.moveStep();
}
}
}

View File

@@ -1,172 +0,0 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from './types';
import { Column, Table } from '../../mol-data/db';
import { toTable } from '../../mol-io/reader/cif/schema';
import { Model } from '../../mol-model/structure';
import { CustomProperty } from '../../mol-model-props/common/custom-property';
import { PropertyWrapper } from '../../mol-model-props/common/wrapper';
import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
export type DnatcoSteps = PropertyWrapper<DnatcoTypes.Steps | undefined>;
export const DnatcoParams = {};
export type DnatcoParams = typeof DnatcoParams;
export type DnatcoProps = PD.Values<DnatcoParams>;
export namespace Dnatco {
export const Schema = {
ndb_struct_ntc_step: {
id: Column.Schema.int,
name: Column.Schema.str,
PDB_model_number: Column.Schema.int,
label_entity_id_1: Column.Schema.int,
label_asym_id_1: Column.Schema.str,
label_seq_id_1: Column.Schema.int,
label_comp_id_1: Column.Schema.str,
label_alt_id_1: Column.Schema.str,
label_entity_id_2: Column.Schema.int,
label_asym_id_2: Column.Schema.str,
label_seq_id_2: Column.Schema.int,
label_comp_id_2: Column.Schema.str,
label_alt_id_2: Column.Schema.str,
auth_asym_id_1: Column.Schema.str,
auth_seq_id_1: Column.Schema.int,
auth_asym_id_2: Column.Schema.str,
auth_seq_id_2: Column.Schema.int,
PDB_ins_code_1: Column.Schema.str,
PDB_ins_code_2: Column.Schema.str,
},
ndb_struct_ntc_step_summary: {
step_id: Column.Schema.int,
assigned_CANA: Column.Schema.str,
assigned_NtC: Column.Schema.str,
confal_score: Column.Schema.int,
euclidean_distance_NtC_ideal: Column.Schema.float,
cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
closest_CANA: Column.Schema.str,
closest_NtC: Column.Schema.str,
closest_step_golden: Column.Schema.str
}
};
export type Schema = typeof Schema;
export function getStepsFromCif(
model: Model,
cifSteps: Table<typeof Dnatco.Schema.ndb_struct_ntc_step>,
stepsSummary: StepsSummaryTable
): DnatcoTypes.Steps {
const steps = new Array<DnatcoTypes.Step>();
const mapping = new Array<DnatcoTypes.MappedChains>();
const {
id, PDB_model_number, name,
auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
_rowCount
} = cifSteps;
if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
for (let i = 0; i < _rowCount; i++) {
const {
NtC,
confal_score,
rmsd
} = getSummaryData(id.value(i), i, stepsSummary);
const modelNum = PDB_model_number.value(i);
const chainId = auth_asym_id_1.value(i);
const seqId = auth_seq_id_1.value(i);
const modelIdx = modelNum - 1;
if (mapping.length <= modelIdx || !mapping[modelIdx])
mapping[modelIdx] = new Map<string, DnatcoTypes.MappedResidues>();
const step = {
PDB_model_number: modelNum,
name: name.value(i),
auth_asym_id_1: chainId,
auth_seq_id_1: seqId,
label_comp_id_1: label_comp_id_1.value(i),
label_alt_id_1: label_alt_id_1.value(i),
PDB_ins_code_1: PDB_ins_code_1.value(i),
auth_asym_id_2: auth_asym_id_2.value(i),
auth_seq_id_2: auth_seq_id_2.value(i),
label_comp_id_2: label_comp_id_2.value(i),
label_alt_id_2: label_alt_id_2.value(i),
PDB_ins_code_2: PDB_ins_code_2.value(i),
confal_score,
NtC,
rmsd,
};
steps.push(step);
const mappedChains = mapping[modelIdx];
const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
const stepsForResidue = residuesOnChain.get(seqId) ?? [];
stepsForResidue.push(steps.length - 1);
residuesOnChain.set(seqId, stepsForResidue);
mappedChains.set(chainId, residuesOnChain);
mapping[modelIdx] = mappedChains;
}
return { steps, mapping };
}
export async function fromCif(ctx: CustomProperty.Context, model: Model, props: DnatcoProps): Promise<CustomProperty.Data<DnatcoSteps>> {
const info = PropertyWrapper.createInfo();
const data = getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const fromCif = getStepsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: fromCif } };
}
export function getCifData(model: Model) {
if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
if (!hasNdbStructNtcCategories(model)) return undefined;
return {
steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
};
}
function hasNdbStructNtcCategories(model: Model): boolean {
if (!MmcifFormat.is(model.sourceData)) return false;
const names = (model.sourceData).data.frame.categoryNames;
return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
}
export function isApplicable(model?: Model): boolean {
return !!model && hasNdbStructNtcCategories(model);
}
}
type StepsSummaryTable = Table<typeof Dnatco.Schema.ndb_struct_ntc_step_summary>;
function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
const {
step_id,
confal_score,
assigned_NtC,
cartesian_rmsd_closest_NtC_representative,
} = stepsSummary;
// Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
for (let j = i; j < stepsSummary._rowCount; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
// Safety net for cases where the previous assumption is not met
for (let j = 0; j < i; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
throw new Error('Inconsistent mmCIF data');
}

View File

@@ -1,41 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
export namespace DnatcoTypes {
export const DataTag = 'dnatco-confal-half-step';
export type Step = {
PDB_model_number: number,
name: string,
auth_asym_id_1: string,
auth_seq_id_1: number,
label_comp_id_1: string,
label_alt_id_1: string,
PDB_ins_code_1: string,
auth_asym_id_2: string,
auth_seq_id_2: number,
label_comp_id_2: string,
label_alt_id_2: string,
PDB_ins_code_2: string,
confal_score: number,
NtC: string,
rmsd: number,
}
export type MappedChains = Map<string, MappedResidues>;
export type MappedResidues = Map<number, number[]>;
export interface Steps {
steps: Array<Step>,
mapping: MappedChains[],
}
export interface HalfStep {
step: Step,
isLower: boolean,
}
}

View File

@@ -1,117 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from './types';
import { OrderedSet, Segmentation } from '../../mol-data/int';
import { EmptyLoci } from '../../mol-model/loci';
import { ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
const EmptyStepIndices = new Array<number>();
export namespace DnatcoUtil {
export type Residue = Segmentation.Segment<ResidueIndex>;
export function copyResidue(r?: Residue) {
return r ? { index: r.index, start: r.start, end: r.end } : void 0;
}
export function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string, insCode: string): ElementIndex {
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = loc.unit.elements[eI];
const elName = StructureProperties.atom.label_atom_id(loc);
const elAltId = StructureProperties.atom.label_alt_id(loc);
const elInsCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
if (names.includes(elName) && (elAltId === altId || elAltId.length === 0) && (elInsCode === insCode))
return loc.element;
}
return -1 as ElementIndex;
}
export function getStepIndices(data: DnatcoTypes.Steps, loc: StructureElement.Location, r: DnatcoUtil.Residue) {
loc.element = loc.unit.elements[r.start];
const modelIdx = StructureProperties.unit.model_num(loc) - 1;
const chainId = StructureProperties.chain.auth_asym_id(loc);
const seqId = StructureProperties.residue.auth_seq_id(loc);
const insCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
const chains = data.mapping[modelIdx];
if (!chains) return EmptyStepIndices;
const residues = chains.get(chainId);
if (!residues) return EmptyStepIndices;
const indices = residues.get(seqId);
if (!indices) return EmptyStepIndices;
return insCode !== '' ? indices.filter(idx => data.steps[idx].PDB_ins_code_1 === insCode) : indices;
}
export function residueAltIds(structure: Structure, unit: Unit, residue: Residue) {
const altIds = new Array<string>();
const loc = StructureElement.Location.create(structure, unit);
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = OrderedSet.getAt(unit.elements, eI);
const altId = StructureProperties.atom.label_alt_id(loc);
if (altId !== '' && !altIds.includes(altId))
altIds.push(altId);
}
return altIds;
}
const _loc = StructureElement.Location.create();
export function residueToLoci(asymId: string, seqId: number, altId: string | undefined, insCode: string, loci: StructureElement.Loci, source: 'label' | 'auth') {
_loc.structure = loci.structure;
for (const e of loci.elements) {
_loc.unit = e.unit;
const getAsymId = source === 'label' ? StructureProperties.chain.label_asym_id : StructureProperties.chain.auth_asym_id;
const getSeqId = source === 'label' ? StructureProperties.residue.label_seq_id : StructureProperties.residue.auth_seq_id;
// Walk the entire unit and look for the requested residue
const chainIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.chainAtomSegments, e.unit.elements);
const residueIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.residueAtomSegments, e.unit.elements);
const elemIndex = (idx: number) => OrderedSet.getAt(e.unit.elements, idx);
while (chainIt.hasNext) {
const chain = chainIt.move();
_loc.element = elemIndex(chain.start);
const _asymId = getAsymId(_loc);
if (_asymId !== asymId)
continue; // Wrong chain, skip it
residueIt.setSegment(chain);
while (residueIt.hasNext) {
const residue = residueIt.move();
_loc.element = elemIndex(residue.start);
const _seqId = getSeqId(_loc);
if (_seqId === seqId) {
const _insCode = StructureProperties.residue.pdbx_PDB_ins_code(_loc);
if (_insCode !== insCode)
continue;
if (altId) {
const _altIds = residueAltIds(loci.structure, e.unit, residue);
if (!_altIds.includes(altId))
continue;
}
const start = residue.start as StructureElement.UnitIndex;
const end = residue.end as StructureElement.UnitIndex;
return StructureElement.Loci(
loci.structure,
[{ unit: e.unit, indices: OrderedSet.ofBounds(start, end) }]
);
}
}
}
}
return EmptyLoci;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -13,7 +13,7 @@ import { PLUGIN_VERSION } from '../../mol-plugin/version';
import { RuntimeContext } from '../../mol-task';
import { Color } from '../../mol-util/color/color';
import { fillSerial } from '../../mol-util/array';
import { NumberArray, assertUnreachable } from '../../mol-util/type-helpers';
import { NumberArray } from '../../mol-util/type-helpers';
import { MeshExporter, AddMeshInput, MeshGeoData } from './mesh-exporter';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
@@ -35,15 +35,6 @@ const BIN_CHUNK_TYPE = 0x004E4942;
const JSON_PAD_CHAR = 0x20;
const BIN_PAD_CHAR = 0x00;
function getPrimitiveMode(mode: 'points' | 'lines' | 'triangles'): number {
switch (mode) {
case 'points': return 0;
case 'lines': return 1;
case 'triangles': return 4;
default: assertUnreachable(mode);
}
}
export type GlbData = {
glb: Uint8Array
}
@@ -98,12 +89,12 @@ export class GlbExporter extends MeshExporter<GlbData> {
return accessorOffset;
}
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array | undefined, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
const tmpV = Vec3();
const stride = isGeoTexture ? 4 : 3;
const vertexArray = new Float32Array(vertexCount * 3);
let normalArray: Float32Array | undefined;
const normalArray = new Float32Array(vertexCount * 3);
let indexArray: Uint32Array | undefined;
// position
@@ -113,35 +104,32 @@ export class GlbExporter extends MeshExporter<GlbData> {
}
// normal
if (normals) {
normalArray = new Float32Array(vertexCount * 3);
for (let i = 0; i < vertexCount; ++i) {
v3fromArray(tmpV, normals, i * stride);
v3normalize(tmpV, tmpV);
v3toArray(tmpV, normalArray, i * 3);
}
for (let i = 0; i < vertexCount; ++i) {
v3fromArray(tmpV, normals, i * stride);
v3normalize(tmpV, tmpV);
v3toArray(tmpV, normalArray, i * 3);
}
// face
if (!isGeoTexture && indices) {
indexArray = indices.slice(0, drawCount);
if (!isGeoTexture) {
indexArray = indices!.slice(0, drawCount);
}
const [vertexMin, vertexMax] = GlbExporter.vec3MinMax(vertexArray);
let vertexBuffer = vertexArray.buffer;
let normalBuffer = normalArray?.buffer;
let indexBuffer = (isGeoTexture || !indexArray) ? undefined : indexArray.buffer;
let normalBuffer = normalArray.buffer;
let indexBuffer = isGeoTexture ? undefined : indexArray!.buffer;
if (!IsNativeEndianLittle) {
vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
if (normalBuffer) normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
if (!isGeoTexture) indexBuffer = flipByteOrder(new Uint8Array(indexBuffer!), 4);
}
return {
vertexAccessorIndex: this.addBuffer(vertexBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER, vertexMin, vertexMax),
normalAccessorIndex: normalBuffer ? this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER) : undefined,
indexAccessorIndex: (isGeoTexture || !indexBuffer) ? undefined : this.addBuffer(indexBuffer, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
normalAccessorIndex: this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER),
indexAccessorIndex: isGeoTexture ? undefined : this.addBuffer(indexBuffer!, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
};
}
@@ -170,8 +158,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
}
private addMaterial(metalness: number, roughness: number, doubleSided: boolean, alpha: boolean) {
const hash = `${metalness}|${roughness}|${doubleSided}`;
private addMaterial(metalness: number, roughness: number) {
const hash = `${metalness}|${roughness}`;
if (!this.materialMap.has(hash)) {
this.materialMap.set(hash, this.materials.length);
this.materials.push({
@@ -179,16 +167,14 @@ export class GlbExporter extends MeshExporter<GlbData> {
baseColorFactor: [1, 1, 1, 1],
metallicFactor: metalness,
roughnessFactor: roughness
},
doubleSided,
alphaMode: alpha ? 'BLEND' : 'OPAQUE',
}
});
}
return this.materialMap.get(hash)!;
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
const { mesh, values, isGeoTexture, webgl, ctx } = input;
const t = Mat4();
@@ -200,34 +186,32 @@ export class GlbExporter extends MeshExporter<GlbData> {
const instanceCount = values.uInstanceCount.ref.value;
const metalness = values.uMetalness.ref.value;
const roughness = values.uRoughness.ref.value;
const doubleSided = values.uDoubleSided?.ref.value || values.hasReflection.ref.value;
const alpha = values.uAlpha.ref.value < 1;
const material = this.addMaterial(metalness, roughness, doubleSided, alpha);
const material = this.addMaterial(metalness, roughness);
let interpolatedColors: Uint8Array | undefined;
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
if (colorType === 'volume' || colorType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedColors = GlbExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
interpolatedColors = GlbExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
}
let interpolatedOverpaint: Uint8Array | undefined;
if (webgl && mesh && overpaintType === 'volumeInstance') {
if (overpaintType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
}
let interpolatedTransparency: Uint8Array | undefined;
if (webgl && mesh && transparencyType === 'volumeInstance') {
if (transparencyType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
}
// instancing
const sameGeometryBuffers = mesh !== undefined;
const sameColorBuffer = sameGeometryBuffers && colorType !== 'instance' && !colorType.endsWith('Instance') && !dTransparency;
let vertexAccessorIndex: number;
let normalAccessorIndex: number | undefined;
let normalAccessorIndex: number;
let indexAccessorIndex: number | undefined;
let colorAccessorIndex: number;
let meshIndex: number;
@@ -251,7 +235,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
// create a color buffer if needed
if (instanceIndex === 0 || !sameColorBuffer) {
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
}
// glTF mesh
@@ -264,8 +248,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
COLOR_0: colorAccessorIndex!
},
indices: indexAccessorIndex,
material,
mode: getPrimitiveMode(mode),
material
}]
});
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -28,32 +28,24 @@ import { Color } from '../../mol-util/color/color';
import { unpackRGBToInt } from '../../mol-util/number-packing';
import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { ValueCell } from '../../mol-util/value-cell';
const GeoExportName = 'geo-export';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3fromArray = Vec3.fromArray;
const v3sub = Vec3.sub;
const v3dot = Vec3.dot;
const v3unitY = Vec3.unitY;
type MeshMode = 'points' | 'lines' | 'triangles'
export interface AddMeshInput {
mesh: {
vertices: Float32Array
normals: Float32Array | undefined
normals: Float32Array
indices: Uint32Array | undefined
groups: Float32Array | Uint8Array
vertexCount: number
drawCount: number
} | undefined
meshes: Mesh[] | undefined
values: BaseValues & { readonly uDoubleSided?: ValueCell<any> }
values: BaseValues
isGeoTexture: boolean
mode: MeshMode
webgl: WebGLContext | undefined
ctx: RuntimeContext
}
@@ -63,8 +55,7 @@ export type MeshGeoData = {
groups: Float32Array | Uint8Array,
vertexCount: number,
instanceIndex: number,
isGeoTexture: boolean,
mode: MeshMode
isGeoTexture: boolean
}
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
@@ -231,7 +222,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
}
protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
const groupCount = values.uGroupCount.ref.value;
const colorType = values.dColorType.ref.value;
const uColor = values.uColor.ref.value;
@@ -240,12 +231,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const dOverpaint = values.dOverpaint.ref.value;
const tOverpaint = values.tOverpaint.ref.value.array;
let vertexCount = geoData.vertexCount;
if (mode === 'lines') {
vertexIndex *= 2;
vertexCount *= 2;
}
let color: Color;
switch (colorType) {
case 'uniform':
@@ -313,18 +298,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
}
protected static getTransparency(vertexIndex: number, geoData: MeshGeoData, interpolatedTransparency?: Uint8Array): number {
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
const groupCount = values.uGroupCount.ref.value;
const dTransparency = values.dTransparency.ref.value;
const tTransparency = values.tTransparency.ref.value.array;
const transparencyType = values.dTransparencyType.ref.value;
let vertexCount = geoData.vertexCount;
if (mode === 'lines') {
vertexIndex *= 2;
vertexCount *= 2;
}
let transparency: number = 0;
if (dTransparency) {
switch (transparencyType) {
@@ -350,7 +329,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
return transparency;
}
protected abstract addMeshWithColors(input: AddMeshInput): Promise<void>;
protected abstract addMeshWithColors(input: AddMeshInput): void;
private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aPosition = values.aPosition.ref.value;
@@ -370,132 +349,36 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
drawCount = values.drawCount.ref.value;
}
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, webgl, ctx });
}
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aStart = values.aStart.ref.value;
const aEnd = values.aEnd.ref.value;
const aGroup = values.aGroup.ref.value;
const vertexCount = (values.uVertexCount.ref.value / 4) * 2;
const drawCount = values.drawCount.ref.value / (2 * 3);
if (this.options.linesAsTriangles) {
const start = Vec3();
const end = Vec3();
const instanceCount = values.instanceCount.ref.value;
const meshes: Mesh[] = [];
const radialSegments = 6;
const topCap = true;
const bottomCap = true;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
for (let i = 0, il = vertexCount * 2; i < il; i += 4) {
v3fromArray(start, aStart, i * 3);
v3fromArray(end, aEnd, i * 3);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
state.currentGroup = aGroup[i];
addCylinder(state, start, end, 1, cylinderProps);
}
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
} else {
const n = vertexCount / 2;
const vertices = new Float32Array(n * 2 * 3);
for (let i = 0; i < n; ++i) {
vertices[i * 6] = aStart[i * 4 * 3];
vertices[i * 6 + 1] = aStart[i * 4 * 3 + 1];
vertices[i * 6 + 2] = aStart[i * 4 * 3 + 2];
vertices[i * 6 + 3] = aEnd[i * 4 * 3];
vertices[i * 6 + 4] = aEnd[i * 4 * 3 + 1];
vertices[i * 6 + 5] = aEnd[i * 4 * 3 + 2];
}
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
}
// TODO
}
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aPosition = values.aPosition.ref.value;
const aGroup = values.aGroup.ref.value;
const vertexCount = values.uVertexCount.ref.value;
const drawCount = values.drawCount.ref.value;
if (this.options.pointsAsTriangles) {
const center = Vec3();
const instanceCount = values.instanceCount.ref.value;
const meshes: Mesh[] = [];
const detail = 0;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
for (let i = 0; i < vertexCount; ++i) {
v3fromArray(center, aPosition, i * 3);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
state.currentGroup = group;
addSphere(state, center, radius, detail);
}
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
} else {
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'points', webgl, ctx });
}
// TODO
}
private async addSpheres(values: SpheresValues, webgl: WebGLContext, ctx: RuntimeContext) {
const center = Vec3();
const aPosition = values.centerBuffer.ref.value;
const aGroup = values.groupBuffer.ref.value;
const aPosition = values.aPosition.ref.value;
const aGroup = values.aGroup.ref.value;
const instanceCount = values.instanceCount.ref.value;
const vertexCount = values.uVertexCount.ref.value;
const meshes: Mesh[] = [];
const sphereCount = vertexCount / 6 * instanceCount;
const sphereCount = vertexCount / 4 * instanceCount;
let detail: number;
switch (this.options.primitivesQuality) {
case 'auto':
if (sphereCount < 2000) detail = 3;
else if (sphereCount < 20000) detail = 2;
else detail = 1;
break;
case 'high':
detail = 3;
break;
case 'medium':
detail = 2;
break;
case 'low':
detail = 1;
break;
default:
assertUnreachable(this.options.primitivesQuality);
}
if (sphereCount < 2000) detail = 3;
else if (sphereCount < 20000) detail = 2;
else detail = 1;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
for (let i = 0; i < sphereCount; ++i) {
for (let i = 0; i < vertexCount; i += 4) {
v3fromArray(center, aPosition, i * 3);
const group = aGroup[i];
@@ -507,13 +390,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
}
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
const start = Vec3();
const end = Vec3();
const dir = Vec3();
const aStart = values.aStart.ref.value;
const aEnd = values.aEnd.ref.value;
@@ -526,24 +408,9 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const cylinderCount = vertexCount / 6 * instanceCount;
let radialSegments: number;
switch (this.options.primitivesQuality) {
case 'auto':
if (cylinderCount < 2000) radialSegments = 36;
else if (cylinderCount < 20000) radialSegments = 24;
else radialSegments = 12;
break;
case 'high':
radialSegments = 36;
break;
case 'medium':
radialSegments = 24;
break;
case 'low':
radialSegments = 12;
break;
default:
assertUnreachable(this.options.primitivesQuality);
}
if (cylinderCount < 2000) radialSegments = 36;
else if (cylinderCount < 20000) radialSegments = 24;
else radialSegments = 12;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
@@ -551,17 +418,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
for (let i = 0; i < vertexCount; i += 6) {
v3fromArray(start, aStart, i * 3);
v3fromArray(end, aEnd, i * 3);
v3sub(dir, end, start);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
const cap = aCap[i];
let topCap = cap === 1 || cap === 3;
let bottomCap = cap >= 2;
if (v3dot(v3unitY, dir) > 0) {
[bottomCap, topCap] = [topCap, bottomCap];
}
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
const topCap = cap === 1 || cap === 3;
const bottomCap = cap >= 2;
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
state.currentGroup = aGroup[i];
addCylinder(state, start, end, 1, cylinderProps);
}
@@ -569,7 +432,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
}
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
@@ -594,13 +457,11 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const vertexCount = values.uVertexCount.ref.value;
const drawCount = values.drawCount.ref.value;
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, webgl, ctx });
}
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext) {
if (!renderObject.state.visible && !this.options.includeHidden) return;
if (renderObject.values.drawCount.ref.value === 0) return;
if (renderObject.values.instanceCount.ref.value === 0) return;
if (!renderObject.state.visible) return;
switch (renderObject.type) {
case 'mesh':
@@ -618,13 +479,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
}
}
protected options = {
includeHidden: false,
linesAsTriangles: false,
pointsAsTriangles: false,
primitivesQuality: 'auto' as 'auto' | 'high' | 'medium' | 'low',
};
abstract getData(ctx: RuntimeContext): Promise<D>;
abstract getBlob(ctx: RuntimeContext): Promise<Blob>;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -70,8 +70,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
if (mode !== 'triangles') return;
const { mesh, values, isGeoTexture, webgl, ctx } = input;
const obj = this.obj;
const t = Mat4();
@@ -87,19 +86,19 @@ export class ObjExporter extends MeshExporter<ObjData> {
const instanceCount = values.uInstanceCount.ref.value;
let interpolatedColors: Uint8Array | undefined;
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
interpolatedColors = ObjExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
if (colorType === 'volume' || colorType === 'volumeInstance') {
interpolatedColors = ObjExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
}
let interpolatedOverpaint: Uint8Array | undefined;
if (webgl && mesh && overpaintType === 'volumeInstance') {
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
if (overpaintType === 'volumeInstance') {
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
}
let interpolatedTransparency: Uint8Array | undefined;
if (webgl && mesh && transparencyType === 'volumeInstance') {
if (transparencyType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
}
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
@@ -127,7 +126,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
// normal
for (let i = 0; i < vertexCount; ++i) {
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
StringBuilder.writeSafe(obj, 'vn ');
StringBuilder.writeFloat(obj, tmpV[0], 100);
StringBuilder.whitespace1(obj);
@@ -137,7 +136,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
StringBuilder.newline(obj);
}
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
// color
const quantizedColors = new Uint8Array(drawCount * 3);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
*/
@@ -30,8 +30,7 @@ export class StlExporter extends MeshExporter<StlData> {
private centerTransform: Mat4;
protected async addMeshWithColors(input: AddMeshInput) {
const { values, isGeoTexture, mode, ctx } = input;
if (mode !== 'triangles') return;
const { values, isGeoTexture, ctx } = input;
const t = Mat4();
const tmpV = Vec3();

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -61,8 +61,7 @@ def Material "material${materialKey}"
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
if (mode !== 'triangles') return;
const { mesh, values, isGeoTexture, webgl, ctx } = input;
const t = Mat4();
const n = Mat3();
@@ -79,20 +78,20 @@ def Material "material${materialKey}"
const roughness = values.uRoughness.ref.value;
let interpolatedColors: Uint8Array | undefined;
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
if (colorType === 'volume' || colorType === 'volumeInstance') {
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
}
let interpolatedOverpaint: Uint8Array | undefined;
if (webgl && mesh && overpaintType === 'volumeInstance') {
if (overpaintType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
}
let interpolatedTransparency: Uint8Array | undefined;
if (webgl && mesh && transparencyType === 'volumeInstance') {
if (transparencyType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
}
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
@@ -124,7 +123,7 @@ def Material "material${materialKey}"
// normal
for (let i = 0; i < vertexCount; ++i) {
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',(');
StringBuilder.writeFloat(normalBuilder, tmpV[0], 100);
StringBuilder.writeSafe(normalBuilder, ',');
@@ -134,7 +133,7 @@ def Material "material${materialKey}"
StringBuilder.writeSafe(normalBuilder, ')');
}
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
// face
for (let i = 0; i < drawCount; ++i) {

View File

@@ -1,187 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Testing examples for using mesh-extension.ts. */
import { CIF } from '../../mol-io/reader/cif';
import { Volume } from '../../mol-model/volume';
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector } from '../../mol-state';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import { ParamDefinition } from '../../mol-util/param-definition';
import { createMeshFromUrl } from './mesh-extension';
import { MeshServerInfo } from './mesh-streaming/server-info';
import { InitMeshStreaming } from './mesh-streaming/transformers';
import * as MeshUtils from './mesh-utils';
export const DB_URL = '/db'; // local
export async function runMeshExtensionExamples(plugin: PluginContext, db_url: string = DB_URL) {
console.time('TIME MESH EXAMPLES');
// await runIsosurfaceExample(plugin, db_url);
// await runMolsurfaceExample(plugin);
// Focused Ion Beam-Scanning Electron Microscopy of mitochondrial reticulum in murine skeletal muscle: https://www.ebi.ac.uk/empiar/EMPIAR-10070/
// await runMeshExample(plugin, 'all', db_url);
// await runMeshExample(plugin, 'fg', db_url);
// await runMultimeshExample(plugin, 'fg', 'worst', db_url);
// await runCifMeshExample(plugin);
// await runMeshExample2(plugin, 'fg');
await runMeshStreamingExample(plugin);
console.timeEnd('TIME MESH EXAMPLES');
}
/** Example for downloading multiple separate segments, each containing 1 mesh. */
export async function runMeshExample(plugin: PluginContext, segments: 'fg' | 'all', db_url: string = DB_URL) {
const detail = 2;
const segmentIds = (segments === 'all') ?
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
for (const segmentId of segmentIds) {
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-mesh-rounded/segment-${segmentId}/detail-${detail}`, segmentId, detail, true, undefined);
}
}
/** Example for downloading multiple separate segments, each containing 1 mesh. */
export async function runMeshExample2(plugin: PluginContext, segments: 'one' | 'few' | 'fg' | 'all') {
const detail = 1;
const segmentIds = (segments === 'one') ? [15]
: (segments === 'few') ? [1, 4, 7, 10, 16]
: (segments === 'all') ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
for (const segmentId of segmentIds) {
await createMeshFromUrl(plugin, `http://localhost:9000/v2/empiar/empiar-10070/mesh_bcif/${segmentId}/${detail}`, segmentId, detail, false, undefined);
}
}
/** Example for downloading a single segment containing multiple meshes. */
export async function runMultimeshExample(plugin: PluginContext, segments: 'fg' | 'all', detailChoice: 'best' | 'worst', db_url: string = DB_URL) {
const urlDetail = (detailChoice === 'best') ? '2' : 'worst';
const numDetail = (detailChoice === 'best') ? 2 : 1000;
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-multimesh-rounded/segments-${segments}/detail-${urlDetail}`, 0, numDetail, false, undefined);
}
export async function runMeshStreamingExample(plugin: PluginContext, source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070', serverUrl?: string, parent?: StateObjectSelector) {
const params = ParamDefinition.getDefaultValues(MeshServerInfo.Params);
if (serverUrl) params.serverUrl = serverUrl;
params.source = source;
params.entryId = entryId;
await plugin.runTask(plugin.state.data.applyAction(InitMeshStreaming, params, parent?.ref), { useOverlay: false });
}
/** Example for downloading a protein structure and visualizing molecular surface. */
export async function runMolsurfaceExample(plugin: PluginContext) {
const entryId = 'pdb-7etq';
// Node "https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif" ("transformer": "ms-plugin.download") -> var data
const data = await plugin.builders.data.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif', isBinary: true }, { state: { isGhost: false } });
console.log('formats:', plugin.dataFormats.list);
// Node "CIF File" ("transformer": "ms-plugin.parse-cif")
// Node "7ETQ 1 model" ("transformer": "ms-plugin.trajectory-from-mmcif") -> var trajectory
const parsed = await plugin.dataFormats.get('mmcif')!.parse(plugin, data, { entryId });
const trajectory: StateObjectSelector<PluginStateObject.Molecule.Trajectory> = parsed.trajectory;
console.log('parsed', parsed);
console.log('trajectory', trajectory);
// Node "Model 1" ("transformer": "ms-plugin.model-from-trajectory") -> var model
const model = await plugin.build().to(trajectory).apply(StateTransforms.Model.ModelFromTrajectory).commit();
console.log('model:', model);
// Node "Model 91 elements" ("transformer": "ms-plugin.structure-from-model") -> var structure
const structure = await plugin.build().to(model).apply(StateTransforms.Model.StructureFromModel,).commit();
console.log('structure:', structure);
// Node "Molecular Surface" ("transformer": "ms-plugin.structure-representation-3d") -> var repr
const reprParams = createStructureRepresentationParams(plugin, undefined, { type: 'molecular-surface' });
const repr = await plugin.build().to(structure).apply(StateTransforms.Representation.StructureRepresentation3D, reprParams).commit();
console.log('repr:', repr);
}
/** Example for downloading an EMDB density data and visualizing isosurface. */
export async function runIsosurfaceExample(plugin: PluginContext, db_url: string = DB_URL) {
const entryId = 'emd-1832';
const isoLevel = 2.73;
let root = await plugin.build();
const data = await plugin.builders.data.download({ url: `${db_url}/emd-1832-box`, isBinary: true }, { state: { isGhost: false } });
const parsed = await plugin.dataFormats.get('dscif')!.parse(plugin, data, { entryId });
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
const volumeData = volume.cell!.obj!.data;
console.log('data:', data);
console.log('parsed:', parsed);
console.log('volume:', volume);
console.log('volumeData:', volumeData);
root = await plugin.build();
console.log('root:', root);
console.log('to:', root.to(volume));
console.log('toRoot:', root.toRoot());
let volumeParams;
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
type: 'isosurface',
typeParams: {
alpha: 0.5,
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
visuals: ['solid'],
sizeFactor: 1,
},
color: 'uniform',
colorParams: { value: Color(0x00aaaa) },
});
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
type: 'isosurface',
typeParams: {
alpha: 1.0,
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
visuals: ['wireframe'],
sizeFactor: 1,
},
color: 'uniform',
colorParams: { value: Color(0x8800aa) },
});
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
await root.commit();
}
export async function runCifMeshExample(plugin: PluginContext, api: string = 'http://localhost:9000/v2',
source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070',
segmentId: number = 1, detail: number = 10,
) {
const url = `${api}/${source}/${entryId}/mesh_bcif/${segmentId}/${detail}`;
getMeshFromBcif(plugin, url);
}
async function getMeshFromBcif(plugin: PluginContext, url: string) {
const urlAsset = Asset.getUrlAsset(plugin.managers.asset, url);
const asset = await plugin.runTask(plugin.managers.asset.resolve(urlAsset, 'binary'));
const parsed = await plugin.runTask(CIF.parseBinary(asset.data));
if (parsed.isError) {
plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
return;
}
console.log('blocks:', parsed.result.blocks);
const mesh = await MeshUtils.meshFromCif(parsed.result);
console.log(mesh);
}

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Column, Database } from '../../mol-data/db';
import { CifFrame } from '../../mol-io/reader/cif';
import { toDatabase } from '../../mol-io/reader/cif/schema';
const int = Column.Schema.int;
const float = Column.Schema.float;
// TODO in future, move to molstar/src/mol-io/reader/cif/schema/mesh.ts
export const Mesh_Data_Schema = {
mesh: {
id: int,
},
mesh_vertex: {
mesh_id: int,
vertex_id: int,
x: float,
y: float,
z: float,
},
/** Table of triangles, 3 rows per triangle */
mesh_triangle: {
mesh_id: int,
/** Indices of vertices within mesh */
vertex_id: int,
}
};
export type Mesh_Data_Schema = typeof Mesh_Data_Schema;
export interface Mesh_Data_Database extends Database<Mesh_Data_Schema> {}
// TODO in future, move to molstar/src/mol-io/reader/cif.ts: CIF.schema.mesh
export const CIF_schema_mesh = (frame: CifFrame) => toDatabase<Mesh_Data_Schema, Mesh_Data_Database>(Mesh_Data_Schema, frame);

View File

@@ -1,223 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Defines new types of State tree transformers for dealing with mesh data. */
import { BaseGeometry, VisualQuality, VisualQualityOptions } from '../../mol-geo/geometry/base';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { CifFile } from '../../mol-io/reader/cif';
import { Box3D } from '../../mol-math/geometry';
import { Vec3 } from '../../mol-math/linear-algebra';
import { Shape } from '../../mol-model/shape';
import { ShapeProvider } from '../../mol-model/shape/provider';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { Download } from '../../mol-plugin-state/transforms/data';
import { ShapeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectRef, StateObjectSelector, StateTransformer } from '../../mol-state';
import { Task } from '../../mol-task';
import { Color } from '../../mol-util/color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import * as MeshUtils from './mesh-utils';
export const BACKGROUND_OPACITY = 0.2;
export const FOREROUND_OPACITY = 1;
export const VolsegTransform: StateTransformer.Builder.Root = StateTransformer.builderFactory('volseg');
// // // // // // // // // // // // // // // // // // // // // // // //
// Parsed data
/** Data type for `MeshlistStateObject` - list of meshes */
export interface MeshlistData {
segmentId: number,
segmentName: string,
detail: number,
meshIds: number[],
mesh: Mesh,
/** Reference to the object which created this meshlist (e.g. `MeshStreaming.Behavior`) */
ownerId?: string,
}
export namespace MeshlistData {
export function empty(): MeshlistData {
return {
segmentId: 0,
segmentName: 'Empty',
detail: 0,
meshIds: [],
mesh: Mesh.createEmpty(),
};
};
export async function fromCIF(data: CifFile, segmentId: number, segmentName: string, detail: number): Promise<MeshlistData> {
const { mesh, meshIds } = await MeshUtils.meshFromCif(data);
return {
segmentId,
segmentName,
detail,
meshIds,
mesh,
};
}
export function stats(meshListData: MeshlistData): string {
return `Meshlist "${meshListData.segmentName}" (detail ${meshListData.detail}): ${meshListData.meshIds.length} meshes, ${meshListData.mesh.vertexCount} vertices, ${meshListData.mesh.triangleCount} triangles`;
}
export function getShape(data: MeshlistData, color: Color): Shape<Mesh> {
const mesh = data.mesh;
const meshShape: Shape<Mesh> = Shape.create(data.segmentName, data, mesh,
() => color,
() => 1,
// group => `${data.segmentName} | Segment ${data.segmentId} | Detail ${data.detail} | Mesh ${group}`,
group => data.segmentName,
);
return meshShape;
}
export function combineBBoxes(boxes: (Box3D | null)[]): Box3D | null {
let result = null;
for (const box of boxes) {
if (!box) continue;
if (result) {
Vec3.min(result.min, result.min, box.min);
Vec3.max(result.max, result.max, box.max);
} else {
result = Box3D.zero();
Box3D.copy(result, box);
}
}
return result;
}
export function bbox(data: MeshlistData): Box3D | null {
return MeshUtils.bbox(data.mesh);
}
export function allVerticesUsed(data: MeshlistData): boolean {
const unusedVertices = new Set();
for (let i = 0; i < data.mesh.vertexCount; i++) {
unusedVertices.add(i);
}
for (let i = 0; i < 3 * data.mesh.triangleCount; i++) {
const v = data.mesh.vertexBuffer.ref.value[i];
unusedVertices.delete(v);
}
return unusedVertices.size === 0;
}
}
// // // // // // // // // // // // // // // // // // // // // // // //
// Raw Data -> Parsed data
export class MeshlistStateObject extends PluginStateObject.Create<MeshlistData>({ name: 'Parsed Meshlist', typeClass: 'Object' }) { }
export const ParseMeshlistTransformer = VolsegTransform({
name: 'meshlist-from-string',
from: PluginStateObject.Format.Cif,
to: MeshlistStateObject,
params: {
label: PD.Text(MeshlistStateObject.type.name, { isHidden: true }),
segmentId: PD.Numeric(1, {}, { isHidden: true }),
segmentName: PD.Text('Segment'),
detail: PD.Numeric(1, {}, { isHidden: true }),
/** Reference to the object which manages this meshlist (e.g. `MeshStreaming.Behavior`) */
ownerId: PD.Text('', { isHidden: true }),
}
})({
apply({ a, params }, globalCtx) { // `a` is the parent node, params are 2nd argument to To.apply(), `globalCtx` is the plugin
return Task.create('Create Parsed Meshlist', async ctx => {
const meshlistData = await MeshlistData.fromCIF(a.data, params.segmentId, params.segmentName, params.detail);
meshlistData.ownerId = params.ownerId;
const es = meshlistData.meshIds.length === 1 ? '' : 'es';
return new MeshlistStateObject(meshlistData, { label: params.label, description: `${meshlistData.segmentName} (${meshlistData.meshIds.length} mesh${es})` });
});
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
// Parsed data -> Shape
/** Data type for PluginStateObject.Shape.Provider */
type MeshShapeProvider = ShapeProvider<MeshlistData, Mesh, Mesh.Params>;
namespace MeshShapeProvider {
export function fromMeshlistData(meshlist: MeshlistData, color?: Color): MeshShapeProvider {
const theColor = color ?? MeshUtils.ColorGenerator.next().value;
return {
label: 'Mesh',
data: meshlist,
params: meshShapeProviderParams,
geometryUtils: Mesh.Utils,
getShape: (ctx, data: MeshlistData) => MeshlistData.getShape(data, theColor),
};
}
}
const meshShapeProviderParams: Mesh.Params = {
...Mesh.Params,
quality: PD.Select<VisualQuality>('custom', VisualQualityOptions, { isEssential: true, description: 'Visual/rendering quality of the representation.' }), // use 'custom' when wanting to apply doubleSided
doubleSided: PD.Boolean(true, BaseGeometry.CustomQualityParamInfo),
// set `flatShaded`: true to see the real mesh vertices and triangles
transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque']), BaseGeometry.ShadingCategory), // 'on' means: show backfaces with correct opacity, even when opacity < 1 (requires doubleSided) ¯\_(ツ)_/¯
};
export const MeshShapeTransformer = VolsegTransform({
name: 'shape-from-meshlist',
display: { name: 'Shape from Meshlist', description: 'Create Shape from Meshlist data' },
from: MeshlistStateObject,
to: PluginStateObject.Shape.Provider,
params: {
color: PD.Value<Color | undefined>(undefined), // undefined means random color
},
})({
apply({ a, params }) {
const shapeProvider = MeshShapeProvider.fromMeshlistData(a.data, params.color);
return new PluginStateObject.Shape.Provider(shapeProvider, { label: PluginStateObject.Shape.Provider.type.name, description: a.description });
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
/** Download data and create state tree hierarchy down to visual representation. */
export async function createMeshFromUrl(plugin: PluginContext, meshDataUrl: string, segmentId: number, detail: number,
collapseTree: boolean, color?: Color, parent?: StateObjectSelector | StateObjectRef, transparentIfBboxAbove?: number,
name?: string, ownerId?: string) {
const update = parent ? plugin.build().to(parent) : plugin.build().toRoot();
const rawDataNodeRef = update.apply(Download,
{ url: meshDataUrl, isBinary: true, label: `Downloaded Data ${segmentId}` },
{ state: { isCollapsed: collapseTree } }
).ref;
const parsedDataNode = await update.to(rawDataNodeRef)
.apply(StateTransforms.Data.ParseCif)
.apply(ParseMeshlistTransformer,
{ label: undefined, segmentId: segmentId, segmentName: name ?? `Segment ${segmentId}`, detail: detail, ownerId: ownerId },
{}
)
.commit();
let transparent = false;
if (transparentIfBboxAbove !== undefined && parsedDataNode.data) {
const bbox = MeshlistData.bbox(parsedDataNode.data) || Box3D.zero();
transparent = Box3D.volume(bbox) > transparentIfBboxAbove;
}
await plugin.build().to(parsedDataNode)
.apply(MeshShapeTransformer, { color: color },)
.apply(ShapeRepresentation3D,
{ alpha: transparent ? BACKGROUND_OPACITY : FOREROUND_OPACITY },
{ tags: ['mesh-segment-visual', `segment-${segmentId}`] }
)
.commit();
return rawDataNodeRef;
}

View File

@@ -1,332 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { distinctUntilChanged, map } from 'rxjs';
import { CIF } from '../../../mol-io/reader/cif';
import { Box3D } from '../../../mol-math/geometry';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginBehavior } from '../../../mol-plugin/behavior';
import { PluginCommand } from '../../../mol-plugin/command';
import { PluginCommands } from '../../../mol-plugin/commands';
import { PluginContext } from '../../../mol-plugin/context';
import { UUID } from '../../../mol-util';
import { Asset } from '../../../mol-util/assets';
import { Color } from '../../../mol-util/color';
import { ColorNames } from '../../../mol-util/color/names';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Choice } from '../../volumes-and-segmentations/helpers';
import { MetadataWrapper } from '../../volumes-and-segmentations/volseg-api/utils';
import { MeshlistData } from '../mesh-extension';
import { MeshServerInfo } from './server-info';
const DEFAULT_SEGMENT_NAME = 'Untitled segment';
const DEFAULT_SEGMENT_COLOR = ColorNames.lightgray;
export const NO_SEGMENT = -1;
/** Maximum (worst) detail level available in GUI (TODO set actual maximum possible value) */
const MAX_DETAIL = 10;
const DEFAULT_DETAIL = 7; // TODO decide a reasonable default
/** Segments whose bounding box volume is above this value (relative to the overall bounding box) are considered as background segments */
export const BACKGROUND_SEGMENT_VOLUME_THRESHOLD = 0.5;
export class MeshStreaming extends PluginStateObject.CreateBehavior<MeshStreaming.Behavior>({ name: 'Mesh Streaming' }) { }
export namespace MeshStreaming {
export namespace Params {
export const ViewTypeChoice = new Choice({ off: 'Off', select: 'Select', all: 'All' }, 'select'); // TODO add camera target?
export type ViewType = Choice.Values<typeof ViewTypeChoice>;
export function create(options: MeshServerInfo.Data) {
return {
view: PD.MappedStatic('select', {
'off': PD.Group({}),
'select': PD.Group({
baseDetail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the non-selected segments (lower number = better)' }),
focusDetail: PD.Numeric(1, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the selected segment (lower number = better)' }),
selectedSegment: PD.Numeric(NO_SEGMENT, {}, { isHidden: true }),
}, { isFlat: true }),
'all': PD.Group({
detail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for all segments (lower number = better)' })
}, { isFlat: true }),
}, { description: '"Off" hides all segments. \n"Select" shows all segments in lower detail, clicked segment in better detail. "All" shows all segment in the same level.' }),
};
}
export type Definition = ReturnType<typeof create>
export type Values = PD.Values<Definition>
export function copyValues(params: Values): Values {
return {
view: {
name: params.view.name,
params: { ...params.view.params } as any,
}
};
}
export function valuesEqual(p: Values, q: Values): boolean {
if (p.view.name !== q.view.name) return false;
for (const key in p.view.params) {
if ((p.view.params as any)[key] !== (q.view.params as any)[key]) return false;
}
return true;
}
export function detailsEqual(p: Values, q: Values): boolean {
switch (p.view.name) {
case 'off':
return q.view.name === 'off';
case 'select':
return q.view.name === 'select' && p.view.params.baseDetail === q.view.params.baseDetail && p.view.params.focusDetail === q.view.params.focusDetail;
case 'all':
return q.view.name === 'all' && p.view.params.detail === q.view.params.detail;
default:
throw new Error('Not implemented');
}
}
}
export interface VisualInfo {
tag: string, // e.g. high-2, low-1 // ? remove if can be omitted
segmentId: number, // ? remove if unused
segmentName: string, // ? remove if unused
detailType: VisualInfo.DetailType, // ? remove if unused
detail: number, // ? remove if unused
color: Color, // move to MeshlistData?
visible: boolean,
data?: MeshlistData,
}
export namespace VisualInfo {
export type DetailType = 'low' | 'high';
export const DetailTypes: DetailType[] = ['low', 'high'];
export function tagFor(segmentId: number, detail: DetailType) {
return `${detail}-${segmentId}`;
}
}
export class Behavior extends PluginBehavior.WithSubscribers<Params.Values> {
private id: string;
private ref: string = '';
public parentData: MeshServerInfo.Data;
private metadata?: MetadataWrapper;
public visuals?: { [tag: string]: VisualInfo };
public backgroundSegments: { [segmentId: number]: boolean } = {};
private focusObservable = this.plugin.behaviors.interaction.click.pipe( // QUESTION is this OK way to get focused segment?
map(evt => evt.current.loci),
map(loci => (loci.kind === 'group-loci') ? loci.shape.sourceData as MeshlistData : null),
map(data => (data?.ownerId === this.id) ? data : null), // do not process shapes created by others
distinctUntilChanged((old, current) => old?.segmentId === current?.segmentId),
);
private focusSubscription?: PluginCommand.Subscription = undefined;
private backgroundSegmentsInitialized = false;
constructor(plugin: PluginContext, data: MeshServerInfo.Data, params: Params.Values) {
super(plugin, params);
this.id = UUID.create22();
this.parentData = data;
}
register(ref: string): void {
this.ref = ref;
}
unregister(): void {
if (this.focusSubscription) {
this.focusSubscription.unsubscribe();
this.focusSubscription = undefined;
}
// TODO empty cache here (if used)
}
selectSegment(segmentId: number) {
if (this.params.view.name === 'select') {
if (this.params.view.params.selectedSegment === segmentId) return;
const newParams = Params.copyValues(this.params);
if (newParams.view.name === 'select') {
newParams.view.params.selectedSegment = segmentId;
}
const state = this.plugin.state.data;
const update = state.build().to(this.ref).update(newParams);
PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
}
}
async update(params: Params.Values) {
const oldParams = this.params;
this.params = params;
if (!this.metadata) {
const response = await fetch(this.getMetadataUrl());
const rawMetadata = await response.json();
this.metadata = new MetadataWrapper(rawMetadata);
}
if (!this.visuals) {
this.initVisualInfos();
} else if (!Params.detailsEqual(this.params, oldParams)) {
this.updateVisualInfoDetails();
}
switch (params.view.name) {
case 'off':
await this.disableVisuals();
break;
case 'select':
await this.enableVisuals(params.view.params.selectedSegment);
break;
case 'all':
await this.enableVisuals();
break;
default:
throw new Error('Not implemented');
}
if (params.view.name !== 'off' && !this.backgroundSegmentsInitialized) {
this.guessBackgroundSegments();
this.backgroundSegmentsInitialized = true;
}
if (params.view.name === 'select' && !this.focusSubscription) {
this.focusSubscription = this.subscribeObservable(this.focusObservable, data => { this.selectSegment(data?.segmentId ?? NO_SEGMENT); });
} else if (params.view.name !== 'select' && this.focusSubscription) {
this.focusSubscription.unsubscribe();
this.focusSubscription = undefined;
}
return true;
}
private getMetadataUrl() {
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/metadata`;
}
private getMeshUrl(segment: number, detail: number) {
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/mesh_bcif/${segment}/${detail}`;
}
private initVisualInfos() {
const visuals: { [tag: string]: VisualInfo } = {};
for (const segid of this.metadata!.meshSegmentIds) {
const name = this.metadata?.getSegment(segid)?.biological_annotation.name ?? DEFAULT_SEGMENT_NAME;
const color = this.metadata?.getSegmentColor(segid) ?? DEFAULT_SEGMENT_COLOR;
for (const detailType of VisualInfo.DetailTypes) {
const visual: VisualInfo = {
tag: VisualInfo.tagFor(segid, detailType),
segmentId: segid,
segmentName: name,
detailType: detailType,
detail: -1, // to be set at the end
color: color,
visible: false,
data: undefined,
};
visuals[visual.tag] = visual;
}
}
this.visuals = visuals;
this.updateVisualInfoDetails();
}
private updateVisualInfoDetails() {
let highDetail: number | undefined;
let lowDetail: number | undefined;
switch (this.params.view.name) {
case 'off':
lowDetail = undefined;
highDetail = undefined;
break;
case 'select':
lowDetail = this.params.view.params.baseDetail;
highDetail = this.params.view.params.focusDetail;
break;
case 'all':
lowDetail = this.params.view.params.detail;
highDetail = undefined;
break;
}
for (const tag in this.visuals) {
const visual = this.visuals[tag];
const preferredDetail = (visual.detailType === 'high') ? highDetail : lowDetail;
if (preferredDetail !== undefined) {
visual.detail = this.metadata!.getSufficientMeshDetail(visual.segmentId, preferredDetail);
}
}
}
private async enableVisuals(highDetailSegment?: number) {
for (const tag in this.visuals) {
const visual = this.visuals[tag];
const requiredDetailType = visual.segmentId === highDetailSegment ? 'high' : 'low';
if (visual.detailType === requiredDetailType) {
visual.data = await this.getMeshData(visual);
visual.visible = true;
} else {
visual.visible = false;
}
}
}
private async disableVisuals() {
for (const tag in this.visuals) {
const visual = this.visuals[tag];
visual.visible = false;
}
}
/** Fetch data in current `visual.detail`, or return already fetched data (if available in the correct detail). */
private async getMeshData(visual: VisualInfo): Promise<MeshlistData> {
if (visual.data && visual.data.detail === visual.detail) {
// Do not recreate
return visual.data;
}
// TODO cache
const url = this.getMeshUrl(visual.segmentId, visual.detail);
const urlAsset = Asset.getUrlAsset(this.plugin.managers.asset, url);
const asset = await this.plugin.runTask(this.plugin.managers.asset.resolve(urlAsset, 'binary'));
const parsed = await this.plugin.runTask(CIF.parseBinary(asset.data));
if (parsed.isError) {
throw new Error(`Failed parsing CIF file from ${url}`);
}
const meshlistData = await MeshlistData.fromCIF(parsed.result, visual.segmentId, visual.segmentName, visual.detail);
meshlistData.ownerId = this.id;
// const bbox = MeshlistData.bbox(meshlistData);
// const bboxVolume = bbox ? MS.Box3D.volume(bbox) : 0.0;
// console.log(`BBox ${visual.segmentId}: ${Math.round(bboxVolume! / 1e6)} M`, bbox); // DEBUG
return meshlistData;
}
private async guessBackgroundSegments() {
const bboxes: { [segid: number]: Box3D } = {};
for (const tag in this.visuals) {
const visual = this.visuals[tag];
if (visual.detailType === 'low' && visual.data) {
const bbox = MeshlistData.bbox(visual.data);
if (bbox) {
bboxes[visual.segmentId] = bbox;
}
}
}
const totalBbox = MeshlistData.combineBBoxes(Object.values(bboxes));
const totalVolume = totalBbox ? Box3D.volume(totalBbox) : 0.0;
// console.log(`BBox total: ${Math.round(totalVolume! / 1e6)} M`, totalBbox); // DEBUG
const isBgSegment: { [segid: number]: boolean } = {};
for (const segid in bboxes) {
const bbox = bboxes[segid];
const bboxVolume = Box3D.volume(bbox);
isBgSegment[segid] = (bboxVolume > totalVolume * BACKGROUND_SEGMENT_VOLUME_THRESHOLD);
// console.log(`BBox ${segid}: ${Math.round(bboxVolume! / 1e6)} M, ${bboxVolume / totalVolume}`, bbox); // DEBUG
}
this.backgroundSegments = isBgSegment;
}
getDescription() {
return Params.ViewTypeChoice.prettyName(this.params.view.name);
}
}
}

View File

@@ -1,28 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Choice } from '../../volumes-and-segmentations/helpers';
export const DEFAULT_MESH_SERVER = 'http://localhost:9000/v2';
export class MeshServerInfo extends PluginStateObject.Create<MeshServerInfo.Data>({ name: 'Volume Server', typeClass: 'Object' }) { }
export namespace MeshServerInfo {
export const MeshSourceChoice = new Choice({ empiar: 'EMPIAR', emdb: 'EMDB' }, 'empiar');
export type MeshSource = Choice.Values<typeof MeshSourceChoice>;
export const Params = {
serverUrl: PD.Text(DEFAULT_MESH_SERVER),
source: MeshSourceChoice.PDSelect(),
entryId: PD.Text(''),
};
export type Data = PD.Values<typeof Params>;
}

View File

@@ -1,214 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginContext } from '../../../mol-plugin/context';
import { ShapeRepresentation } from '../../../mol-repr/shape/representation';
import { StateAction, StateTransformer } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { shallowEqualObjects } from '../../../mol-util';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { BACKGROUND_OPACITY, FOREROUND_OPACITY, MeshlistData, VolsegTransform } from '../mesh-extension';
import { MeshStreaming, NO_SEGMENT } from './behavior';
import { MeshServerInfo } from './server-info';
// // // // // // // // // // // // // // // // // // // // // // // //
export const MeshServerTransformer = VolsegTransform({
name: 'mesh-server-info',
from: PluginStateObject.Root,
to: MeshServerInfo,
params: MeshServerInfo.Params,
})({
apply({ a, params }, plugin: PluginContext) { // `a` is the parent node, `params` are 2nd argument to To.apply()
params.serverUrl = params.serverUrl.replace(/\/*$/, ''); // trim trailing slash
const description: string = params.entryId;
return new MeshServerInfo({ ...params }, { label: 'Mesh Server', description: description });
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
export const MeshStreamingTransformer = VolsegTransform({
name: 'mesh-streaming-from-server-info',
display: { name: 'Mesh Streaming' },
from: MeshServerInfo,
to: MeshStreaming,
params: a => MeshStreaming.Params.create(a!.data),
})({
canAutoUpdate() { return true; },
apply({ a, params }, plugin: PluginContext) {
return Task.create('Mesh Streaming', async ctx => {
const behavior = new MeshStreaming.Behavior(plugin, a.data, params);
await behavior.update(params);
return new MeshStreaming(behavior, { label: 'Mesh Streaming', description: behavior.getDescription() });
});
},
update({ a, b, oldParams, newParams }) {
return Task.create('Update Mesh Streaming', async ctx => {
if (a.data.source !== b.data.parentData.source || a.data.entryId !== b.data.parentData.entryId) {
return StateTransformer.UpdateResult.Recreate;
}
b.data.parentData = a.data;
await b.data.update(newParams);
b.description = b.data.getDescription();
return StateTransformer.UpdateResult.Updated;
});
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
interface MeshVisualGroupData {
opacity: number,
}
// export type MeshVisualGroupTransformer = typeof MeshVisualGroupTransformer;
export const MeshVisualGroupTransformer = VolsegTransform({
name: 'mesh-visual-group-from-streaming',
display: { name: 'Mesh Visuals for a Segment' },
from: MeshStreaming,
to: PluginStateObject.Group,
params: {
/** Shown on the node in GUI */
label: PD.Text('', { isHidden: true }),
/** Shown on the node in GUI (gray letters) */
description: PD.Text(''),
segmentId: PD.Numeric(NO_SEGMENT, {}, { isHidden: true }),
opacity: PD.Numeric(-1, { min: 0, max: 1, step: 0.01 }),
}
})({
apply({ a, params }, plugin) {
trySetAutoOpacity(params, a);
return new PluginStateObject.Group({ opacity: params.opacity }, params);
},
update({ a, b, oldParams, newParams }, plugin) {
if (shallowEqualObjects(oldParams, newParams)) {
return StateTransformer.UpdateResult.Unchanged;
}
newParams.label ||= oldParams.label; // Protect against resetting params to invalid defaults
if (newParams.segmentId === NO_SEGMENT) newParams.segmentId = oldParams.segmentId; // Protect against resetting params to invalid defaults
trySetAutoOpacity(newParams, a);
b.label = newParams.label;
b.description = newParams.description;
(b.data as MeshVisualGroupData).opacity = newParams.opacity;
return StateTransformer.UpdateResult.Updated;
},
canAutoUpdate({ oldParams, newParams }, plugin) {
return newParams.description === oldParams.description;
},
});
function trySetAutoOpacity(params: StateTransformer.Params<typeof MeshVisualGroupTransformer>, parent: MeshStreaming) {
if (params.opacity === -1) {
const isBgSegment = parent.data.backgroundSegments[params.segmentId];
if (isBgSegment !== undefined) {
params.opacity = isBgSegment ? BACKGROUND_OPACITY : FOREROUND_OPACITY;
}
}
}
// // // // // // // // // // // // // // // // // // // // // // // //
export const MeshVisualTransformer = VolsegTransform({
name: 'mesh-visual-from-streaming',
display: { name: 'Mesh Visual from Streaming' },
from: MeshStreaming,
to: PluginStateObject.Shape.Representation3D,
params: {
/** Must be set to PluginStateObject reference to self */
ref: PD.Text('', { isHidden: true, isEssential: true }), // QUESTION what is isEssential
/** Identification of the mesh visual, e.g. 'low-2' */
tag: PD.Text('', { isHidden: true, isEssential: true }),
/** Opacity of the visual (not to be set directly, but controlled by the opacity of the parent Group, and by VisualInfo.visible) */
opacity: PD.Numeric(-1, { min: 0, max: 1, step: 0.01 }, { isHidden: true }),
}
})({
apply({ a, params, spine }, plugin: PluginContext) {
return Task.create('Mesh Visual', async ctx => {
const visualInfo: MeshStreaming.VisualInfo = a.data.visuals![params.tag];
if (!visualInfo) throw new Error(`VisualInfo with tag '${params.tag}' is missing.`);
const groupData = spine.getAncestorOfType(PluginStateObject.Group)?.data as MeshVisualGroupData | undefined;
params.opacity = visualInfo.visible ? (groupData?.opacity ?? FOREROUND_OPACITY) : 0.0;
const props = PD.getDefaultValues(Mesh.Params);
props.flatShaded = true; // `flatShaded: true` is to see the real mesh vertices and triangles (default: false)
props.alpha = params.opacity;
const repr = ShapeRepresentation((ctx, meshlist: MeshlistData) => MeshlistData.getShape(meshlist, visualInfo.color), Mesh.Utils);
await repr.createOrUpdate(props, visualInfo.data ?? MeshlistData.empty()).runInContext(ctx);
return new PluginStateObject.Shape.Representation3D({ repr, sourceData: visualInfo.data }, { label: 'Mesh Visual', description: params.tag });
});
},
update({ a, b, oldParams, newParams, spine }, plugin: PluginContext) {
return Task.create('Update Mesh Visual', async ctx => {
newParams.ref ||= oldParams.ref; // Protect against resetting params to invalid defaults
newParams.tag ||= oldParams.tag; // Protect against resetting params to invalid defaults
const visualInfo: MeshStreaming.VisualInfo = a.data.visuals![newParams.tag];
if (!visualInfo) throw new Error(`VisualInfo with tag '${newParams.tag}' is missing.`);
const oldData = b.data.sourceData as MeshlistData | undefined;
if (visualInfo.data?.detail !== oldData?.detail) {
return StateTransformer.UpdateResult.Recreate;
}
const groupData = spine.getAncestorOfType(PluginStateObject.Group)?.data as MeshVisualGroupData | undefined;
const newOpacity = visualInfo.visible ? (groupData?.opacity ?? FOREROUND_OPACITY) : 0.0; // do not store to newParams directly, because oldParams and newParams might point to the same object!
if (newOpacity !== oldParams.opacity) {
newParams.opacity = newOpacity;
await b.data.repr.createOrUpdate({ alpha: newParams.opacity }).runInContext(ctx);
return StateTransformer.UpdateResult.Updated;
} else {
return StateTransformer.UpdateResult.Unchanged;
}
});
},
canAutoUpdate(params, globalCtx) {
return true;
},
dispose({ b, params }, plugin) {
b?.data.repr.destroy(); // QUESTION is this correct?
},
});
// // // // // // // // // // // // // // // // // // // // // // // //
export const InitMeshStreaming = StateAction.build({
display: { name: 'Mesh Streaming' },
from: PluginStateObject.Root,
params: MeshServerInfo.Params,
isApplicable: (a, _, plugin: PluginContext) => true
})(function (p, plugin: PluginContext) {
return Task.create('Mesh Streaming', async ctx => {
const { params } = p;
// p.ref
const serverNode = await plugin.build().to(p.ref).apply(MeshServerTransformer, params).commit();
// const serverNode = await plugin.build().toRoot().apply(MeshServerTransformer, params).commit();
const streamingNode = await plugin.build().to(serverNode).apply(MeshStreamingTransformer, {}).commit();
const visuals = streamingNode.data?.visuals ?? {};
const bgSegments = streamingNode.data?.backgroundSegments ?? {};
const segmentGroups: { [segid: number]: string } = {};
for (const tag in visuals) {
const segid = visuals[tag].segmentId;
if (!segmentGroups[segid]) {
let description = visuals[tag].segmentName;
if (bgSegments[segid]) description += ' (background)';
const group = await plugin.build().to(streamingNode).apply(MeshVisualGroupTransformer, { label: `Segment ${segid}`, description: description, segmentId: segid }, { state: { isCollapsed: true } }).commit();
segmentGroups[segid] = group.ref;
}
}
const visualsUpdate = plugin.build();
for (const tag in visuals) {
const ref = `${streamingNode.ref}-${tag}`;
const segid = visuals[tag].segmentId;
visualsUpdate.to(segmentGroups[segid]).apply(MeshVisualTransformer, { ref: ref, tag: tag }, { ref: ref }); // ref - hack to allow the node make itself invisible
}
await plugin.state.data.updateTree(visualsUpdate).runInContext(ctx); // QUESTION what is really the difference between this and `visualsUpdate.commit()`?
});
});
// TODO make available in GUI, in left panel or in right panel like Volume Streaming in src/mol-plugin-ui/structure/volume.tsx?

View File

@@ -1,340 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Helper functions for manipulation with mesh data. */
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { CIF, CifFile } from '../../mol-io/reader/cif';
import { Box3D } from '../../mol-math/geometry';
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { volumeFromDensityServerData } from '../../mol-model-formats/volume/density-server';
import { Grid } from '../../mol-model/volume';
import { ColorNames } from '../../mol-util/color/names';
import { TypedArray } from '../../mol-util/type-helpers';
import { CIF_schema_mesh } from './mesh-cif-schema';
type MeshModificationParams = {
scale?: [number, number, number],
shift?: [number, number, number],
matrix?: Mat4,
group?: number,
invertSides?: boolean
};
/** Modify mesh in-place */
export function modify(m: Mesh, params: MeshModificationParams) {
if (params.scale !== undefined) {
const [qx, qy, qz] = params.scale;
const vertices = m.vertexBuffer.ref.value;
for (let i = 0; i < vertices.length; i += 3) {
vertices[i] *= qx;
vertices[i + 1] *= qy;
vertices[i + 2] *= qz;
}
}
if (params.shift !== undefined) {
const [dx, dy, dz] = params.shift;
const vertices = m.vertexBuffer.ref.value;
for (let i = 0; i < vertices.length; i += 3) {
vertices[i] += dx;
vertices[i + 1] += dy;
vertices[i + 2] += dz;
}
}
if (params.matrix !== undefined) {
const r = m.vertexBuffer.ref.value;
const matrix = params.matrix;
const size = 3 * m.vertexCount;
for (let i = 0; i < size; i += 3) {
Vec3.transformMat4Offset(r, r, matrix, i, i, 0);
}
}
if (params.group !== undefined) {
const groups = m.groupBuffer.ref.value;
for (let i = 0; i < groups.length; i++) {
groups[i] = params.group;
}
}
if (params.invertSides) {
const indices = m.indexBuffer.ref.value;
let tmp;
for (let i = 0; i < indices.length; i += 3) {
tmp = indices[i];
indices[i] = indices[i + 1];
indices[i + 1] = tmp;
}
const normals = m.normalBuffer.ref.value;
for (let i = 0; i < normals.length; i++) {
normals[i] *= -1;
}
}
}
/** Create a copy a mesh, possibly modified */
export function copy(m: Mesh, modification?: MeshModificationParams): Mesh {
const nVertices = m.vertexCount;
const nTriangles = m.triangleCount;
const vertices = new Float32Array(m.vertexBuffer.ref.value);
const indices = new Uint32Array(m.indexBuffer.ref.value);
const normals = new Float32Array(m.normalBuffer.ref.value);
const groups = new Float32Array(m.groupBuffer.ref.value);
const result = Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
if (modification) {
modify(result, modification);
}
return result;
}
/** Join more meshes into one */
export function concat(...meshes: Mesh[]): Mesh {
const nVertices = sum(meshes.map(m => m.vertexCount));
const nTriangles = sum(meshes.map(m => m.triangleCount));
const vertices = concatArrays(Float32Array, meshes.map(m => m.vertexBuffer.ref.value));
const normals = concatArrays(Float32Array, meshes.map(m => m.normalBuffer.ref.value));
const groups = concatArrays(Float32Array, meshes.map(m => m.groupBuffer.ref.value));
const newIndices = [];
let offset = 0;
for (const m of meshes) {
newIndices.push(m.indexBuffer.ref.value.map(i => i + offset));
offset += m.vertexCount;
}
const indices = concatArrays(Uint32Array, newIndices);
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
}
/** Return Mesh from CIF data and mesh IDs (group IDs).
* Assume the CIF contains coords in grid space,
* transform the output mesh to `space` */
export async function meshFromCif(data: CifFile, invertSides: boolean | undefined = undefined, outSpace: 'grid' | 'fractional' | 'cartesian' = 'cartesian'): Promise<{ mesh: Mesh, meshIds: number[] }> {
const volumeInfoBlock = data.blocks.find(b => b.header === 'VOLUME_INFO');
const meshesBlock = data.blocks.find(b => b.header === 'MESHES');
if (!volumeInfoBlock || !meshesBlock) throw new Error('Missing VOLUME_INFO or MESHES block in mesh CIF file');
const volumeInfoCif = CIF.schema.densityServer(volumeInfoBlock);
const meshCif = CIF_schema_mesh(meshesBlock);
const nVertices = meshCif.mesh_vertex._rowCount;
const nTriangles = Math.floor(meshCif.mesh_triangle._rowCount / 3);
const mesh_id = meshCif.mesh.id.toArray();
const vertex_meshId = meshCif.mesh_vertex.mesh_id.toArray();
const x = meshCif.mesh_vertex.x.toArray();
const y = meshCif.mesh_vertex.y.toArray();
const z = meshCif.mesh_vertex.z.toArray();
const triangle_meshId = meshCif.mesh_triangle.mesh_id.toArray();
const triangle_vertexId = meshCif.mesh_triangle.vertex_id.toArray();
// Shift indices from within-mesh indices to overall indices
const indices = new Uint32Array(3 * nTriangles);
const offsets = offsetMap(vertex_meshId);
for (let i = 0; i < 3 * nTriangles; i++) {
const offset = offsets.get(triangle_meshId[i])!;
indices[i] = offset + triangle_vertexId[i];
}
const vertices = flattenCoords(x, y, z);
const normals = new Float32Array(3 * nVertices);
const groups = new Float32Array(vertex_meshId);
const mesh = Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
invertSides ??= isInverted(mesh);
if (invertSides) {
modify(mesh, { invertSides: true }); // Vertex orientation convention is opposite in Volseg API and in MolStar
}
if (outSpace === 'cartesian') {
const volume = await volumeFromDensityServerData(volumeInfoCif).run();
const gridToCartesian = Grid.getGridToCartesianTransform(volume.grid);
modify(mesh, { matrix: gridToCartesian });
} else if (outSpace === 'fractional') {
const gridSize = volumeInfoCif.volume_data_3d_info.sample_count.value(0);
const originFract = volumeInfoCif.volume_data_3d_info.origin.value(0);
const dimensionFract = volumeInfoCif.volume_data_3d_info.dimensions.value(0);
if (dimensionFract[0] !== 1 || dimensionFract[1] !== 1 || dimensionFract[2] !== 1) throw new Error(`Asserted the fractional dimensions are [1,1,1], but are actually [${dimensionFract}]`);
const scale: [number, number, number] = [1 / gridSize[0], 1 / gridSize[1], 1 / gridSize[2]];
modify(mesh, { scale: scale, shift: Array.from(originFract) as any });
}
Mesh.computeNormals(mesh); // normals only necessary if flatShaded==false
// const boxMesh = makeMeshFromBox([[0,0,0], [1,1,1]], 1);
// const gridSize = volumeInfoCif.volume_data_3d_info.sample_count.value(0); const boxMesh = makeMeshFromBox([[0,0,0], Array.from(gridSize)] as any, 1);
// const cellSize = volumeInfoCif.volume_data_3d_info.spacegroup_cell_size.value(0); const boxMesh = makeMeshFromBox([[0, 0, 0], Array.from(cellSize)] as any, 1);
// mesh = concat(mesh, boxMesh); // debug
return { mesh: mesh, meshIds: Array.from(mesh_id) };
}
function isInverted(mesh: Mesh): boolean {
const vertices = mesh.vertexBuffer.ref.value;
const indices = mesh.indexBuffer.ref.value;
const center = meshCenter(mesh);
const center3 = Vec3.create(3 * center[0], 3 * center[1], 3 * center[2]);
let dirMetric = 0.0;
const [a, b, c, u, v, normal, radius] = [Vec3(), Vec3(), Vec3(), Vec3(), Vec3(), Vec3(), Vec3()];
for (let i = 0; i < indices.length; i += 3) {
Vec3.fromArray(a, vertices, 3 * indices[i]);
Vec3.fromArray(b, vertices, 3 * indices[i + 1]);
Vec3.fromArray(c, vertices, 3 * indices[i + 2]);
Vec3.sub(u, b, a);
Vec3.sub(v, c, b);
Vec3.cross(normal, u, v); // direction of the surface
Vec3.add(radius, a, b);
Vec3.add(radius, radius, c);
Vec3.sub(radius, radius, center3); // direction center -> this triangle
dirMetric += Vec3.dot(radius, normal);
}
return dirMetric < 0;
}
function meshCenter(mesh: Mesh) {
const vertices = mesh.vertexBuffer.ref.value;
const n = vertices.length;
let x = 0.0;
let y = 0.0;
let z = 0.0;
for (let i = 0; i < vertices.length; i += 3) {
x += vertices[i];
y += vertices[i + 1];
z += vertices[i + 2];
}
return Vec3.create(x / n, y / n, z / n);
}
function flattenCoords(x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number>): Float32Array {
const n = x.length;
const out = new Float32Array(3 * n);
for (let i = 0; i < n; i++) {
out[3 * i] = x[i];
out[3 * i + 1] = y[i];
out[3 * i + 2] = z[i];
}
return out;
}
/** Get mapping of unique values to the position of their first occurrence */
function offsetMap(values: ArrayLike<number>) {
const result = new Map<number, number>();
for (let i = 0; i < values.length; i++) {
if (!result.has(values[i])) {
result.set(values[i], i);
}
}
return result;
}
/** Return bounding box */
export function bbox(mesh: Mesh): Box3D | null { // Is there no function for this?
const nVertices = mesh.vertexCount;
const coords = mesh.vertexBuffer.ref.value;
if (nVertices === 0) {
return null;
}
let minX = coords[0], minY = coords[1], minZ = coords[2];
let maxX = minX, maxY = minY, maxZ = minZ;
for (let i = 0; i < 3 * nVertices; i += 3) {
const x = coords[i], y = coords[i + 1], z = coords[i + 2];
if (x < minX) minX = x;
if (y < minY) minY = y;
if (z < minZ) minZ = z;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
if (z > maxZ) maxZ = z;
}
return Box3D.create(Vec3.create(minX, minY, minZ), Vec3.create(maxX, maxY, maxZ));
}
/** Example mesh - 1 triangle */
export function fakeFakeMesh1(): Mesh {
const nVertices = 3;
const nTriangles = 1;
const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
const indices = new Uint32Array([0, 1, 2]);
const normals = new Float32Array([0, 0, 1]);
const groups = new Float32Array([0]);
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
}
/** Example mesh - irregular tetrahedron */
export function fakeMesh4(): Mesh {
const nVertices = 4;
const nTriangles = 4;
const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
const indices = new Uint32Array([0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3]);
const normals = new Float32Array([-1, -1, -1, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
const groups = new Float32Array([0, 1, 2, 3]);
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
}
/** Return a box-shaped mesh */
export function meshFromBox(box: [[number, number, number], [number, number, number]], group: number = 0) {
const [[x0, y0, z0], [x1, y1, z1]] = box;
const vertices = new Float32Array([
x0, y0, z0,
x1, y0, z0,
x0, y1, z0,
x1, y1, z0,
x0, y0, z1,
x1, y0, z1,
x0, y1, z1,
x1, y1, z1,
]);
const indices = new Uint32Array([
2, 1, 0, 1, 2, 3,
1, 4, 0, 4, 1, 5,
3, 5, 1, 5, 3, 7,
2, 7, 3, 7, 2, 6,
0, 6, 2, 6, 0, 4,
4, 7, 6, 7, 4, 5,
]);
const groups = new Float32Array([group, group, group, group, group, group, group, group]);
const normals = new Float32Array(8);
const mesh = Mesh.create(vertices, indices, normals, groups, 8, 12);
Mesh.computeNormals(mesh); // normals only necessary if flatShaded==false
return mesh;
}
function sum(array: number[]): number {
return array.reduce((a, b) => a + b, 0);
}
function concatArrays<T extends TypedArray>(t: new (len: number) => T, arrays: T[]): T {
const totalLength = arrays.map(a => a.length).reduce((a, b) => a + b, 0);
const result: T = new t(totalLength);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
/** Generate random colors (in a cycle) */
export const ColorGenerator = function* () {
const colors = shuffleArray(Object.values(ColorNames));
let i = 0;
while (true) {
yield colors[i];
i++;
if (i >= colors.length) i = 0;
}
}();
function shuffleArray<T>(array: T[]): T[] {
// Stealed from https://www.w3docs.com/snippets/javascript/how-to-randomize-shuffle-a-javascript-array.html
let curId = array.length;
// There remain elements to shuffle
while (0 !== curId) {
// Pick a remaining element
const randId = Math.floor(Math.random() * curId);
curId -= 1;
// Swap it with the current element.
const tmp = array[curId];
array[curId] = array[randId];
array[randId] = tmp;
}
return array;
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) 2021 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 { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -15,7 +14,6 @@ import { CustomPropSymbol } from '../../../mol-script/language/symbol';
import { Type } from '../../../mol-script/language/type';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
export { QualityAssessment };
@@ -73,28 +71,14 @@ namespace QualityAssessment {
localNames.set(ma_qa_metric.id.value(i), name);
}
const residueKey: AtomicIndex.ResidueLabelKey = {
label_entity_id: '',
label_asym_id: '',
label_seq_id: 0,
pdbx_PDB_ins_code: undefined,
};
for (let i = 0, il = ma_qa_metric_local._rowCount; i < il; i++) {
if (model_id.value(i) !== model.modelNum) continue;
const labelAsymId = label_asym_id.value(i);
const entityIndex = index.findEntity(labelAsymId);
residueKey.label_entity_id = model.entities.data.id.value(entityIndex);
residueKey.label_asym_id = labelAsymId;
residueKey.label_seq_id = label_seq_id.value(i);
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 rI = index.findResidue(model.entities.data.id.value(entityIndex), labelAsymId, label_seq_id.value(i));
const name = localNames.get(metric_id.value(i))!;
localMetrics.get(name)!.set(rI, metric_value.value(i));
}
return {

View File

@@ -1,28 +1,17 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
import { Structure, to_mmCIF, Unit } from '../../mol-model/structure';
import { to_mmCIF, Unit } from '../../mol-model/structure';
import { PluginContext } from '../../mol-plugin/context';
import { Task } from '../../mol-task';
import { getFormattedTime } from '../../mol-util/date';
import { download } from '../../mol-util/download';
import { zip } from '../../mol-util/zip/zip';
const ModelExportNameProp = '__ModelExportName__';
export const ModelExport = {
getStructureName(structure: Structure): string | undefined {
return structure.inheritedPropertyData[ModelExportNameProp];
},
setStructureName(structure: Structure, name: string) {
return structure.inheritedPropertyData[ModelExportNameProp] = name;
}
};
export async function exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
try {
await plugin.runTask(_exportHierarchy(plugin, options), { useOverlay: true });
@@ -54,21 +43,19 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
continue;
}
const name = ModelExport.getStructureName(s) || s.model.entryId || 'unnamed';
const name = entryMap.has(s.model.entryId)
? `${s.model.entryId}_${entryMap.get(s.model.entryId)! + 1}.${format}`
: `${s.model.entryId}.${format}`;
entryMap.set(s.model.entryId, (entryMap.get(s.model.entryId) ?? 0) + 1);
const fileName = entryMap.has(name)
? `${name}_${entryMap.get(name)! + 1}.${format}`
: `${name}.${format}`;
entryMap.set(name, (entryMap.get(name) ?? 0) + 1);
await ctx.update({ message: `Exporting ${name}...`, isIndeterminate: true, canAbort: false });
await ctx.update({ message: `Exporting ${s.model.entryId}...`, isIndeterminate: true, canAbort: false });
if (s.elementCount > 100000) {
// Give UI chance to update, only needed for larger structures.
await new Promise(res => setTimeout(res, 50));
}
try {
files.push([fileName, to_mmCIF(name, s, format === 'bcif', { copyAllCategories: true })]);
files.push([name, to_mmCIF(s.model.entryId, s, format === 'bcif', { copyAllCategories: true })]);
} catch (e) {
if (format === 'cif' && s.elementCount > 2000000) {
plugin.log.warn(`[Export] The structure might be too big to be exported as Text CIF, consider using the BinaryCIF format instead.`);

View File

@@ -29,7 +29,7 @@ export { StructureQualityReport };
type StructureQualityReport = PropertyWrapper<{
issues: IndexedCustomProperty.Residue<string[]>,
issueTypes: string[]
} | undefined>
}| undefined>
namespace StructureQualityReport {
export const DefaultServerUrl = 'https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/';

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
# SB NCBR extensions
## Partial charges
This extension visualizes partial atomic charges for atoms and residues. The extension reads charge data of a structure from a mmcif file and displays them as a color gradient on the atoms/residues. The coloring uses two gradients: one for positive charges (white-to-blue) and one for negative charges (red-to-white). The color is interpolated between the appropriate gradient based on the charge value. The extension also displays the charge values in the description label when an atom/residue is selected.
### How to use
To visualize partial charges, you need to provide a mmcif file with the structure and its charges. The charges are stored under the following categories:
```
_sb_ncbr_partial_atomic_charges_meta.id # id of the charges (e.g. 1)
_sb_ncbr_partial_atomic_charges_meta.type # type of the charges (optional, e.g. 'empirical')
_sb_ncbr_partial_atomic_charges_meta.method # calculation method name (e.g. 'QEq', 'SQE+qp/Schindler 2021 (PUB_pept)')
_sb_ncbr_partial_atomic_charges.type_id # id of the charges (pointer to _sb_ncbr_partial_atomic_charges_meta.id)
_sb_ncbr_partial_atomic_charges.atom_id # atom id (pointer to _atom_site.id)
_sb_ncbr_partial_atomic_charges.charge # partial atomic charge
```
> Note that the mmcif item `_partial_atomic_charges_meta.method` is used as a description of the charge set in the UI (described in *Controls*).
The extension will automatically read the charges from the mmcif file and color the structure accordingly.
### Controls
The extension provides controls for setting the color gradient range and for selecting charge type (atom charges or residue charges).
These controls are available in Color Theme settings for 3D Representation cells in the State Tree UI.
There is also a dropdown menu for switching between charge sets.
These controls are available in Custom Model Properties settings for Model cell in the State Tree UI.

View File

@@ -1,3 +0,0 @@
export { SbNcbrPartialCharges } from './partial-charges/behavior';
export { SbNcbrPartialChargesPreset } from './partial-charges/preset';
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';

View File

@@ -1,38 +0,0 @@
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
import { PluginBehavior } from '../../../mol-plugin/behavior';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { SbNcbrPartialChargesColorThemeProvider } from './color';
import { SbNcbrPartialChargesPropertyProvider } from './property';
import { SbNcbrPartialChargesLociLabelProvider } from './labels';
import { SbNcbrPartialChargesPreset } from './preset';
export const SbNcbrPartialCharges = PluginBehavior.create<{ autoAttach: boolean; showToolTip: boolean }>({
name: 'sb-ncbr-partial-charges',
category: 'misc',
display: {
name: 'SB NCBR Partial Charges',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean; showToolTip: boolean }> {
private SbNcbrPartialChargesLociLabelProvider: LociLabelProvider = SbNcbrPartialChargesLociLabelProvider(
this.ctx
);
register(): void {
this.ctx.customModelProperties.register(SbNcbrPartialChargesPropertyProvider, this.params.autoAttach);
this.ctx.representation.structure.themes.colorThemeRegistry.add(SbNcbrPartialChargesColorThemeProvider);
this.ctx.managers.lociLabels.addProvider(this.SbNcbrPartialChargesLociLabelProvider);
this.ctx.builders.structure.representation.registerPreset(SbNcbrPartialChargesPreset);
}
unregister() {
this.ctx.customModelProperties.unregister(SbNcbrPartialChargesPropertyProvider.descriptor.name);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(SbNcbrPartialChargesColorThemeProvider);
this.ctx.managers.lociLabels.removeProvider(this.SbNcbrPartialChargesLociLabelProvider);
this.ctx.builders.structure.representation.unregisterPreset(SbNcbrPartialChargesPreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
showToolTip: PD.Boolean(true),
}),
});

View File

@@ -1,150 +0,0 @@
import { Bond, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { ThemeDataContext } from '../../../mol-theme/theme';
import { Color } from '../../../mol-util/color';
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';
const Colors = {
Bond: Color(0xffffff),
Error: Color(0x00ff00),
MissingCharge: Color(0xffffff),
Negative: Color(0xff0000),
Zero: Color(0xffffff),
Positive: Color(0x0000ff),
getColor: (charge: number, maxCharge: number): Color => {
if (charge === 0) return Colors.Zero;
if (charge <= -maxCharge) return Colors.Negative;
if (charge >= maxCharge) return Colors.Positive;
const t = maxCharge !== 0 ? Math.abs(charge) / maxCharge : 1;
const endColor = charge < 0 ? Colors.Negative : Colors.Positive;
return Color.interpolate(Colors.Zero, endColor, t);
},
};
export const PartialChargesThemeParams = {
maxAbsoluteCharge: PD.Numeric(
0,
{ min: 0 },
{
label: 'Charge Range',
}
),
absolute: PD.Boolean(false, { isHidden: false, label: 'Use Range' }),
chargeType: PD.Select(
'residue',
[
['atom', 'Atom charges'],
['residue', 'Residue charges'],
],
{ isHidden: false }
),
};
export type PartialChargesThemeParams = typeof PartialChargesThemeParams;
export function getPartialChargesThemeParams() {
return PD.clone(PartialChargesThemeParams);
}
export function PartialChargesColorTheme(
ctx: ThemeDataContext,
props: PD.Values<PartialChargesThemeParams>
): ColorTheme<PartialChargesThemeParams> {
const model = ctx.structure?.models[0];
if (!model) {
throw new Error('No model found');
}
const data = SbNcbrPartialChargesPropertyProvider.get(model).value;
if (!data) {
throw new Error('No partial charges data found');
}
const { absolute, chargeType } = props;
const { typeIdToAtomIdToCharge, typeIdToResidueToCharge, maxAbsoluteAtomCharges, maxAbsoluteResidueCharges } = data;
const typeId = SbNcbrPartialChargesPropertyProvider.props(model).typeId;
const atomToCharge = typeIdToAtomIdToCharge.get(typeId);
const residueToCharge = typeIdToResidueToCharge.get(typeId);
let maxCharge = 0;
if (absolute) {
maxCharge = props.maxAbsoluteCharge < 0 ? 0 : props.maxAbsoluteCharge;
} else if (chargeType === 'atom') {
maxCharge = maxAbsoluteAtomCharges.get(typeId) || 0;
} else {
maxCharge = maxAbsoluteResidueCharges.get(typeId) || 0;
}
// forces coloring updates
const contextHash = SbNcbrPartialChargesPropertyProvider.get(model)?.version;
const chargeMap = chargeType === 'atom' ? atomToCharge : residueToCharge;
let color: LocationColor;
if (!chargeMap) {
color = (_: Location): Color => Colors.MissingCharge;
} else {
color = (location: Location): Color => {
let id = -1;
if (StructureElement.Location.is(location)) {
if (Unit.isAtomic(location.unit)) {
id = StructureProperties.atom.id(location);
}
} else if (Bond.isLocation(location)) {
if (Unit.isAtomic(location.aUnit)) {
const l = StructureElement.Location.create(ctx.structure?.root);
l.unit = location.aUnit;
l.element = location.aUnit.elements[location.aIndex];
id = StructureProperties.atom.id(l);
}
}
const charge = chargeMap.get(id);
if (charge === undefined) {
console.warn('No charge found for id', id);
return Colors.MissingCharge;
}
return Colors.getColor(charge, maxCharge);
};
}
return {
factory: PartialChargesColorTheme,
granularity: 'group',
color,
props,
description: 'Color atoms and residues based on their partial charge.',
preferSmoothing: false,
contextHash,
};
}
export const SbNcbrPartialChargesColorThemeProvider: ColorTheme.Provider<
PartialChargesThemeParams,
'sb-ncbr-partial-charges'
> = {
label: 'SB NCBR Partial Charges',
name: 'sb-ncbr-partial-charges',
category: ColorTheme.Category.Atom,
factory: PartialChargesColorTheme,
getParams: getPartialChargesThemeParams,
defaultValues: PD.getDefaultValues(PartialChargesThemeParams),
isApplicable: (ctx: ThemeDataContext) =>
!!ctx.structure &&
ctx.structure.models.some((model) => SbNcbrPartialChargesPropertyProvider.isApplicable(model)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) =>
data.structure
? SbNcbrPartialChargesPropertyProvider.attach(ctx, data.structure.models[0], void 0, true)
: Promise.resolve(),
detach: (data) => data.structure && SbNcbrPartialChargesPropertyProvider.ref(data.structure.models[0], false),
},
};

View File

@@ -1,40 +0,0 @@
import { StructureElement, StructureProperties } from '../../../mol-model/structure';
import { LociLabel } from '../../../mol-plugin-state/manager/loci-label';
import { SbNcbrPartialChargesPropertyProvider, hasPartialChargesCategories } from './property';
import { Loci } from '../../../mol-model/loci';
import { PluginContext } from '../../../mol-plugin/context';
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
export function SbNcbrPartialChargesLociLabelProvider(ctx: PluginContext): LociLabelProvider {
return {
label: (loci: Loci) => {
if (!StructureElement.Loci.is(loci)) return;
const model = loci.structure.model;
if (!hasPartialChargesCategories(model)) return;
const data = SbNcbrPartialChargesPropertyProvider.get(model).value;
if (!data) return;
const loc = StructureElement.Loci.getFirstLocation(loci);
if (!loc) return;
const granularity = ctx.managers.interactivity.props.granularity;
if (granularity !== 'element' && granularity !== 'residue') {
return;
}
const atomId = StructureProperties.atom.id(loc);
const { typeIdToAtomIdToCharge, typeIdToResidueToCharge } = data;
const typeId = SbNcbrPartialChargesPropertyProvider.props(model).typeId;
const showResidueCharge = granularity === 'residue';
const charge = showResidueCharge
? typeIdToResidueToCharge.get(typeId)?.get(atomId)
: typeIdToAtomIdToCharge.get(typeId)?.get(atomId);
const label = granularity === 'residue' ? 'Residue charge' : 'Atom charge';
return `<strong>${label}: ${charge?.toFixed(4) || 'undefined'}</strong>`;
},
group: (label: LociLabel): string => (label as string).toString().replace(/Model [0-9]+/g, 'Models'),
};
}

View File

@@ -1,32 +0,0 @@
import {
PresetStructureRepresentations,
StructureRepresentationPresetProvider,
} from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { SbNcbrPartialChargesPropertyProvider } from './property';
import { SbNcbrPartialChargesColorThemeProvider } from './color';
export const SbNcbrPartialChargesPreset = StructureRepresentationPresetProvider({
id: 'sb-ncbr-partial-charges-preset',
display: {
name: 'SB NCBR Partial Charges',
group: 'Annotation',
description: 'Color atoms and residues based on their partial charge.',
},
isApplicable(a) {
return !!a.data.models.some((m) => SbNcbrPartialChargesPropertyProvider.isApplicable(m));
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const structure = structureCell?.obj?.data;
if (!structureCell || !structure) return {};
const colorTheme = SbNcbrPartialChargesColorThemeProvider.name as any;
return PresetStructureRepresentations.auto.apply(
ref,
{ ...params, theme: { globalName: colorTheme, focus: { name: colorTheme, params: { chargeType: 'atom' } } } },
plugin
);
},
});

View File

@@ -1,204 +0,0 @@
import { Model } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { arrayMinMax } from '../../../mol-util/array';
type TypeId = number;
type IdToCharge = Map<number, number>;
export interface SBNcbrPartialChargeData {
typeIdToMethod: Map<TypeId, string>;
typeIdToAtomIdToCharge: Map<TypeId, IdToCharge>;
typeIdToResidueToCharge: Map<TypeId, IdToCharge>;
maxAbsoluteAtomCharges: IdToCharge;
maxAbsoluteResidueCharges: IdToCharge;
maxAbsoluteAtomChargeAll: number;
params: PartialChargesPropertyParams;
}
const PartialChargesPropertyParams = {
typeId: PD.Select<number>(0, [[0, '0']]),
};
type PartialChargesPropertyParams = typeof PartialChargesPropertyParams;
const DefaultPartialChargesPropertyParams = PD.clone(PartialChargesPropertyParams);
function getParams(model: Model) {
return getData(model).value?.params ?? DefaultPartialChargesPropertyParams;
}
const PropertyKey = 'sb-ncbr-partial-charges-property-data';
function getData(model: Model): CustomProperty.Data<SBNcbrPartialChargeData | undefined> {
if (PropertyKey in model._staticPropertyData) {
return model._staticPropertyData[PropertyKey];
}
let data: CustomProperty.Data<SBNcbrPartialChargeData | undefined>;
if (!SbNcbrPartialChargesPropertyProvider.isApplicable(model)) {
data = { value: undefined };
} else {
const typeIdToMethod = getTypeIdToMethod(model);
const typeIdToAtomIdToCharge = getTypeIdToAtomIdToCharge(model);
const typeIdToResidueToCharge = getTypeIdToResidueIdToCharge(model, typeIdToAtomIdToCharge);
const maxAbsoluteAtomCharges = getMaxAbsoluteCharges(typeIdToAtomIdToCharge);
const maxAbsoluteResidueCharges = getMaxAbsoluteCharges(typeIdToResidueToCharge);
const maxAbsoluteAtomChargeAll = getMaxAbsoluteAtomChargeAll(maxAbsoluteAtomCharges, maxAbsoluteResidueCharges);
const options = Array.from(typeIdToMethod.entries()).map(
([typeId, method]) => [typeId, method] as [number, string]
);
const params = {
typeId: PD.Select<number>(1, options),
};
data = {
value: {
typeIdToMethod,
typeIdToAtomIdToCharge,
typeIdToResidueToCharge,
maxAbsoluteAtomCharges,
maxAbsoluteResidueCharges,
maxAbsoluteAtomChargeAll,
params,
},
};
}
model._staticPropertyData[PropertyKey] = data;
return data;
}
function getTypeIdToMethod(model: Model) {
const typeIdToMethod: SBNcbrPartialChargeData['typeIdToMethod'] = new Map();
const sourceData = model.sourceData as MmcifFormat;
const rowCount = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.rowCount;
const typeIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.getField('id');
const methods = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.getField('method');
if (!typeIds || !methods) {
return typeIdToMethod;
}
for (let i = 0; i < rowCount; ++i) {
const typeId = typeIds.int(i);
const method = methods.str(i);
typeIdToMethod.set(typeId, method);
}
return typeIdToMethod;
}
function getTypeIdToAtomIdToCharge(model: Model): SBNcbrPartialChargeData['typeIdToAtomIdToCharge'] {
const atomIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge'] = new Map();
const sourceData = model.sourceData as MmcifFormat;
const rowCount = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.rowCount;
const typeIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('type_id');
const atomIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('atom_id');
const charges = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('charge');
if (!typeIds || !atomIds || !charges) return atomIdToCharge;
for (let i = 0; i < rowCount; ++i) {
const typeId = typeIds.int(i);
const atomId = atomIds.int(i);
const charge = charges.float(i);
if (!atomIdToCharge.has(typeId)) atomIdToCharge.set(typeId, new Map());
atomIdToCharge.get(typeId)?.set(atomId, charge);
}
return atomIdToCharge;
}
function getTypeIdToResidueIdToCharge(
model: Model,
typeIdToAtomIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge']
) {
const { offsets, count } = model.atomicHierarchy.residueAtomSegments;
const { atomId: atomIds } = model.atomicConformation;
const residueToCharge: SBNcbrPartialChargeData['typeIdToResidueToCharge'] = new Map();
typeIdToAtomIdToCharge.forEach((atomIdToCharge, typeId: number) => {
if (!residueToCharge.has(typeId)) residueToCharge.set(typeId, new Map());
const residueCharges = residueToCharge.get(typeId)!;
for (let rI = 0; rI < count; rI++) {
let charge = 0;
for (let aI = offsets[rI], _aI = offsets[rI + 1]; aI < _aI; aI++) {
const atom_id = atomIds.value(aI);
charge += atomIdToCharge.get(atom_id) || 0;
}
for (let aI = offsets[rI], _aI = offsets[rI + 1]; aI < _aI; aI++) {
const atom_id = atomIds.value(aI);
residueCharges.set(atom_id, charge);
}
}
});
return residueToCharge;
}
function getMaxAbsoluteCharges(
typeIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge']
): SBNcbrPartialChargeData['maxAbsoluteAtomCharges'];
function getMaxAbsoluteCharges(
typeIdToCharge: SBNcbrPartialChargeData['typeIdToResidueToCharge']
): SBNcbrPartialChargeData['maxAbsoluteResidueCharges'] {
const maxAbsoluteCharges: Map<number, number> = new Map();
typeIdToCharge.forEach((idToCharge, typeId) => {
const charges = Array.from(idToCharge.values());
const [min, max] = arrayMinMax(charges);
const bound = Math.max(Math.abs(min), max);
maxAbsoluteCharges.set(typeId, bound);
});
return maxAbsoluteCharges;
}
function getMaxAbsoluteAtomChargeAll(
maxAbsoluteAtomCharges: SBNcbrPartialChargeData['maxAbsoluteAtomCharges'],
maxAbsoluteResidueCharges: SBNcbrPartialChargeData['maxAbsoluteResidueCharges']
): number {
let maxAbsoluteCharge = 0;
maxAbsoluteAtomCharges.forEach((_, typeId) => {
const maxCharge = maxAbsoluteAtomCharges.get(typeId) || 0;
if (maxCharge > maxAbsoluteCharge) maxAbsoluteCharge = maxCharge;
});
maxAbsoluteResidueCharges.forEach((_, typeId) => {
const maxCharge = maxAbsoluteResidueCharges.get(typeId) || 0;
if (maxCharge > maxAbsoluteCharge) maxAbsoluteCharge = maxCharge;
});
return maxAbsoluteCharge;
}
export function hasPartialChargesCategories(model: Model): boolean {
if (!model || !MmcifFormat.is(model.sourceData)) return false;
const { categories } = model.sourceData.data.frame;
return (
'atom_site' in categories &&
'sb_ncbr_partial_atomic_charges' in categories &&
'sb_ncbr_partial_atomic_charges_meta' in categories
);
}
export const SbNcbrPartialChargesPropertyProvider: CustomModelProperty.Provider<
PartialChargesPropertyParams,
SBNcbrPartialChargeData | undefined
> = CustomModelProperty.createProvider({
label: 'SB NCBR Partial Charges Property Provider',
descriptor: CustomPropertyDescriptor({
name: 'sb-ncbr-partial-charges-property-provider',
}),
type: 'static',
defaultParams: DefaultPartialChargesPropertyParams,
getParams: (data: Model) => getParams(data),
isApplicable: (model: Model) => hasPartialChargesCategories(model),
obtain: (_ctx: CustomProperty.Context, model: Model) => Promise.resolve(getData(model)),
});

View File

@@ -1,103 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { ShapeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { PluginCommands } from '../../mol-plugin/commands';
import { Color } from '../../mol-util/color';
import { ColorNames } from '../../mol-util/color/names';
import { BACKGROUND_SEGMENT_VOLUME_THRESHOLD } from '../meshes/mesh-streaming/behavior';
import { createMeshFromUrl } from '../meshes/mesh-extension';
import { Segment } from './volseg-api/data';
import { VolsegEntryData } from './entry-root';
const DEFAULT_MESH_DETAIL: number | null = 5; // null means worst
export class VolsegMeshSegmentationData {
private entryData: VolsegEntryData;
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
async loadSegmentation() {
const hasMeshes = this.entryData.metadata.meshSegmentIds.length > 0;
if (hasMeshes) {
await this.showSegments(this.entryData.metadata.allSegmentIds);
}
}
updateOpacity(opacity: number) {
const visuals = this.entryData.findNodesByTags('mesh-segment-visual');
const update = this.entryData.newUpdate();
for (const visual of visuals) {
update.to(visual).update(ShapeRepresentation3D, p => { (p as any).alpha = opacity; });
}
return update.commit();
}
async highlightSegment(segment: Segment) {
const visuals = this.entryData.findNodesByTags('mesh-segment-visual', `segment-${segment.id}`);
for (const visual of visuals) {
await PluginCommands.Interactivity.Object.Highlight(this.entryData.plugin, { state: this.entryData.plugin.state.data, ref: visual.transform.ref });
}
}
async selectSegment(segment?: number) {
if (segment === undefined || segment < 0) return;
const visuals = this.entryData.findNodesByTags('mesh-segment-visual', `segment-${segment}`);
const reprNode: PluginStateObject.Shape.Representation3D | undefined = visuals[0]?.obj;
if (!reprNode) return;
const loci = reprNode.data.repr.getAllLoci()[0];
if (!loci) return;
this.entryData.plugin.managers.interactivity.lociSelects.select({ loci: loci, repr: reprNode.data.repr }, false);
}
/** Make visible the specified set of mesh segments */
async showSegments(segments: number[]) {
const segmentsToShow = new Set(segments);
const visuals = this.entryData.findNodesByTags('mesh-segment-visual');
for (const visual of visuals) {
const theTag = visual.obj?.tags?.find(tag => tag.startsWith('segment-'));
if (!theTag) continue;
const id = parseInt(theTag.split('-')[1]);
const visibility = segmentsToShow.has(id);
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, !visibility); // true means hide, ¯\_(ツ)_/¯
segmentsToShow.delete(id);
}
const segmentsToCreate = this.entryData.metadata.meshSegmentIds.filter(seg => segmentsToShow.has(seg));
if (segmentsToCreate.length === 0) return;
let group = this.entryData.findNodesByTags('mesh-segmentation-group')[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Segmentation', description: 'Mesh' }, { tags: ['mesh-segmentation-group'], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const totalVolume = this.entryData.metadata.gridTotalVolume;
const awaiting = [];
for (const seg of segmentsToCreate) {
const segment = this.entryData.metadata.getSegment(seg);
if (!segment) continue;
const detail = this.entryData.metadata.getSufficientMeshDetail(seg, DEFAULT_MESH_DETAIL);
const color = segment.colour.length >= 3 ? Color.fromNormalizedArray(segment.colour, 0) : ColorNames.gray;
const url = this.entryData.api.meshUrl_Bcif(this.entryData.source, this.entryData.entryId, seg, detail);
const label = segment.biological_annotation.name ?? `Segment ${seg}`;
const meshPromise = createMeshFromUrl(this.entryData.plugin, url, seg, detail, true, color, group,
BACKGROUND_SEGMENT_VOLUME_THRESHOLD * totalVolume, `<b>${label}</b>`, this.entryData.ref);
awaiting.push(meshPromise);
}
for (const promise of awaiting) await promise;
}
}

View File

@@ -1,60 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
import { VolsegEntryData } from './entry-root';
export class VolsegModelData {
private entryData: VolsegEntryData;
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
private async loadPdb(pdbId: string, parent: StateObjectSelector | StateObjectRef) {
const url = `https://www.ebi.ac.uk/pdbe/entry-files/download/${pdbId}.bcif`;
const dataNode = await this.entryData.plugin.build().to(parent).apply(Download, { url: url, isBinary: true }, { tags: ['fitted-model-data', `pdbid-${pdbId}`] }).commit();
const cifNode = await this.entryData.plugin.build().to(dataNode).apply(ParseCif).commit();
const trajectoryNode = await this.entryData.plugin.build().to(cifNode).apply(TrajectoryFromMmCif).commit();
await this.entryData.plugin.builders.structure.hierarchy.applyPreset(trajectoryNode, 'default', { representationPreset: 'polymer-cartoon' });
return dataNode;
}
async showPdbs(pdbIds: string[]) {
const segmentsToShow = new Set(pdbIds);
const visuals = this.entryData.findNodesByTags('fitted-model-data');
for (const visual of visuals) {
const theTag = visual.obj?.tags?.find(tag => tag.startsWith('pdbid-'));
if (!theTag) continue;
const id = theTag.split('-')[1];
const visibility = segmentsToShow.has(id);
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, !visibility); // true means hide, ¯\_(ツ)_/¯
segmentsToShow.delete(id);
}
const segmentsToCreate = Array.from(segmentsToShow);
if (segmentsToCreate.length === 0) return;
let group = this.entryData.findNodesByTags('fitted-models-group')[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Fitted Models' }, { tags: ['fitted-models-group'], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const awaiting = [];
for (const pdbId of segmentsToCreate) {
awaiting.push(this.loadPdb(pdbId, group));
}
for (const promise of awaiting) await promise;
}
}

View File

@@ -1,377 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { BehaviorSubject, distinctUntilChanged, Subject, throttleTime } from 'rxjs';
import { VolsegVolumeServerConfig } from '.';
import { Loci } from '../../mol-model/loci';
import { ShapeGroup } from '../../mol-model/shape';
import { Volume } from '../../mol-model/volume';
import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectCell, StateSelection, StateTransform } from '../../mol-state';
import { shallowEqualObjects } from '../../mol-util';
import { ParamDefinition } from '../../mol-util/param-definition';
import { MeshlistData } from '../meshes/mesh-extension';
import { DEFAULT_VOLSEG_SERVER, VolumeApiV2 } from './volseg-api/api';
import { Segment } from './volseg-api/data';
import { MetadataWrapper } from './volseg-api/utils';
import { VolsegMeshSegmentationData } from './entry-meshes';
import { VolsegModelData } from './entry-models';
import { VolsegLatticeSegmentationData } from './entry-segmentation';
import { VolsegState, VolsegStateData, VolsegStateParams } from './entry-state';
import { VolsegVolumeData, SimpleVolumeParamValues, VOLUME_VISUAL_TAG } from './entry-volume';
import * as ExternalAPIs from './external-api';
import { VolsegGlobalStateData } from './global-state';
import { applyEllipsis, Choice, isDefined, lazyGetter, splitEntryId } from './helpers';
import { type VolsegStateFromEntry } from './transformers';
import { StateTransforms } from '../../mol-plugin-state/transforms';
export const MAX_VOXELS = 10 ** 7;
// export const MAX_VOXELS = 10 ** 2; // DEBUG
export const BOX: [[number, number, number], [number, number, number]] | null = null;
// export const BOX: [[number, number, number], [number, number, number]] | null = [[-90, -90, -90], [90, 90, 90]]; // DEBUG
const MAX_ANNOTATIONS_IN_LABEL = 6;
const SourceChoice = new Choice({ emdb: 'EMDB', empiar: 'EMPIAR', idr: 'IDR' }, 'emdb');
export type Source = Choice.Values<typeof SourceChoice>;
export function createLoadVolsegParams(plugin?: PluginContext, entrylists: { [source: string]: string[] } = {}) {
const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
return {
serverUrl: ParamDefinition.Text(defaultVolumeServer),
source: ParamDefinition.Mapped(SourceChoice.values[0], SourceChoice.options, src => entryParam(entrylists[src])),
};
}
function entryParam(entries: string[] = []) {
const options: [string, string][] = entries.map(e => [e, e]);
options.push(['__custom__', 'Custom']);
return ParamDefinition.Group({
entryId: ParamDefinition.Select(options[0][0], options, { description: 'Choose an entry from the list, or choose "Custom" and type any entry ID (useful when using other than default server).' }),
customEntryId: ParamDefinition.Text('', { hideIf: p => p.entryId !== '__custom__', description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
}, { isFlat: true });
}
type LoadVolsegParamValues = ParamDefinition.Values<ReturnType<typeof createLoadVolsegParams>>;
export function createVolsegEntryParams(plugin?: PluginContext) {
const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
return {
serverUrl: ParamDefinition.Text(defaultVolumeServer),
source: SourceChoice.PDSelect(),
entryId: ParamDefinition.Text('emd-1832', { description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
};
}
type VolsegEntryParamValues = ParamDefinition.Values<ReturnType<typeof createVolsegEntryParams>>;
export namespace VolsegEntryParamValues {
export function fromLoadVolsegParamValues(params: LoadVolsegParamValues): VolsegEntryParamValues {
let entryId = (params.source.params as any).entryId;
if (entryId === '__custom__') {
entryId = (params.source.params as any).customEntryId;
}
return {
serverUrl: params.serverUrl,
source: params.source.name as Source,
entryId: entryId
};
}
}
export class VolsegEntry extends PluginStateObject.CreateBehavior<VolsegEntryData>({ name: 'Vol & Seg Entry' }) { }
type VolRepr3DT = typeof StateTransforms.Representation.VolumeRepresentation3D
export class VolsegEntryData extends PluginBehavior.WithSubscribers<VolsegEntryParamValues> {
plugin: PluginContext;
ref: string = '';
api: VolumeApiV2;
source: Source;
/** Number part of entry ID; e.g. '1832' */
entryNumber: string;
/** Full entry ID; e.g. 'emd-1832' */
entryId: string;
metadata: MetadataWrapper;
pdbs: string[];
public readonly volumeData = new VolsegVolumeData(this);
private readonly latticeSegmentationData = new VolsegLatticeSegmentationData(this);
private readonly meshSegmentationData = new VolsegMeshSegmentationData(this);
private readonly modelData = new VolsegModelData(this);
private highlightRequest = new Subject<Segment | undefined>();
private getStateNode = lazyGetter(() => this.plugin.state.data.selectQ(q => q.byRef(this.ref).subtree().ofType(VolsegState))[0] as StateObjectCell<VolsegState, StateTransform<typeof VolsegStateFromEntry>>, 'Missing VolsegState node. Must first create VolsegState for this VolsegEntry.');
public currentState = new BehaviorSubject(ParamDefinition.getDefaultValues(VolsegStateParams));
public currentVolume = new BehaviorSubject<StateTransform<VolRepr3DT> | undefined>(undefined);
private constructor(plugin: PluginContext, params: VolsegEntryParamValues) {
super(plugin, params);
this.plugin = plugin;
this.api = new VolumeApiV2(params.serverUrl);
this.source = params.source;
this.entryId = params.entryId;
this.entryNumber = splitEntryId(this.entryId).entryNumber;
}
private async initialize() {
const metadata = await this.api.getMetadata(this.source, this.entryId);
this.metadata = new MetadataWrapper(metadata);
this.pdbs = await ExternalAPIs.getPdbIdsForEmdbEntry(this.metadata.raw.grid.general.source_db_id ?? this.entryId);
// TODO use Asset?
}
static async create(plugin: PluginContext, params: VolsegEntryParamValues) {
const result = new VolsegEntryData(plugin, params);
await result.initialize();
return result;
}
async register(ref: string) {
this.ref = ref;
this.plugin.managers.lociLabels.addProvider(this.labelProvider);
try {
const params = this.getStateNode().obj?.data;
if (params) {
this.currentState.next(params);
}
} catch {
// do nothing
}
const volumeVisual = this.findNodesByTags(VOLUME_VISUAL_TAG)[0];
if (volumeVisual) this.currentVolume.next(volumeVisual.transform);
let volumeRef: string | undefined;
this.subscribeObservable(this.plugin.state.data.events.cell.stateUpdated, e => {
try { (this.getStateNode()); } catch { return; } // if state not does not exist yet
if (e.cell.transform.ref === this.getStateNode().transform.ref) {
const newState = this.getStateNode().obj?.data;
if (newState && !shallowEqualObjects(newState, this.currentState.value)) { // avoid repeated update
this.currentState.next(newState);
}
} else if (e.cell.transform.tags?.includes(VOLUME_VISUAL_TAG)) {
if (e.ref === volumeRef) {
this.currentVolume.next(e.cell.transform);
} else if (StateSelection.findAncestor(this.plugin.state.data.tree, this.plugin.state.data.cells, e.ref, a => a.transform.ref === ref)) {
volumeRef = e.ref;
this.currentVolume.next(e.cell.transform);
}
}
});
this.subscribeObservable(this.plugin.state.data.events.cell.removed, e => {
if (e.ref === volumeRef) {
volumeRef = undefined;
this.currentVolume.next(undefined);
}
});
this.subscribeObservable(this.plugin.behaviors.interaction.click, async e => {
const loci = e.current.loci;
const clickedSegment = this.getSegmentIdFromLoci(loci);
if (clickedSegment === undefined) return;
if (clickedSegment === this.currentState.value.selectedSegment) {
this.actionSelectSegment(undefined);
} else {
this.actionSelectSegment(clickedSegment);
}
});
this.subscribeObservable(
this.highlightRequest.pipe(throttleTime(50, undefined, { leading: true, trailing: true })),
async segment => await this.highlightSegment(segment)
);
this.subscribeObservable(
this.currentState.pipe(distinctUntilChanged((a, b) => a.selectedSegment === b.selectedSegment)),
async state => {
if (VolsegGlobalStateData.getGlobalState(this.plugin)?.selectionMode) await this.selectSegment(state.selectedSegment);
}
);
}
async unregister() {
this.plugin.managers.lociLabels.removeProvider(this.labelProvider);
}
async loadVolume() {
const result = await this.volumeData.loadVolume();
if (result) {
const isovalue = result.isovalue.kind === 'relative' ? result.isovalue.relativeValue : result.isovalue.absoluteValue;
await this.updateStateNode({ volumeIsovalueKind: result.isovalue.kind, volumeIsovalueValue: isovalue });
}
}
async loadSegmentations() {
await this.latticeSegmentationData.loadSegmentation();
await this.meshSegmentationData.loadSegmentation();
await this.actionShowSegments(this.metadata.allSegmentIds);
}
actionHighlightSegment(segment?: Segment) {
this.highlightRequest.next(segment);
}
async actionToggleSegment(segment: number) {
const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
if (current.includes(segment)) {
await this.actionShowSegments(current.filter(s => s !== segment));
} else {
await this.actionShowSegments([...current, segment]);
}
}
async actionToggleAllSegments() {
const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
if (current.length !== this.metadata.allSegments.length) {
await this.actionShowSegments(this.metadata.allSegmentIds);
} else {
await this.actionShowSegments([]);
}
}
async actionSelectSegment(segment?: number) {
if (segment !== undefined && this.currentState.value.visibleSegments.find(s => s.segmentId === segment) === undefined) {
// first make the segment visible if it is not
await this.actionToggleSegment(segment);
}
await this.updateStateNode({ selectedSegment: segment });
}
async actionSetOpacity(opacity: number) {
if (opacity === this.getStateNode().obj?.data.segmentOpacity) return;
this.latticeSegmentationData.updateOpacity(opacity);
this.meshSegmentationData.updateOpacity(opacity);
await this.updateStateNode({ segmentOpacity: opacity });
}
async actionShowFittedModel(pdbIds: string[]) {
await this.modelData.showPdbs(pdbIds);
await this.updateStateNode({ visibleModels: pdbIds.map(pdbId => ({ pdbId: pdbId })) });
}
async actionSetVolumeVisual(type: 'isosurface' | 'direct-volume' | 'off') {
await this.volumeData.setVolumeVisual(type);
await this.updateStateNode({ volumeType: type });
}
async actionUpdateVolumeVisual(params: SimpleVolumeParamValues) {
await this.volumeData.updateVolumeVisual(params);
await this.updateStateNode({
volumeType: params.volumeType,
volumeOpacity: params.opacity,
});
}
private async actionShowSegments(segments: number[]) {
await this.latticeSegmentationData.showSegments(segments);
await this.meshSegmentationData.showSegments(segments);
await this.updateStateNode({ visibleSegments: segments.map(s => ({ segmentId: s })) });
}
private async highlightSegment(segment?: Segment) {
await PluginCommands.Interactivity.ClearHighlights(this.plugin);
if (segment) {
await this.latticeSegmentationData.highlightSegment(segment);
await this.meshSegmentationData.highlightSegment(segment);
}
}
private async selectSegment(segment: number) {
this.plugin.managers.interactivity.lociSelects.deselectAll();
await this.latticeSegmentationData.selectSegment(segment);
await this.meshSegmentationData.selectSegment(segment);
await this.highlightSegment();
}
private async updateStateNode(params: Partial<VolsegStateData>) {
const oldParams = this.getStateNode().transform.params;
const newParams = { ...oldParams, ...params };
const state = this.plugin.state.data;
const update = state.build().to(this.getStateNode().transform.ref).update(newParams);
await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
}
/** Find the nodes under this entry root which have all of the given tags. */
findNodesByTags(...tags: string[]) {
return this.plugin.state.data.selectQ(q => {
let builder = q.byRef(this.ref).subtree();
for (const tag of tags) builder = builder.withTag(tag);
return builder;
});
}
newUpdate() {
if (this.ref !== '') {
return this.plugin.build().to(this.ref);
} else {
return this.plugin.build().toRoot();
}
}
private readonly labelProvider: LociLabelProvider = {
label: (loci: Loci): string | undefined => {
const segmentId = this.getSegmentIdFromLoci(loci);
if (segmentId === undefined) return;
const segment = this.metadata.getSegment(segmentId);
if (!segment) return;
const annotLabels = segment.biological_annotation.external_references.map(annot => `${applyEllipsis(annot.label)} [${annot.resource}:${annot.accession}]`);
if (annotLabels.length === 0) return;
if (annotLabels.length > MAX_ANNOTATIONS_IN_LABEL + 1) {
const nHidden = annotLabels.length - MAX_ANNOTATIONS_IN_LABEL;
annotLabels.length = MAX_ANNOTATIONS_IN_LABEL;
annotLabels.push(`(${nHidden} more annotations, click on the segment to see all)`);
}
return '<hr class="msp-highlight-info-hr"/>' + annotLabels.filter(isDefined).join('<br/>');
}
};
private getSegmentIdFromLoci(loci: Loci): number | undefined {
if (Volume.Segment.isLoci(loci) && loci.volume._propertyData.ownerId === this.ref) {
if (loci.segments.length === 1) {
return loci.segments[0];
}
}
if (ShapeGroup.isLoci(loci)) {
const meshData = (loci.shape.sourceData ?? {}) as MeshlistData;
if (meshData.ownerId === this.ref && meshData.segmentId !== undefined) {
return meshData.segmentId;
}
}
}
async setTryUseGpu(tryUseGpu: boolean) {
await Promise.all([
this.volumeData.setTryUseGpu(tryUseGpu),
this.latticeSegmentationData.setTryUseGpu(tryUseGpu),
]);
}
async setSelectionMode(selectSegments: boolean) {
if (selectSegments) {
await this.selectSegment(this.currentState.value.selectedSegment);
} else {
this.plugin.managers.interactivity.lociSelects.deselectAll();
}
}
}

View File

@@ -1,131 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Volume } from '../../mol-model/volume';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { VolumeFromSegmentationCif } from '../../mol-plugin-state/transforms/volume';
import { PluginCommands } from '../../mol-plugin/commands';
import { Color } from '../../mol-util/color';
import { Segment } from './volseg-api/data';
import { BOX, VolsegEntryData, MAX_VOXELS } from './entry-root';
import { VolumeVisualParams } from './entry-volume';
import { VolsegGlobalStateData } from './global-state';
const GROUP_TAG = 'lattice-segmentation-group';
const SEGMENT_VISUAL_TAG = 'lattice-segment-visual';
const DEFAULT_SEGMENT_COLOR = Color.fromNormalizedRgb(0.8, 0.8, 0.8);
export class VolsegLatticeSegmentationData {
private entryData: VolsegEntryData;
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
async loadSegmentation() {
const hasLattices = this.entryData.metadata.raw.grid.segmentation_lattices.segmentation_lattice_ids.length > 0;
if (hasLattices) {
const url = this.entryData.api.latticeUrl(this.entryData.source, this.entryData.entryId, 0, BOX, MAX_VOXELS);
let group = this.entryData.findNodesByTags(GROUP_TAG)[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup,
{ label: 'Segmentation', description: 'Lattice' }, { tags: [GROUP_TAG], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const segmentLabels = this.entryData.metadata.allSegments.map(seg => ({ id: seg.id, label: seg.biological_annotation.name ? `<b>${seg.biological_annotation.name}</b>` : '' }));
const volumeNode = await this.entryData.newUpdate().to(group)
.apply(Download, { url, isBinary: true, label: `Segmentation Data: ${url}` })
.apply(ParseCif)
.apply(VolumeFromSegmentationCif, { blockHeader: 'SEGMENTATION_DATA', segmentLabels: segmentLabels, ownerId: this.entryData.ref })
.commit();
const volumeData = volumeNode.data as Volume;
const segmentation = Volume.Segmentation.get(volumeData);
const segmentIds: number[] = Array.from(segmentation?.segments.keys() ?? []);
await this.entryData.newUpdate().to(volumeNode)
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.entryData.plugin, volumeData, {
type: 'segment',
typeParams: { tryUseGpu: VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu },
color: 'volume-segment',
colorParams: { palette: this.createPalette(segmentIds) },
}), { tags: [SEGMENT_VISUAL_TAG] }).commit();
}
}
private createPalette(segmentIds: number[]) {
const colorMap = new Map<number, Color>();
for (const segment of this.entryData.metadata.allSegments) {
const color = Color.fromNormalizedArray(segment.colour, 0);
colorMap.set(segment.id, color);
}
if (colorMap.size === 0) return undefined;
for (const segid of segmentIds) {
colorMap.get(segid);
}
const colors = segmentIds.map(segid => colorMap.get(segid) ?? DEFAULT_SEGMENT_COLOR);
return { name: 'colors' as const, params: { list: { kind: 'set' as const, colors: colors } } };
}
async updateOpacity(opacity: number) {
const reprs = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG);
const update = this.entryData.newUpdate();
for (const s of reprs) {
update.to(s).update(StateTransforms.Representation.VolumeRepresentation3D, p => { p.type.params.alpha = opacity; });
}
return await update.commit();
}
private makeLoci(segments: number[]) {
const vis = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG)[0];
if (!vis) return undefined;
const repr = vis.obj?.data.repr;
const wholeLoci = repr.getAllLoci()[0];
if (!wholeLoci || !Volume.Segment.isLoci(wholeLoci)) return undefined;
return { loci: Volume.Segment.Loci(wholeLoci.volume, segments), repr: repr };
}
async highlightSegment(segment: Segment) {
const segmentLoci = this.makeLoci([segment.id]);
if (!segmentLoci) return;
this.entryData.plugin.managers.interactivity.lociHighlights.highlight(segmentLoci, false);
}
async selectSegment(segment?: number) {
if (segment === undefined || segment < 0) return;
const segmentLoci = this.makeLoci([segment]);
if (!segmentLoci) return;
this.entryData.plugin.managers.interactivity.lociSelects.select(segmentLoci, false);
}
/** Make visible the specified set of lattice segments */
async showSegments(segments: number[]) {
const repr = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG)[0];
if (!repr) return;
const selectedSegment = this.entryData.currentState.value.selectedSegment;
const mustReselect = segments.includes(selectedSegment) && !repr.params?.values.type.params.segments.includes(selectedSegment);
const update = this.entryData.newUpdate();
update.to(repr).update(StateTransforms.Representation.VolumeRepresentation3D, p => { p.type.params.segments = segments; });
await update.commit();
if (mustReselect) {
await this.selectSegment(this.entryData.currentState.value.selectedSegment);
}
}
async setTryUseGpu(tryUseGpu: boolean) {
const visuals = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG);
for (const visual of visuals) {
const oldParams: VolumeVisualParams = visual.transform.params;
if (oldParams.type.params.tryUseGpu === !tryUseGpu) {
const newParams = { ...oldParams, type: { ...oldParams.type, params: { ...oldParams.type.params, tryUseGpu: tryUseGpu } } };
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
}
}

View File

@@ -1,33 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Choice } from './helpers';
export const VolumeTypeChoice = new Choice({ 'isosurface': 'Isosurface', 'direct-volume': 'Direct volume', 'off': 'Off' }, 'isosurface');
export type VolumeType = Choice.Values<typeof VolumeTypeChoice>
export const VolsegStateParams = {
volumeType: VolumeTypeChoice.PDSelect(),
volumeIsovalueKind: PD.Select('relative', [['relative', 'Relative'], ['absolute', 'Absolute']]),
volumeIsovalueValue: PD.Numeric(1),
volumeOpacity: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }),
segmentOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.05 }),
selectedSegment: PD.Numeric(-1, { step: 1 }),
visibleSegments: PD.ObjectList({ segmentId: PD.Numeric(0) }, s => s.segmentId.toString()),
visibleModels: PD.ObjectList({ pdbId: PD.Text('') }, s => s.pdbId.toString()),
};
export type VolsegStateData = PD.Values<typeof VolsegStateParams>;
export class VolsegState extends PluginStateObject.Create<VolsegStateData>({ name: 'Vol & Seg Entry State', typeClass: 'Data' }) { }
export const VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME = 'volseg-state-from-entry'; // defined here to avoid cyclic dependency

View File

@@ -1,191 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Vec2 } from '../../mol-math/linear-algebra';
import { Volume } from '../../mol-model/volume';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { Download } from '../../mol-plugin-state/transforms/data';
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { PluginCommands } from '../../mol-plugin/commands';
import { StateObjectSelector } from '../../mol-state';
import { Color } from '../../mol-util/color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { BOX, VolsegEntryData, MAX_VOXELS } from './entry-root';
import { VolsegStateParams, VolumeTypeChoice } from './entry-state';
import * as ExternalAPIs from './external-api';
import { VolsegGlobalStateData } from './global-state';
const GROUP_TAG = 'volume-group';
export const VOLUME_VISUAL_TAG = 'volume-visual';
const DIRECT_VOLUME_RELATIVE_PEAK_HALFWIDTH = 0.5;
export type VolumeVisualParams = ReturnType<typeof createVolumeRepresentationParams>;
interface VolumeStats { min: number, max: number, mean: number, sigma: number };
export const SimpleVolumeParams = {
volumeType: VolumeTypeChoice.PDSelect(),
opacity: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }, { hideIf: p => p.volumeType === 'off' }),
};
export type SimpleVolumeParamValues = PD.Values<typeof SimpleVolumeParams>;
export class VolsegVolumeData {
private entryData: VolsegEntryData;
private visualTypeParamCache: { [type: string]: any } = {};
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
async loadVolume() {
const hasVolumes = this.entryData.metadata.raw.grid.volumes.volume_downsamplings.length > 0;
if (hasVolumes) {
const isoLevelPromise = ExternalAPIs.tryGetIsovalue(this.entryData.metadata.raw.grid.general.source_db_id ?? this.entryData.entryId);
let group = this.entryData.findNodesByTags(GROUP_TAG)[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Volume' }, { tags: [GROUP_TAG], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const url = this.entryData.api.volumeUrl(this.entryData.source, this.entryData.entryId, BOX, MAX_VOXELS);
const data = await this.entryData.newUpdate().to(group).apply(Download, { url, isBinary: true, label: `Volume Data: ${url}` }).commit();
const parsed = await this.entryData.plugin.dataFormats.get('dscif')!.parse(this.entryData.plugin, data);
const volumeNode: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
const volumeData = volumeNode.cell!.obj!.data;
const volumeType = VolsegStateParams.volumeType.defaultValue;
let isovalue = await isoLevelPromise;
if (!isovalue) {
const stats = volumeData.grid.stats;
const maxRelative = (stats.max - stats.mean) / stats.sigma;
if (maxRelative > 1) {
isovalue = { kind: 'relative', value: 1.0 };
} else {
isovalue = { kind: 'relative', value: maxRelative * 0.5 };
}
}
const adjustedIsovalue = Volume.adjustedIsoValue(volumeData, isovalue.value, isovalue.kind);
const visualParams = this.createVolumeVisualParams(volumeData, volumeType);
this.changeIsovalueInVolumeVisualParams(visualParams, adjustedIsovalue, volumeData.grid.stats);
await this.entryData.newUpdate()
.to(volumeNode)
.apply(StateTransforms.Representation.VolumeRepresentation3D, visualParams, { tags: [VOLUME_VISUAL_TAG], state: { isHidden: volumeType === 'off' } })
.commit();
return { isovalue: adjustedIsovalue };
}
}
async setVolumeVisual(type: 'isosurface' | 'direct-volume' | 'off') {
const visual = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG)[0];
if (!visual) return;
const oldParams: VolumeVisualParams = visual.transform.params;
this.visualTypeParamCache[oldParams.type.name] = oldParams.type.params;
if (type === 'off') {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, true); // true means hide, ¯\_(ツ)_/¯
} else {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, false); // true means hide, ¯\_(ツ)_/¯
if (oldParams.type.name === type) return;
const newParams: VolumeVisualParams = {
...oldParams,
type: {
name: type,
params: this.visualTypeParamCache[type] ?? oldParams.type.params,
}
};
const volumeStats = visual.obj?.data.sourceData.grid.stats;
if (!volumeStats) throw new Error(`Cannot get volume stats from volume visual ${visual.transform.ref}`);
this.changeIsovalueInVolumeVisualParams(newParams, undefined, volumeStats);
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
async updateVolumeVisual(newParams: SimpleVolumeParamValues) {
const { volumeType, opacity } = newParams;
const visual = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG)[0];
if (!visual) return;
const oldVisualParams: VolumeVisualParams = visual.transform.params;
this.visualTypeParamCache[oldVisualParams.type.name] = oldVisualParams.type.params;
if (volumeType === 'off') {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, true); // true means hide, ¯\_(ツ)_/¯
} else {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, false); // true means hide, ¯\_(ツ)_/¯
const newVisualParams: VolumeVisualParams = {
...oldVisualParams,
type: {
name: volumeType,
params: this.visualTypeParamCache[volumeType] ?? oldVisualParams.type.params,
}
};
newVisualParams.type.params.alpha = opacity;
const volumeStats = visual.obj?.data.sourceData.grid.stats;
if (!volumeStats) throw new Error(`Cannot get volume stats from volume visual ${visual.transform.ref}`);
this.changeIsovalueInVolumeVisualParams(newVisualParams, undefined, volumeStats);
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newVisualParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
async setTryUseGpu(tryUseGpu: boolean) {
const visuals = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG);
for (const visual of visuals) {
const oldParams: VolumeVisualParams = visual.transform.params;
if (oldParams.type.params.tryUseGpu === !tryUseGpu) {
const newParams = { ...oldParams, type: { ...oldParams.type, params: { ...oldParams.type.params, tryUseGpu: tryUseGpu } } };
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
}
private getIsovalueFromState(): Volume.IsoValue {
const { volumeIsovalueKind, volumeIsovalueValue } = this.entryData.currentState.value;
return volumeIsovalueKind === 'relative'
? Volume.IsoValue.relative(volumeIsovalueValue)
: Volume.IsoValue.absolute(volumeIsovalueValue);
}
private createVolumeVisualParams(volume: Volume, type: 'isosurface' | 'direct-volume' | 'off'): VolumeVisualParams {
if (type === 'off') type = 'isosurface';
return createVolumeRepresentationParams(this.entryData.plugin, volume, {
type: type,
typeParams: { alpha: 0.2, tryUseGpu: VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu },
color: 'uniform',
colorParams: { value: Color(0x121212) },
});
}
private changeIsovalueInVolumeVisualParams(params: VolumeVisualParams, isovalue: Volume.IsoValue | undefined, stats: VolumeStats) {
isovalue ??= this.getIsovalueFromState();
switch (params.type.name) {
case 'isosurface':
params.type.params.isoValue = isovalue;
params.type.params.tryUseGpu = VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu;
break;
case 'direct-volume':
const absIso = Volume.IsoValue.toAbsolute(isovalue, stats).absoluteValue;
const fractIso = (absIso - stats.min) / (stats.max - stats.min);
const peakHalfwidth = DIRECT_VOLUME_RELATIVE_PEAK_HALFWIDTH * stats.sigma / (stats.max - stats.min);
params.type.params.controlPoints = [
Vec2.create(Math.max(fractIso - peakHalfwidth, 0), 0),
Vec2.create(fractIso, 1),
Vec2.create(Math.min(fractIso + peakHalfwidth, 1), 0),
];
break;
}
}
}

View File

@@ -1,51 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { splitEntryId } from './helpers';
/** Try to get author-defined contour value for isosurface from EMDB API. Return relative value 1.0, if not applicable or fails. */
export async function tryGetIsovalue(entryId: string): Promise<{ kind: 'absolute' | 'relative', value: number } | undefined> {
const split = splitEntryId(entryId);
if (split.source === 'emdb') {
try {
const response = await fetch(`https://www.ebi.ac.uk/emdb/api/entry/map/${split.entryNumber}`);
const json = await response.json();
const contours: any[] = json?.map?.contour_list?.contour;
if (contours && contours.length > 0) {
const theContour = contours.find(c => c.primary) || contours[0];
if (theContour.level === undefined) throw new Error('EMDB API response missing contour level.');
return { kind: 'absolute', value: theContour.level };
}
} catch {
// do nothing
}
}
return undefined;
}
export async function getPdbIdsForEmdbEntry(entryId: string): Promise<string[]> {
const split = splitEntryId(entryId);
const result = [];
if (split.source === 'emdb') {
entryId = entryId.toUpperCase();
const apiUrl = `https://www.ebi.ac.uk/pdbe/api/emdb/entry/fitted/${entryId}`;
try {
const response = await fetch(apiUrl);
if (response.ok) {
const json = await response.json();
const jsonEntry = json[entryId] ?? [];
for (const record of jsonEntry) {
const pdbs = record?.fitted_emdb_id_list?.pdb_id ?? [];
result.push(...pdbs);
}
}
} catch (ex) {
// do nothing
}
}
return result;
}

View File

@@ -1,65 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { BehaviorSubject } from 'rxjs';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { PluginContext } from '../../mol-plugin/context';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { VolsegEntry } from './entry-root';
import { isDefined } from './helpers';
export const VolsegGlobalStateParams = {
tryUseGpu: PD.Boolean(true, { description: 'Attempt using GPU for faster rendering. \nCaution: with some hardware setups, this might render some objects incorrectly or not at all.' }),
selectionMode: PD.Boolean(true, { description: 'Allow selecting/deselecting a segment by clicking on it.' }),
};
export type VolsegGlobalStateParamValues = PD.Values<typeof VolsegGlobalStateParams>;
export class VolsegGlobalState extends PluginStateObject.CreateBehavior<VolsegGlobalStateData>({ name: 'Vol & Seg Global State' }) { }
export class VolsegGlobalStateData extends PluginBehavior.WithSubscribers<VolsegGlobalStateParamValues> {
private ref: string;
currentState = new BehaviorSubject(PD.getDefaultValues(VolsegGlobalStateParams));
constructor(plugin: PluginContext, params: VolsegGlobalStateParamValues) {
super(plugin, params);
this.currentState.next(params);
}
register(ref: string) {
this.ref = ref;
}
unregister() {
this.ref = '';
}
isRegistered() {
return this.ref !== '';
}
async updateState(plugin: PluginContext, state: Partial<VolsegGlobalStateParamValues>) {
const oldState = this.currentState.value;
const promises = [];
const allEntries = plugin.state.data.selectQ(q => q.ofType(VolsegEntry)).map(cell => cell.obj?.data).filter(isDefined);
if (state.tryUseGpu !== undefined && state.tryUseGpu !== oldState.tryUseGpu) {
for (const entry of allEntries) {
promises.push(entry.setTryUseGpu(state.tryUseGpu));
}
}
if (state.selectionMode !== undefined && state.selectionMode !== oldState.selectionMode) {
for (const entry of allEntries) {
promises.push(entry.setSelectionMode(state.selectionMode));
}
}
await Promise.all(promises);
await plugin.build().to(this.ref).update(state).commit();
}
static getGlobalState(plugin: PluginContext): VolsegGlobalStateParamValues | undefined {
return plugin.state.data.selectQ(q => q.ofType(VolsegGlobalState))[0]?.obj?.data.currentState.value;
}
}

View File

@@ -1,163 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Volume } from '../../mol-model/volume';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { StateBuilder, StateObjectSelector, StateTransformer } from '../../mol-state';
import { ParamDefinition } from '../../mol-util/param-definition';
import { Source } from './entry-root';
/** Split entry ID (e.g. 'emd-1832') into source ('emdb') and number ('1832') */
export function splitEntryId(entryId: string) {
const PREFIX_TO_SOURCE: { [prefix: string]: Source } = { 'emd': 'emdb' };
const [prefix, entry] = entryId.split('-');
return {
source: PREFIX_TO_SOURCE[prefix] ?? prefix,
entryNumber: entry
};
}
/** Create entry ID (e.g. 'emd-1832') for a combination of source ('emdb') and number ('1832') */
export function createEntryId(source: Source, entryNumber: string | number) {
const SOURCE_TO_PREFIX: { [prefix: string]: string } = { 'emdb': 'emd' };
const prefix = SOURCE_TO_PREFIX[source] ?? source;
return `${prefix}-${entryNumber}`;
}
/**
* Represents a set of values to choose from, with a default value. Example:
* ```
* export const MyChoice = new Choice({ yes: 'I agree', no: 'Nope' }, 'yes');
* export type MyChoiceType = Choice.Values<typeof MyChoice>; // 'yes'|'no'
* ```
*/
export class Choice<T extends string, D extends T> {
readonly defaultValue: D;
readonly options: [T, string][];
private readonly nameDict: { [value in T]: string };
constructor(opts: { [value in T]: string }, defaultValue: D) {
this.defaultValue = defaultValue;
this.options = Object.keys(opts).map(k => [k as T, opts[k as T]]);
this.nameDict = opts;
}
PDSelect(defaultValue?: T, info?: ParamDefinition.Info): ParamDefinition.Select<T> {
return ParamDefinition.Select<T>(defaultValue ?? this.defaultValue, this.options, info);
}
prettyName(value: T): string {
return this.nameDict[value];
}
get values(): T[] {
return this.options.map(([value, pretty]) => value);
}
}
export namespace Choice {
export type Values<T extends Choice<any, any>> = T extends Choice<infer R, any> ? R : any;
}
export function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
export class NodeManager {
private nodes: { [key: string]: StateObjectSelector };
constructor() {
this.nodes = {};
}
private static nodeExists(node: StateObjectSelector): boolean {
try {
return node.checkValid();
} catch {
return false;
}
}
public getNode(key: string): StateObjectSelector | undefined {
const node = this.nodes[key];
if (node && !NodeManager.nodeExists(node)) {
delete this.nodes[key];
return undefined;
}
return node;
}
public getNodes(): StateObjectSelector[] {
return Object.keys(this.nodes).map(key => this.getNode(key)).filter(node => node) as StateObjectSelector[];
}
public deleteAllNodes(update: StateBuilder.Root) {
for (const node of this.getNodes()) {
update.delete(node);
}
this.nodes = {};
}
public hideAllNodes() {
for (const node of this.getNodes()) {
setSubtreeVisibility(node.state!, node.ref, true); // hide
}
}
public async showNode(key: string, factory: () => StateObjectSelector | Promise<StateObjectSelector>, forceVisible: boolean = true) {
let node = this.getNode(key);
if (node) {
if (forceVisible) {
setSubtreeVisibility(node.state!, node.ref, false); // show
}
} else {
node = await factory();
this.nodes[key] = node;
}
return node;
}
}
const CreateTransformer = StateTransformer.builderFactory('volseg');
export const CreateVolume = CreateTransformer({
name: 'create-transformer',
from: PluginStateObject.Root,
to: PluginStateObject.Volume.Data,
params: {
label: ParamDefinition.Text('Volume', { isHidden: true }),
description: ParamDefinition.Text('', { isHidden: true }),
volume: ParamDefinition.Value<Volume>(undefined as any, { isHidden: true }),
}
})({
apply({ params }) {
return new PluginStateObject.Volume.Data(params.volume, { label: params.label, description: params.description });
}
});
export function applyEllipsis(name: string, max_chars: number = 60) {
if (name.length <= max_chars) return name;
const beginning = name.substring(0, max_chars);
let lastSpace = beginning.lastIndexOf(' ');
if (lastSpace === -1) return beginning + '...';
if (lastSpace > 0 && ',;.'.includes(name.charAt(lastSpace - 1))) lastSpace--;
return name.substring(0, lastSpace) + '...';
}
export function lazyGetter<T>(getter: () => T, errorIfUndefined?: string) {
let value: T | undefined = undefined;
return () => {
if (value === undefined) value = getter();
if (errorIfUndefined && value === undefined) throw new Error(errorIfUndefined);
return value;
};
}

View File

@@ -1,102 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject as SO } from '../../mol-plugin-state/objects';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { PluginConfigItem } from '../../mol-plugin/config';
import { PluginContext } from '../../mol-plugin/context';
import { StateAction } from '../../mol-state';
import { Task } from '../../mol-task';
import { DEFAULT_VOLSEG_SERVER, VolumeApiV2 } from './volseg-api/api';
import { VolsegEntryData, VolsegEntryParamValues, createLoadVolsegParams } from './entry-root';
import { VolsegGlobalState } from './global-state';
import { createEntryId } from './helpers';
import { VolsegEntryFromRoot, VolsegGlobalStateFromRoot, VolsegStateFromEntry } from './transformers';
import { VolsegUI } from './ui';
const DEBUGGING = typeof window !== 'undefined' ? window?.location?.hostname === 'localhost' : false;
export const VolsegVolumeServerConfig = {
// DefaultServer: new PluginConfigItem('volseg-volume-server', DEFAULT_VOLUME_SERVER_V2),
DefaultServer: new PluginConfigItem('volseg-volume-server', DEBUGGING ? 'http://localhost:9000/v2' : DEFAULT_VOLSEG_SERVER),
};
export const Volseg = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
name: 'volseg',
category: 'misc',
display: {
name: 'Volseg',
description: 'Volseg'
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
register() {
this.ctx.state.data.actions.add(LoadVolseg);
this.ctx.customStructureControls.set('volseg', VolsegUI as any);
this.initializeEntryLists(); // do not await
const entries = new Map<string, VolsegEntryData>();
this.subscribeObservable(this.ctx.state.data.events.cell.created, o => {
if (o.cell.obj instanceof VolsegEntryData) entries.set(o.ref, o.cell.obj);
});
this.subscribeObservable(this.ctx.state.data.events.cell.removed, o => {
if (entries.has(o.ref)) {
entries.get(o.ref)!.dispose();
entries.delete(o.ref);
}
});
}
unregister() {
this.ctx.state.data.actions.remove(LoadVolseg);
this.ctx.customStructureControls.delete('volseg');
}
private async initializeEntryLists() {
const apiUrl = this.ctx.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
const api = new VolumeApiV2(apiUrl);
const entryLists = await api.getEntryList(10 ** 6);
Object.values(entryLists).forEach(l => l.sort());
(this.ctx.customState as any).volsegAvailableEntries = entryLists;
}
}
});
export const LoadVolseg = StateAction.build({
display: { name: 'Load Volume & Segmentation' },
from: SO.Root,
params: (a, plugin: PluginContext) => {
const res = createLoadVolsegParams(plugin, (plugin.customState as any).volsegAvailableEntries);
return res;
},
})(({ params, state }, ctx: PluginContext) => Task.create('Loading Volume & Segmentation', taskCtx => {
return state.transaction(async () => {
const entryParams = VolsegEntryParamValues.fromLoadVolsegParamValues(params);
if (entryParams.entryId.trim().length === 0) {
alert('Must specify Entry Id!');
throw new Error('Specify Entry Id');
}
if (!entryParams.entryId.includes('-')) {
// add source prefix if the user omitted it (e.g. 1832 -> emd-1832)
entryParams.entryId = createEntryId(entryParams.source, entryParams.entryId);
}
ctx.behaviors.layout.leftPanelTabName.next('data');
const globalStateNode = ctx.state.data.selectQ(q => q.ofType(VolsegGlobalState))[0];
if (!globalStateNode) {
await state.build().toRoot().apply(VolsegGlobalStateFromRoot, {}, { state: { isGhost: !DEBUGGING } }).commit();
}
const entryNode = await state.build().toRoot().apply(VolsegEntryFromRoot, entryParams).commit();
await state.build().to(entryNode).apply(VolsegStateFromEntry, {}, { state: { isGhost: !DEBUGGING } }).commit();
if (entryNode.data) {
await entryNode.data.loadVolume();
await entryNode.data.loadSegmentations();
}
}).runInContext(taskCtx);
}));

View File

@@ -1,70 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
import { PluginContext } from '../../mol-plugin/context';
import { StateTransformer } from '../../mol-state';
import { Task } from '../../mol-task';
import { VolsegEntry, VolsegEntryData, createVolsegEntryParams } from './entry-root';
import { VolsegState, VolsegStateParams, VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME } from './entry-state';
import { VolsegGlobalState, VolsegGlobalStateData, VolsegGlobalStateParams } from './global-state';
export const VolsegEntryFromRoot = PluginStateTransform.BuiltIn({
name: 'volseg-entry-from-root',
display: { name: 'Vol & Seg Entry', description: 'Vol & Seg Entry' },
from: PluginStateObject.Root,
to: VolsegEntry,
params: (a, plugin: PluginContext) => createVolsegEntryParams(plugin),
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Load Vol & Seg Entry', async () => {
const data = await VolsegEntryData.create(plugin, params);
return new VolsegEntry(data, { label: data.entryId, description: 'Vol & Seg Entry' });
});
},
update({ b, oldParams, newParams }) {
Object.assign(newParams, oldParams);
console.error('Changing params of existing VolsegEntry node is not allowed');
return StateTransformer.UpdateResult.Unchanged;
}
});
export const VolsegStateFromEntry = PluginStateTransform.BuiltIn({
name: VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME,
display: { name: 'Vol & Seg Entry State', description: 'Vol & Seg Entry State' },
from: VolsegEntry,
to: VolsegState,
params: VolsegStateParams,
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Create Vol & Seg Entry State', async () => {
return new VolsegState(params, { label: 'State' });
});
}
});
export const VolsegGlobalStateFromRoot = PluginStateTransform.BuiltIn({
name: 'volseg-global-state-from-root',
display: { name: 'Vol & Seg Global State', description: 'Vol & Seg Global State' },
from: PluginStateObject.Root,
to: VolsegGlobalState,
params: VolsegGlobalStateParams,
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Create Vol & Seg Global State', async () => {
const data = new VolsegGlobalStateData(plugin, params);
return new VolsegGlobalState(data, { label: 'Global State', description: 'Vol & Seg Global State' });
});
},
update({ b, oldParams, newParams }) {
b.data.currentState.next(newParams);
return StateTransformer.UpdateResult.Updated;
}
});

View File

@@ -1,264 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
import { Button, ControlRow, ExpandGroup, IconButton } from '../../mol-plugin-ui/controls/common';
import * as Icons from '../../mol-plugin-ui/controls/icons';
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
import { Slider } from '../../mol-plugin-ui/controls/slider';
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
import { UpdateTransformControl } from '../../mol-plugin-ui/state/update-transform';
import { PluginContext } from '../../mol-plugin/context';
import { shallowEqualArrays } from '../../mol-util';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { sleep } from '../../mol-util/sleep';
import { VolsegEntry, VolsegEntryData } from './entry-root';
import { SimpleVolumeParams, SimpleVolumeParamValues } from './entry-volume';
import { VolsegGlobalState, VolsegGlobalStateData, VolsegGlobalStateParams } from './global-state';
import { isDefined } from './helpers';
interface VolsegUIData {
globalState?: VolsegGlobalStateData,
availableNodes: VolsegEntry[],
activeNode?: VolsegEntry,
}
namespace VolsegUIData {
export function changeAvailableNodes(data: VolsegUIData, newNodes: VolsegEntry[]): VolsegUIData {
const newActiveNode = newNodes.length > data.availableNodes.length ?
newNodes[newNodes.length - 1]
: newNodes.find(node => node.data.ref === data.activeNode?.data.ref) ?? newNodes[0];
return { ...data, availableNodes: newNodes, activeNode: newActiveNode };
}
export function changeActiveNode(data: VolsegUIData, newActiveRef: string): VolsegUIData {
const newActiveNode = data.availableNodes.find(node => node.data.ref === newActiveRef) ?? data.availableNodes[0];
return { ...data, availableNodes: data.availableNodes, activeNode: newActiveNode };
}
export function equals(data1: VolsegUIData, data2: VolsegUIData) {
return shallowEqualArrays(data1.availableNodes, data2.availableNodes) && data1.activeNode === data2.activeNode && data1.globalState === data2.globalState;
}
}
export class VolsegUI extends CollapsableControls<{}, { data: VolsegUIData }> {
protected defaultState(): CollapsableState & { data: VolsegUIData } {
return {
header: 'Volume & Segmentation',
isCollapsed: true,
brand: { accent: 'orange', svg: Icons.ExtensionSvg },
data: {
globalState: undefined,
availableNodes: [],
activeNode: undefined,
}
};
}
protected renderControls(): JSX.Element | null {
return <VolsegControls plugin={this.plugin} data={this.state.data} setData={d => this.setState({ data: d })} />;
}
componentDidMount(): void {
this.setState({ isHidden: true, isCollapsed: false });
this.subscribe(this.plugin.state.data.events.changed, e => {
const nodes = e.state.selectQ(q => q.ofType(VolsegEntry)).map(cell => cell?.obj).filter(isDefined);
const isHidden = nodes.length === 0;
const newData = VolsegUIData.changeAvailableNodes(this.state.data, nodes);
if (!this.state.data.globalState?.isRegistered()) {
const globalState = e.state.selectQ(q => q.ofType(VolsegGlobalState))[0]?.obj?.data;
if (globalState) newData.globalState = globalState;
}
if (!VolsegUIData.equals(this.state.data, newData) || this.state.isHidden !== isHidden) {
this.setState({ data: newData, isHidden: isHidden });
}
});
}
}
function VolsegControls({ plugin, data, setData }: { plugin: PluginContext, data: VolsegUIData, setData: (d: VolsegUIData) => void }) {
const entryData = data.activeNode?.data;
if (!entryData) {
return <p>No data!</p>;
}
if (!data.globalState) {
return <p>No global state!</p>;
}
const params = {
/** Reference to the active VolsegEntry node */
entry: PD.Select(data.activeNode!.data.ref, data.availableNodes.map(entry => [entry.data.ref, entry.data.entryId]))
};
const values: PD.Values<typeof params> = {
entry: data.activeNode!.data.ref,
};
const globalState = useBehavior(data.globalState.currentState);
return <>
<ParameterControls params={params} values={values} onChangeValues={next => setData(VolsegUIData.changeActiveNode(data, next.entry))} />
<ExpandGroup header='Global options'>
<WaitingParameterControls params={VolsegGlobalStateParams} values={globalState} onChangeValues={async next => await data.globalState?.updateState(plugin, next)} />
</ExpandGroup>
<VolsegEntryControls entryData={entryData} key={entryData.ref} />
</>;
}
function VolsegEntryControls({ entryData }: { entryData: VolsegEntryData }) {
const state = useBehavior(entryData.currentState);
const allSegments = entryData.metadata.allSegments;
const selectedSegment = entryData.metadata.getSegment(state.selectedSegment);
const visibleSegments = state.visibleSegments.map(seg => seg.segmentId);
const visibleModels = state.visibleModels.map(model => model.pdbId);
const allPdbs = entryData.pdbs;
return <>
{/* Title */}
<div style={{ fontWeight: 'bold', padding: 8, paddingTop: 6, paddingBottom: 4, overflow: 'hidden' }}>
{entryData.metadata.raw.annotation?.name ?? 'Unnamed Annotation'}
</div>
{/* Fitted models */}
{allPdbs.length > 0 && <ExpandGroup header='Fitted models in PDB' initiallyExpanded>
{allPdbs.map(pdb =>
<WaitingButton key={pdb} onClick={() => entryData.actionShowFittedModel(visibleModels.includes(pdb) ? [] : [pdb])}
style={{ fontWeight: visibleModels.includes(pdb) ? 'bold' : undefined, textAlign: 'left', marginTop: 1 }}>
{pdb}
</WaitingButton>
)}
</ExpandGroup>}
{/* Volume */}
<VolumeControls entryData={entryData} />
<ExpandGroup header='Segmentation data' initiallyExpanded>
{/* Segment opacity slider */}
<ControlRow label='Opacity' control={
<WaitingSlider min={0} max={1} value={state.segmentOpacity} step={0.05} onChange={async v => await entryData.actionSetOpacity(v)} />
} />
{/* Segment toggles */}
{allSegments.length > 0 && <>
<WaitingButton onClick={async () => { await sleep(20); await entryData.actionToggleAllSegments(); }} style={{ marginTop: 1 }}>
Toggle All segments
</WaitingButton>
<div style={{ maxHeight: 200, overflow: 'hidden', overflowY: 'auto', marginBlock: 1 }}>
{allSegments.map(segment =>
<div style={{ display: 'flex', marginBottom: 1 }} key={segment.id}
onMouseEnter={() => entryData.actionHighlightSegment(segment)}
onMouseLeave={() => entryData.actionHighlightSegment()}>
<Button onClick={() => entryData.actionSelectSegment(segment !== selectedSegment ? segment.id : undefined)}
style={{ fontWeight: segment.id === selectedSegment?.id ? 'bold' : undefined, marginRight: 1, flexGrow: 1, textAlign: 'left' }}>
<div title={segment.biological_annotation.name ?? 'Unnamed segment'} style={{ maxWidth: 240, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{segment.biological_annotation.name ?? 'Unnamed segment'} ({segment.id})
</div>
</Button>
<IconButton svg={visibleSegments.includes(segment.id) ? Icons.VisibilityOutlinedSvg : Icons.VisibilityOffOutlinedSvg}
onClick={() => entryData.actionToggleSegment(segment.id)} />
</div>
)}
</div>
</>}
</ExpandGroup>
{/* Segment annotations */}
<ExpandGroup header='Selected segment annotation' initiallyExpanded>
<div style={{ paddingTop: 4, paddingRight: 8, maxHeight: 300, overflow: 'hidden', overflowY: 'auto' }}>
{!selectedSegment && 'No segment selected'}
{selectedSegment && <b>Segment {selectedSegment.id}:<br />{selectedSegment.biological_annotation.name ?? 'Unnamed segment'}</b>}
{selectedSegment?.biological_annotation.external_references.map(ref =>
<p key={ref.id} style={{ marginTop: 4 }}>
<small>{ref.resource}:{ref.accession}</small><br />
<b>{capitalize(ref.label)}</b><br />
{ref.description}
</p>)}
</div>
</ExpandGroup>
</>;
}
function VolumeControls({ entryData }: { entryData: VolsegEntryData }) {
const vol = useBehavior(entryData.currentVolume);
if (!vol) return null;
const volumeValues: SimpleVolumeParamValues = {
volumeType: vol.state.isHidden ? 'off' : vol.params?.type.name as any,
opacity: vol.params?.type.params.alpha,
};
return <ExpandGroup header='Volume data' initiallyExpanded>
<WaitingParameterControls params={SimpleVolumeParams} values={volumeValues} onChangeValues={async next => { await sleep(20); await entryData.actionUpdateVolumeVisual(next); }} />
<ExpandGroup header='Detailed Volume Params' headerStyle={{ marginTop: 1 }}>
<UpdateTransformControl state={entryData.plugin.state.data} transform={vol} customHeader='none' />
</ExpandGroup>
</ExpandGroup>;
}
type ComponentParams<T extends React.Component<any, any, any> | ((props: any) => JSX.Element)> =
T extends React.Component<infer P, any, any> ? P : T extends (props: infer P) => JSX.Element ? P : never;
function WaitingSlider({ value, onChange, ...etc }: { value: number, onChange: (value: number) => any } & ComponentParams<Slider>) {
const [changing, sliderValue, execute] = useAsyncChange(value);
return <Slider value={sliderValue} disabled={changing} onChange={newValue => execute(onChange, newValue)} {...etc} />;
}
function WaitingButton({ onClick, ...etc }: { onClick: () => any } & ComponentParams<typeof Button>) {
const [changing, _, execute] = useAsyncChange(undefined);
return <Button disabled={changing} onClick={() => execute(onClick, undefined)} {...etc}>
{etc.children}
</Button>;
}
function WaitingParameterControls<T extends PD.Params>({ values, onChangeValues, ...etc }: { values: PD.ValuesFor<T>, onChangeValues: (values: PD.ValuesFor<T>) => any } & ComponentParams<ParameterControls<T>>) {
const [changing, currentValues, execute] = useAsyncChange(values);
return <ParameterControls isDisabled={changing} values={currentValues} onChangeValues={newValue => execute(onChangeValues, newValue)} {...etc} />;
}
function capitalize(text: string) {
const first = text.charAt(0);
const rest = text.slice(1);
return first.toUpperCase() + rest;
}
function useAsyncChange<T>(initialValue: T) {
const [isExecuting, setIsExecuting] = useState(false);
const [value, setValue] = useState(initialValue);
const isMounted = useRef(false);
useEffect(() => setValue(initialValue), [initialValue]);
useEffect(() => {
isMounted.current = true;
return () => { isMounted.current = false; };
}, []);
const execute = useCallback(
async (func: (val: T) => Promise<any>, val: T) => {
setIsExecuting(true);
setValue(val);
try {
await func(val);
} catch (err) {
if (isMounted.current) {
setValue(initialValue);
}
throw err;
} finally {
if (isMounted.current) {
setIsExecuting(false);
}
}
},
[]
);
return [isExecuting, value, execute] as const;
}

View File

@@ -1,65 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { type Metadata } from './data';
export const DEFAULT_VOLSEG_SERVER = 'https://molstarvolseg.ncbr.muni.cz/v2';
export class VolumeApiV2 {
public volumeServerUrl: string;
public constructor(volumeServerUrl: string = DEFAULT_VOLSEG_SERVER) {
this.volumeServerUrl = volumeServerUrl.replace(/\/$/, ''); // trim trailing slash
}
public entryListUrl(maxEntries: number, keyword?: string): string {
return `${this.volumeServerUrl}/list_entries/${maxEntries}/${keyword ?? ''}`;
}
public metadataUrl(source: string, entryId: string): string {
return `${this.volumeServerUrl}/${source}/${entryId}/metadata`;
}
public volumeUrl(source: string, entryId: string, box: [[number, number, number], [number, number, number]] | null, maxPoints: number): string {
if (box) {
const [[a1, a2, a3], [b1, b2, b3]] = box;
return `${this.volumeServerUrl}/${source}/${entryId}/volume/box/${a1}/${a2}/${a3}/${b1}/${b2}/${b3}?max_points=${maxPoints}`;
} else {
return `${this.volumeServerUrl}/${source}/${entryId}/volume/cell?max_points=${maxPoints}`;
}
}
public latticeUrl(source: string, entryId: string, segmentation: number, box: [[number, number, number], [number, number, number]] | null, maxPoints: number): string {
if (box) {
const [[a1, a2, a3], [b1, b2, b3]] = box;
return `${this.volumeServerUrl}/${source}/${entryId}/segmentation/box/${segmentation}/${a1}/${a2}/${a3}/${b1}/${b2}/${b3}?max_points=${maxPoints}`;
} else {
return `${this.volumeServerUrl}/${source}/${entryId}/segmentation/cell/${segmentation}?max_points=${maxPoints}`;
}
}
public meshUrl_Json(source: string, entryId: string, segment: number, detailLevel: number): string {
return `${this.volumeServerUrl}/${source}/${entryId}/mesh/${segment}/${detailLevel}`;
}
public meshUrl_Bcif(source: string, entryId: string, segment: number, detailLevel: number): string {
return `${this.volumeServerUrl}/${source}/${entryId}/mesh_bcif/${segment}/${detailLevel}`;
}
public volumeInfoUrl(source: string, entryId: string): string {
return `${this.volumeServerUrl}/${source}/${entryId}/volume_info`;
}
public async getEntryList(maxEntries: number, keyword?: string): Promise<{ [source: string]: string[] }> {
const response = await fetch(this.entryListUrl(maxEntries, keyword));
return await response.json();
}
public async getMetadata(source: string, entryId: string): Promise<Metadata> {
const url = this.metadataUrl(source, entryId);
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch metadata from ${url}`);
return await response.json();
}
}

View File

@@ -1,83 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
export interface Metadata {
grid: {
general: {
details: string,
source_db_name: string,
source_db_id: string,
},
volumes: Volumes,
segmentation_lattices: SegmentationLattices,
segmentation_meshes: SegmentationMeshes,
},
annotation: Annotation | null,
}
export interface Volumes {
volume_downsamplings: number[],
voxel_size: { [downsampling: number]: Vector3 },
origin: Vector3,
grid_dimensions: Vector3,
sampled_grid_dimensions: { [downsampling: number]: Vector3 },
mean: { [downsampling: number]: number },
std: { [downsampling: number]: number },
min: { [downsampling: number]: number },
max: { [downsampling: number]: number },
volume_force_dtype: string,
}
export interface SegmentationLattices {
segmentation_lattice_ids: number[],
segmentation_downsamplings: { [lattice: number]: number[] },
}
export interface SegmentationMeshes {
mesh_component_numbers: {
segment_ids?: {
[segId: number]: {
detail_lvls: {
[detail: number]: {
mesh_ids: {
[meshId: number]: {
num_triangles: number,
num_vertices: number
}
}
}
}
}
}
}
detail_lvl_to_fraction: {
[lvl: number]: number
}
}
export interface Annotation {
name: string,
details: string,
segment_list: Segment[],
}
export interface Segment {
id: number,
colour: number[],
biological_annotation: BiologicalAnnotation,
}
export interface BiologicalAnnotation {
name: string,
external_references: ExternalReference[]
}
export interface ExternalReference {
id: number, resource: string, accession: string, label: string,
description: string
}
type Vector3 = [number, number, number];

View File

@@ -1,74 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Color } from '../../../mol-util/color';
import { Metadata, Segment } from './data';
export class MetadataWrapper {
raw: Metadata;
private segmentMap?: { [id: number]: Segment };
constructor(rawMetadata: Metadata) {
this.raw = rawMetadata;
}
get allSegments() {
return this.raw.annotation?.segment_list ?? [];
}
get allSegmentIds() {
return this.allSegments.map(segment => segment.id);
}
getSegment(segmentId: number): Segment | undefined {
if (!this.segmentMap) {
this.segmentMap = {};
for (const segment of this.allSegments) {
this.segmentMap[segment.id] = segment;
}
}
return this.segmentMap[segmentId];
}
getSegmentColor(segmentId: number): Color | undefined {
const colorArray = this.getSegment(segmentId)?.colour;
return colorArray ? Color.fromNormalizedArray(colorArray, 0) : undefined;
}
/** Get the list of detail levels available for the given mesh segment. */
getMeshDetailLevels(segmentId: number): number[] {
const segmentIds = this.raw.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
if (!segmentIds) return [];
const details = segmentIds[segmentId].detail_lvls;
return Object.keys(details).map(s => parseInt(s));
}
/** Get the worst available detail level that is not worse than preferredDetail.
* If preferredDetail is null, get the worst detail level overall.
* (worse = greater number) */
getSufficientMeshDetail(segmentId: number, preferredDetail: number | null) {
let availDetails = this.getMeshDetailLevels(segmentId);
if (preferredDetail !== null) {
availDetails = availDetails.filter(det => det <= preferredDetail);
}
return Math.max(...availDetails);
}
/** IDs of all segments available as meshes */
get meshSegmentIds() {
const segmentIds = this.raw.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
if (!segmentIds) return [];
return Object.keys(segmentIds).map(s => parseInt(s));
}
get gridTotalVolume() {
const [vx, vy, vz] = this.raw.grid.volumes.voxel_size[1];
const [gx, gy, gz] = this.raw.grid.volumes.grid_dimensions;
return vx * vy * vz * gx * gy * gz;
}
}

View File

@@ -1,47 +0,0 @@
# Chemical Component Dictionary Extension
The [Chemical Component Dictionary (CCD)](https://www.wwpdb.org/data/ccd) describes all small molecules and monomers found in PDB entries. The dictionary provides a plethora of additional information not present in wwPDB archive structures such as chemical descriptors (SMILES & InChI) and stereochemical assignments, information on bond order and more. Most notably, the CCD provides 2 sets of coordinates:
- `ideal`: idealized/minimized coordinates, obtained using Molecular Networks' Corina, and if there are issues, OpenEye's OMEGA
- `model`: coordinates extracted from an archive structure
## How to Load a Component from URL
1. "Download Structure" -- switch "Source" to "URL"
2. Enter URL of component, e.g. https://files.rcsb.org/ligands/view/HEM.cif, leave "Format" as is
3. Click "Apply"
This parses the corresponding component into 2 models (1st: `ideal` coordinates, 2nd: `model` coordinates) and applies the default representaiton to the 1st model. `model` coordinates are available as 2nd model. Click the canvas to re-focus if you don't see anything after switching models due to the coordinates being far away.
## How to Visualize Components
There's a dedicated representation preset that faciliates the comparison of `ideal` and `model` coordinates.
1. Load a component as described above
2. Switch structure preset to "Chemical Component" (button in the top-right, in the "Structure" panel)
This creates a dedicated component for `ideal` as well as `model` coordinates and represents them as ball-and-stick. Initially, only `ideal` coordinates are shown. After toggling the visibility of `model` coordinates, they appear superimposed with the `ideal` coordinates.
## Examples & Test Cases
Ligand | Description | Details
-- | -- | --
https://files.rcsb.org/ligands/view/HEM.cif | metal coordination |
https://files.rcsb.org/ligands/view/FE.cif | +3 oxidation state |
https://files.rcsb.org/ligands/view/FE2.cif | +2 oxidation state |
https://files.rcsb.org/ligands/view/RUC.cif | transition metal |
https://files.rcsb.org/ligands/view/SF4.cif | Fe-S cluster | doesn't align nicely
https://files.rcsb.org/ligands/view/TBR.cif | coords identical |
https://files.rcsb.org/ligands/view/OER.cif | coords identical |
https://files.rcsb.org/ligands/view/FEA.cif | charges |
https://files.rcsb.org/ligands/view/PR2.cif | orientation differs |
https://files.rcsb.org/ligands/view/03R.cif | some atoms missing |
https://files.rcsb.org/ligands/view/02U.cif | many atoms missing |
https://files.rcsb.org/ligands/view/HC0.cif | no ideal coords | unrelated: O and H atoms clashing
https://files.rcsb.org/ligands/view/Q6O.cif | no model coords |
https://files.rcsb.org/ligands/view/H0C.cif | big ligand |
https://files.rcsb.org/ligands/view/2NC.cif | dual representation as PRD and CC |
https://files.rcsb.org/birds/view/PRDCC_000001.cif | PRDCC |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/BB87Q.cif | extended CCD identifier |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/7ZTVU.cif | extended CCD identifier |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/9QRZS.cif | extended CCD identifier |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/9ABCD.cif | extended CCD identifier |
https://files.rcsb.org/ligands/view/UNK.cif | CCD special: unknown amino acid | unrelated: some model H are placed far away
https://files.rcsb.org/ligands/view/UNX.cif | CCD special: unknown atom/ion | no ideal coordinates
https://files.rcsb.org/ligands/view/UNL.cif | CCD special: unknown ligand | no coordinates whatsoever

View File

@@ -1,33 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
*/
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
import { ChemicalComponentPreset, ChemicalCompontentTrajectoryHierarchyPreset } from './representation';
export const wwPDBChemicalComponentDictionary = PluginBehavior.create<{ }>({
name: 'wwpdb-chemical-component-dictionary',
category: 'representation',
display: {
name: 'wwPDB Chemical Compontent Dictionary',
description: 'Custom representation for data loaded from the CCD.'
},
ctor: class extends PluginBehavior.Handler<{ }> {
register(): void {
this.ctx.builders.structure.hierarchy.registerPreset(ChemicalCompontentTrajectoryHierarchyPreset);
this.ctx.builders.structure.representation.registerPreset(ChemicalComponentPreset);
}
update() {
return false;
}
unregister() {
this.ctx.builders.structure.hierarchy.unregisterPreset(ChemicalCompontentTrajectoryHierarchyPreset);
this.ctx.builders.structure.representation.unregisterPreset(ChemicalComponentPreset);
}
},
params: () => ({ })
});

View File

@@ -1,165 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
*/
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { StateObjectRef, StateTransform } from '../../../mol-state';
import { StateTransforms } from '../../../mol-plugin-state/transforms';
import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { PluginContext } from '../../../mol-plugin/context';
import { Mat4 } from '../../../mol-math/linear-algebra';
import { Structure } from '../../../mol-model/structure';
import { CCDFormat } from '../../../mol-model-formats/structure/mmcif';
import { MinimizeRmsd } from '../../../mol-math/linear-algebra/3d/minimize-rmsd';
import { SetUtils } from '../../../mol-util/set';
import { TrajectoryHierarchyPresetProvider } from '../../../mol-plugin-state/builder/structure/hierarchy-preset';
import { capitalize } from '../../../mol-util/string';
const CCDParams = (a: PluginStateObject.Molecule.Trajectory | undefined, plugin: PluginContext) => ({
representationPresetParams: PD.Optional(PD.Group(StructureRepresentationPresetProvider.CommonParams)),
showOriginalCoordinates: PD.Optional(PD.Boolean(true, { description: `Show original coordinates for 'model' and 'ideal' structure and do not align them.` })),
shownCoordinateType: PD.Select('ideal', PD.arrayToOptions(['ideal', 'model', 'both'] as const), { description: `What coordinate sets are visible.` }),
...TrajectoryHierarchyPresetProvider.CommonParams(a, plugin)
});
export const ChemicalCompontentTrajectoryHierarchyPreset = TrajectoryHierarchyPresetProvider({
id: 'preset-trajectory-ccd',
display: {
name: 'Chemical Component', group: 'Preset',
description: 'Shows molecules from the Chemical Component Dictionary.'
},
isApplicable: o => {
return CCDFormat.is(o.data.representative.sourceData);
},
params: CCDParams,
async apply(trajectory, params, plugin) {
const tr = StateObjectRef.resolveAndCheck(plugin.state.data, trajectory)?.obj?.data;
if (!tr) return {};
const builder = plugin.builders.structure;
const idealModel = await builder.createModel(trajectory, { modelIndex: 0 });
const idealModelProperties = await builder.insertModelProperties(idealModel, params.modelProperties, { isCollapsed: true });
const idealStructure = await builder.createStructure(idealModelProperties || idealModel, { name: 'model', params: {} });
const idealStructureProperties = await builder.insertStructureProperties(idealStructure, params.structureProperties);
const representationPreset = params.representationPreset || ChemicalComponentPreset.id;
const representationPresetParams = params.representationPresetParams || {};
if (representationPresetParams.ignoreHydrogens === undefined) representationPresetParams.ignoreHydrogens = true;
// degenerate case where either model or ideal coordinates are missing
if (tr.frameCount !== 2) {
const coordinateType = CCDFormat.CoordinateType.get(idealModel.obj!.data);
await builder.representation.applyPreset(idealStructureProperties, representationPreset, { ...representationPresetParams, coordinateType });
return { models: [idealModel], structures: [idealStructure] };
}
const modelModel = await builder.createModel(trajectory, { modelIndex: 1 });
const modelModelProperties = await builder.insertModelProperties(modelModel, params.modelProperties, { isCollapsed: true });
const modelStructure = await builder.createStructure(modelModelProperties || modelModel, { name: 'model', params: {} });
const modelStructureProperties = await builder.insertStructureProperties(modelStructure, params.structureProperties);
// align ideal and model coordinates
if (!params.showOriginalCoordinates) {
const [a, b] = getPositionTables(idealStructure.obj!.data, modelStructure.obj!.data);
if (!a) {
plugin.log.warn(`Cannot align chemical components whose atom sets are disjoint.`);
} else {
const { bTransform, rmsd } = MinimizeRmsd.compute({ a, b });
await transform(plugin, modelStructure.cell!, bTransform);
plugin.log.info(`Superposed [model] and [ideal] with RMSD ${rmsd.toFixed(2)}.`);
}
}
await builder.representation.applyPreset(idealStructureProperties, representationPreset, { ...representationPresetParams, coordinateType: 'ideal', isHidden: params.shownCoordinateType === 'model' });
await builder.representation.applyPreset(modelStructureProperties, representationPreset, { ...representationPresetParams, coordinateType: 'model', isHidden: params.shownCoordinateType === 'ideal' });
return { models: [idealModel, modelModel], structures: [idealStructure, modelStructure] };
}
});
function getPositionTables(s1: Structure, s2: Structure) {
const m1 = getAtomIdSerialMap(s1);
const m2 = getAtomIdSerialMap(s2);
const intersecting = SetUtils.intersection(new Set(m1.keys()), new Set(m2.keys()));
const ret = [
MinimizeRmsd.Positions.empty(intersecting.size),
MinimizeRmsd.Positions.empty(intersecting.size)
];
let o = 0;
intersecting.forEach(k => {
ret[0].x[o] = s1.model.atomicConformation.x[m1.get(k)!];
ret[0].y[o] = s1.model.atomicConformation.y[m1.get(k)!];
ret[0].z[o] = s1.model.atomicConformation.z[m1.get(k)!];
ret[1].x[o] = s2.model.atomicConformation.x[m2.get(k)!];
ret[1].y[o] = s2.model.atomicConformation.y[m2.get(k)!];
ret[1].z[o] = s2.model.atomicConformation.z[m2.get(k)!];
o++;
});
return ret;
}
function getAtomIdSerialMap(structure: Structure) {
const map = new Map<string, number>();
const { label_atom_id } = structure.model.atomicHierarchy.atoms;
for (let i = 0, il = label_atom_id.rowCount; i < il; ++i) {
const id = label_atom_id.value(i);
if (!map.has(id)) map.set(id, map.size);
}
return map;
}
function transform(plugin: PluginContext, s: StateObjectRef<PluginStateObject.Molecule.Structure>, matrix: Mat4) {
const b = plugin.state.data.build().to(s)
.insert(StateTransforms.Model.TransformStructureConformation, { transform: { name: 'matrix', params: { data: matrix, transpose: false } } });
return plugin.runTask(plugin.state.data.updateTree(b));
}
export const ChemicalComponentPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-chemical-component',
display: {
name: 'Chemical Component', group: 'Miscellaneous',
description: `Show 'Ideal' and 'Model' coordinates of chemical components.`
},
isApplicable: o => {
return CCDFormat.is(o.data.model.sourceData);
},
params: () => ({
...StructureRepresentationPresetProvider.CommonParams,
coordinateType: PD.Select<CCDFormat.CoordinateType>('ideal', PD.arrayToOptions(['ideal', 'model'])),
isHidden: PD.Boolean(false)
}),
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
if (!structureCell) return {};
const { coordinateType, isHidden } = params;
const components = {
[coordinateType]: await presetStaticComponent(plugin, structureCell, 'all', { label: capitalize(coordinateType), tags: [coordinateType] })
};
const structure = structureCell.obj!.data;
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
const representations = {
[coordinateType]: builder.buildRepresentation(update, components[coordinateType], { type: 'ball-and-stick', typeParams }, { initialState: { isHidden } }),
};
// sync UI state
if (components[coordinateType]?.cell?.state && isHidden) {
StateTransform.assignState(components[coordinateType]!.cell!.state, { isHidden });
}
await update.commit({ revertOnError: true });
await StructureRepresentationPresetProvider.updateFocusRepr(plugin, structure, params.theme?.focus?.name, params.theme?.focus?.params);
return { components, representations };
}
});

View File

@@ -1,301 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Column } from '../../../mol-data/db';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { Model } from '../../../mol-model/structure';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { StructureComponent } from '../../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
import { setSubtreeVisibility } from '../../../mol-plugin/behavior/static/state';
import { PluginContext } from '../../../mol-plugin/context';
import { MolScriptBuilder } from '../../../mol-script/language/builder';
import { ColorNames } from '../../../mol-util/color/names';
/** Amount by which to expand the camera radius when zooming to atoms involved in struct_conn (angstroms) */
const EXTRA_RADIUS = 4;
/** Tags for state tree nodes managed by this extension */
const TAGS = {
RESIDUE_SEL: 'structconn-focus-residue-sel',
ATOM_SEL: 'structconn-focus-atom-sel',
RESIDUE_REPR: 'structconn-focus-residue-repr',
RESIDUE_NCI_REPR: 'structconn-focus-residue-nci-repr',
ATOM_REPR: 'structconn-focus-atom-repr',
} as const;
type VisualParams = ReturnType<typeof StructureRepresentation3D.createDefaultParams>
/** Parameters for 3D representation of atoms involved in struct_conn (pink bubbles) */
const ATOMS_VISUAL_PARAMS: VisualParams = {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.25, sizeAspectRatio: 0.73, adjustCylinderLength: true, xrayShaded: true, aromaticBonds: false, multipleBonds: 'off', dashCount: 1, dashCap: false } },
colorTheme: { name: 'uniform', params: { value: ColorNames.magenta } },
sizeTheme: { name: 'physical', params: {} },
} as const;
/** Parameters for 3D representation of residues involved in struct_conn (normal ball-and-stick) */
const RESIDUES_VISUAL_PARAMS: VisualParams = {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.16 } },
colorTheme: { name: 'element-symbol', params: {} },
sizeTheme: { name: 'physical', params: {} },
} as const;
/** All public functions provided by the StructConn extension */
export const wwPDBStructConnExtensionFunctions = {
/** Return an object with all struct_conn records for a loaded structure.
* Applies to the first structure belonging to `entry` (e.g. '1tqn'),
* or to the first loaded structure overall if `entry` is `undefined`.
*/
getStructConns(plugin: PluginContext, entry: string | undefined): { [id: string]: StructConnRecord } {
const structNode = selectStructureNode(plugin, entry);
const structure = structNode?.obj?.data;
if (structure) return extractStructConns(structure.model);
else return {};
},
/** Create visuals for residues and atoms involved in a struct_conn with ID `structConnId`
* and zoom on them. If `keepExisting` is false (default), remove any such visuals created by previous calls to this function.
* Also hide all carbohydrate SNFG visuals within the structure (as they would occlude our residues of interest).
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
*/
async inspectStructConn(plugin: PluginContext, entry: string | undefined, structConnId: string, keepExisting: boolean = false): Promise<number> {
const structNode = selectStructureNode(plugin, entry);
const structure = structNode?.obj?.data;
if (!structure) {
console.error('Structure not found');
return 0;
}
const conns: { [id: string]: StructConnRecord } = structure.model._staticPropertyData['wwpdb-struct-conn-extension-data'] ??= extractStructConns(structure.model);
const conn = conns[structConnId];
if (!conn) {
console.error(`The structure does not contain struct_conn "${structConnId}"`);
return 0;
}
if (!keepExisting) {
await removeAllStructConnInspections(plugin, structNode);
}
const nSelectedAtoms = await addStructConnInspection(plugin, structNode, conn);
hideSnfgNodes(plugin, structNode);
return nSelectedAtoms;
},
/** Remove anything created by `inspectStructConn` within the structure and
* make visible any carbohydrate SNFG visuals that have been hidden by `inspectStructConn`.
*/
async clearStructConnInspections(plugin: PluginContext, entry: string | undefined) {
const structNode = selectStructureNode(plugin, entry);
if (!structNode) return;
await removeAllStructConnInspections(plugin, structNode);
unhideSnfgNodes(plugin, structNode);
},
};
type StructNode = Exclude<ReturnType<typeof selectStructureNode>, undefined>
/** Return the first structure node belonging to `entry` (e.g. '1tqn'),
* or to the first loaded structure node overall if `entry` is `undefined`.
* Includes only "root" structures, not structure components. */
function selectStructureNode(plugin: PluginContext, entry: string | undefined) {
const structNodes = plugin.state.data
.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
if (entry) {
const result = structNodes.find(node => node.obj && node.obj.data.model.entry.toLowerCase() === entry.toLowerCase());
if (!result) {
console.warn(`Structure with entry ID "${entry}" was not found. Available structures: ${structNodes.map(node => node.obj?.data.model.entry)}`);
}
return result;
} else {
if (structNodes.length > 1) {
console.warn(`Structure entry ID was not specified, but there is more than one loaded structure (${structNodes.map(node => node.obj?.data.model.entry)}). Taking the first structure.`);
}
if (structNodes.length === 0) {
console.warn(`There are no loaded structures.`);
}
return structNodes[0];
}
}
/** Represents one partner (i.e. atom) of a struct_conn */
interface StructConnPartner {
asymId: string,
seqId: number | undefined,
authSeqId: number | undefined,
insCode: string,
compId: string,
atomId: string,
/** Alternative location (use empty string if not given) */
altId: string,
}
/** Represents a struct_conn (interaction between two partners) */
export interface StructConnRecord {
id: string,
distance: number,
partner1: StructConnPartner,
partner2: StructConnPartner,
}
/** Return an object with all struct_conn records read from mmCIF.
* Return {} if the model comes from another format than mmCIF.
*/
function extractStructConns(model: Model): { [id: string]: StructConnRecord } {
if (!MmcifFormat.is(model.sourceData)) {
console.error('Cannot get struct_conn because source data are not mmCIF.');
return {};
}
const mmcifData = model.sourceData.data;
const {
id,
ptnr1_label_asym_id: asym1,
ptnr1_label_seq_id: seq1,
ptnr1_auth_seq_id: authSeq1,
pdbx_ptnr1_PDB_ins_code: authInsCode1,
ptnr1_label_comp_id: comp1,
ptnr1_label_atom_id: atom1,
pdbx_ptnr1_label_alt_id: alt1,
ptnr2_label_asym_id: asym2,
ptnr2_label_seq_id: seq2,
ptnr2_auth_seq_id: authSeq2,
pdbx_ptnr2_PDB_ins_code: authInsCode2,
ptnr2_label_comp_id: comp2,
ptnr2_label_atom_id: atom2,
pdbx_ptnr2_label_alt_id: alt2,
pdbx_dist_value: distance } = mmcifData.db.struct_conn;
const n = id.rowCount;
const result: { [id: string]: StructConnRecord } = {};
for (let i = 0; i < n; i++) {
const conn: StructConnRecord = {
id: id.value(i),
distance: distance.value(i),
partner1: {
asymId: asym1.value(i),
seqId: seq1.valueKind(i) === Column.ValueKinds.Present ? seq1.value(i) : undefined,
authSeqId: authSeq1.valueKind(i) === Column.ValueKinds.Present ? authSeq1.value(i) : undefined,
insCode: authInsCode1.value(i),
compId: comp1.value(i),
atomId: atom1.value(i),
altId: alt1.value(i),
},
partner2: {
asymId: asym2.value(i),
seqId: seq2.valueKind(i) === Column.ValueKinds.Present ? seq2.value(i) : undefined,
authSeqId: authSeq2.valueKind(i) === Column.ValueKinds.Present ? authSeq2.value(i) : undefined,
insCode: authInsCode2.value(i),
compId: comp2.value(i),
atomId: atom2.value(i),
altId: alt2.value(i),
},
};
result[conn.id] = conn;
}
return result;
}
/** Return MolScript expression for atoms or residues involved in a struct_conn */
function structConnExpression(conn: StructConnRecord, by: 'atoms' | 'residues') {
const { core, struct } = MolScriptBuilder;
const partnerExpressions = [];
for (const partner of [conn.partner1, conn.partner2]) {
const propTests: Parameters<typeof struct.generator.atomGroups>[0] = {
'chain-test': core.rel.eq([struct.atomProperty.macromolecular.label_asym_id(), partner.asymId]),
'group-by': struct.atomProperty.core.operatorName(),
};
if (partner.seqId !== undefined) {
propTests['residue-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_seq_id(), partner.seqId]);
} else if (partner.authSeqId !== undefined) { // for the case of water and carbohydrates (see 5elb, covale3 vs covale5)
propTests['residue-test'] = core.logic.and([
core.rel.eq([struct.atomProperty.macromolecular.auth_seq_id(), partner.authSeqId]),
core.rel.eq([struct.atomProperty.macromolecular.pdbx_PDB_ins_code(), partner.insCode]),
]);
}
if (by === 'residues' && partner.altId !== '') {
propTests['atom-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]);
}
if (by === 'atoms') {
propTests['atom-test'] = core.logic.and([
core.rel.eq([struct.atomProperty.macromolecular.label_atom_id(), partner.atomId]),
core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]),
]);
}
partnerExpressions.push(struct.filter.first([struct.generator.atomGroups(propTests)]));
}
return struct.combinator.merge(partnerExpressions.map(e => struct.modifier.union([e])));
}
/** Create visuals for residues and atoms involved in a struct_conn and zoom on them.
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
*/
async function addStructConnInspection(plugin: PluginContext, structNode: StructNode, conn: StructConnRecord): Promise<number> {
const expressionByResidues = structConnExpression(conn, 'residues');
const expressionByAtoms = structConnExpression(conn, 'atoms');
const update = plugin.build();
update.to(structNode).apply(
StructureComponent,
{ label: `${conn.id} (residues)`, type: { name: 'expression', params: expressionByResidues } },
{ tags: [TAGS.RESIDUE_SEL] }
).apply(
StructureRepresentation3D,
RESIDUES_VISUAL_PARAMS,
{ tags: [TAGS.RESIDUE_REPR] }
);
const atomsSelection = update.to(structNode).apply(
StructureComponent,
{ label: `${conn.id} (atoms)`, type: { name: 'expression', params: expressionByAtoms } },
{ tags: [TAGS.ATOM_SEL] }
);
const atomsVisual = update.to(atomsSelection.ref).apply(
StructureRepresentation3D,
ATOMS_VISUAL_PARAMS,
{ tags: [TAGS.ATOM_REPR] }
);
await update.commit();
plugin.managers.camera.focusRenderObjects(atomsVisual.selector.data?.repr.renderObjects, { extraRadius: EXTRA_RADIUS });
const nSelectedAtoms = atomsSelection.selector.obj?.data?.elementCount ?? 0;
return nSelectedAtoms;
}
/** Remove anything created by `addStructConnInspection` */
async function removeAllStructConnInspections(plugin: PluginContext, structNode: StructNode) {
const selNodes = [
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.RESIDUE_SEL)),
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.ATOM_SEL)),
];
const update = plugin.build();
for (const node of selNodes) {
update.delete(node);
}
await update.commit();
}
/** Hide all carbohydrate SNFG visuals */
function hideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
for (const node of snfgNodes) {
setSubtreeVisibility(plugin.state.data, node.transform.ref, true); // true means hidden
}
}
/** Make visible all carbohydrate SNFG visuals that have been hidden by `hideSnfgNodes` */
function unhideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
for (const node of snfgNodes) {
try {
setSubtreeVisibility(plugin.state.data, node.transform.ref, false); // false means visible
} catch {
// this is OK, the node has been removed
}
}
}

View File

@@ -202,7 +202,7 @@ export class ZenodoImportUI extends CollapsableControls<{}, State> {
}));
} else if (t.name === 'trajectory') {
const [topologyUrl, topologyFormat, topologyIsBinary] = t.params.topology.split('|');
const [coordinatesUrl, coordinatesFormat] = t.params.coordinates.split('|');
const [coordinatesUrl, coordinatesFormat, coordinatesIsBinary] = t.params.coordinates.split('|');
await this.plugin.runTask(this.plugin.state.data.applyAction(LoadTrajectory, {
source: {
@@ -216,6 +216,7 @@ export class ZenodoImportUI extends CollapsableControls<{}, State> {
coordinates: {
url: coordinatesUrl,
format: coordinatesFormat as any,
isBinary: coordinatesIsBinary === 'true',
},
}
}

View File

@@ -1,37 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { Viewport, cameraProject, cameraUnproject } from '../camera/util';
describe('camera', () => {
it('project/unproject', () => {
const proj = Mat4.perspective(Mat4(), -1, 1, 1, -1, 1, 100);
const invProj = Mat4.invert(Mat4(), proj);
const c = Vec4();
const po = Vec3();
const vp = Viewport.create(0, 0, 100, 100);
const pi = Vec3.create(0, 0, 1);
cameraProject(c, pi, vp, proj);
expect(Vec4.equals(c, Vec4.create(50, 50, 2.020202, -1))).toBe(true);
cameraUnproject(po, c, vp, invProj);
expect(Vec3.equals(po, pi)).toBe(true);
Vec3.set(pi, 0.5, 0.5, 1);
cameraProject(c, pi, vp, proj);
cameraUnproject(po, c, vp, invProj);
expect(Vec3.equals(po, pi)).toBe(true);
Viewport.set(vp, 50, 50, 100, 100);
Vec3.set(pi, 0.5, 0.5, 1);
cameraProject(c, pi, vp, proj);
cameraUnproject(po, c, vp, invProj);
expect(Vec3.equals(po, pi)).toBe(true);
});
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2022 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>
@@ -119,11 +119,11 @@ class Camera implements ICamera {
return Camera.targetDistance(radius, this.state.fov, this.viewport.width, this.viewport.height);
}
getFocus(target: Vec3, radius: number, up?: Vec3, dir?: Vec3, snapshot?: Partial<Camera.Snapshot>): Partial<Camera.Snapshot> {
getFocus(target: Vec3, radius: number, up?: Vec3, dir?: Vec3): Partial<Camera.Snapshot> {
const r = Math.max(radius, 0.01);
const targetDistance = this.getTargetDistance(r);
Vec3.sub(this.deltaDirection, snapshot?.target ?? this.target, snapshot?.position ?? this.position);
Vec3.sub(this.deltaDirection, this.target, this.position);
if (dir) Vec3.matchDirection(this.deltaDirection, dir, this.deltaDirection);
Vec3.setMagnitude(this.deltaDirection, this.deltaDirection, targetDistance);
Vec3.sub(this.newPosition, target, this.deltaDirection);
@@ -137,18 +137,6 @@ class Camera implements ICamera {
return state;
}
getCenter(target: Vec3, radius?: number): Partial<Camera.Snapshot> {
Vec3.sub(this.deltaDirection, this.target, this.position);
Vec3.sub(this.newPosition, target, this.deltaDirection);
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
state.target = Vec3.clone(target);
state.position = Vec3.clone(this.newPosition);
if (radius) state.radius = Math.max(radius, 0.01);
return state;
}
getInvariantFocus(target: Vec3, radius: number, up: Vec3, dir: Vec3): Partial<Camera.Snapshot> {
const r = Math.max(radius, 0.01);
const targetDistance = this.getTargetDistance(r);
@@ -172,10 +160,6 @@ class Camera implements ICamera {
}
}
center(target: Vec3, durationMs?: number) {
this.setState(this.getCenter(target), durationMs);
}
/** Transform point into 2D window coordinates. */
project(out: Vec4, point: Vec3) {
return cameraProject(out, point, this.viewport, this.projectionView);
@@ -194,7 +178,7 @@ class Camera implements ICamera {
getPixelSize(point: Vec3) {
// project -> unproject of `point` does not exactly return the same
// to get a sufficiently accurate measure we unproject the original
// clip position in addition to the one shifted by one pixel
// clip position in addition to the one shifted bey one pixel
this.project(tmpClip, point);
this.unproject(tmpPos1, tmpClip);
tmpClip[0] += 1;
@@ -278,7 +262,6 @@ namespace Camera {
fog: 50,
clipFar: true,
minNear: 5,
minFar: 0,
};
}
@@ -295,7 +278,6 @@ namespace Camera {
fog: number
clipFar: boolean
minNear: number
minFar: number
}
export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
@@ -313,7 +295,6 @@ namespace Camera {
if (typeof source.fog !== 'undefined') out.fog = source.fog;
if (typeof source.clipFar !== 'undefined') out.clipFar = source.clipFar;
if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
if (typeof source.minFar !== 'undefined') out.minFar = source.minFar;
return out;
}
@@ -326,7 +307,6 @@ namespace Camera {
&& a.fog === b.fog
&& a.clipFar === b.clipFar
&& a.minNear === b.minNear
&& a.minFar === b.minFar
&& Vec3.exactEquals(a.position, b.position)
&& Vec3.exactEquals(a.up, b.up)
&& Vec3.exactEquals(a.target, b.target);
@@ -394,14 +374,18 @@ function updatePers(camera: Camera) {
}
function updateClip(camera: Camera) {
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar } = camera.state;
let { radius, radiusMax, mode, fog, clipFar, minNear } = camera.state;
if (radius < 0.01) radius = 0.01;
const normalizedFar = Math.max(clipFar ? radius : radiusMax, minFar);
const normalizedFar = clipFar ? radius : radiusMax;
const cameraDistance = Vec3.distance(camera.position, camera.target);
let near = cameraDistance - radius;
let far = cameraDistance + normalizedFar;
const fogNearFactor = -(50 - fog) / 50;
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
const fogFar = far;
if (mode === 'perspective') {
// set at least to 5 to avoid slow sphere impostor rendering
near = Math.max(Math.min(radiusMax, minNear), near);
@@ -417,12 +401,8 @@ function updateClip(camera: Camera) {
far = near + 0.01;
}
const fogNearFactor = -(50 - fog) / 50;
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
const fogFar = far;
camera.near = near;
camera.far = far;
camera.far = 2 * far; // avoid precision issues distingushing far objects from background
camera.fogNear = fogNear;
camera.fogFar = fogFar;
}

View File

@@ -77,7 +77,7 @@ export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projec
// transform into window coordinates, set fourth component to 1 / clip.w as in gl_FragCoord.w
out[0] = (tmpVec4[0] + 1) * width * 0.5 + x;
out[1] = (tmpVec4[1] + 1) * height * 0.5 + y;
out[1] = (1 - tmpVec4[1]) * height * 0.5 + y; // flip Y
out[2] = (tmpVec4[2] + 1) * 0.5;
out[3] = w === 0 ? 0 : 1 / w;
return out;
@@ -92,7 +92,7 @@ export function cameraUnproject(out: Vec3, point: Vec3 | Vec4, viewport: Viewpor
const { x, y, width, height } = viewport;
const px = point[0] - x;
const py = point[1] - y;
const py = (height - point[1] - 1) - y;
const pz = point[2];
out[0] = (2 * px) / width - 1;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2022 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>
@@ -31,7 +31,7 @@ import { PickData } from './passes/pick';
import { PickHelper } from './passes/pick';
import { ImagePass, ImageProps } from './passes/image';
import { Sphere3D } from '../mol-math/geometry';
import { addConsoleStatsProvider, isDebugMode, isTimingMode, removeConsoleStatsProvider } from '../mol-util/debug';
import { isDebugMode, isTimingMode } from '../mol-util/debug';
import { CameraHelperParams } from './helper/camera-helper';
import { produce } from 'immer';
import { HandleHelperParams } from './helper/handle-helper';
@@ -65,7 +65,7 @@ export const Canvas3DParams = {
cameraClipping: PD.Group({
radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
minNear: PD.Numeric(5, { min: 0.1, max: 100, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
minNear: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
}, { pivot: 'radius' }),
viewport: PD.MappedStatic('canvas', {
canvas: PD.Group({}),
@@ -120,7 +120,6 @@ interface Canvas3DContext {
namespace Canvas3DContext {
export const DefaultAttribs = {
powerPreference: 'high-performance' as WebGLContextAttributes['powerPreference'],
failIfMajorPerformanceCaveat: false,
/** true by default to avoid issues with Safari (Jan 2021) */
antialias: true,
@@ -141,9 +140,8 @@ namespace Canvas3DContext {
if (a.enableWboit && a.enableDpoit) throw new Error('Multiple transparency methods not allowed.');
const { powerPreference, failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
const { failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
const gl = getGLContext(canvas, {
powerPreference,
failIfMajorPerformanceCaveat,
antialias,
preserveDrawingBuffer,
@@ -260,7 +258,6 @@ interface Canvas3D {
notifyDidDraw: boolean,
readonly didDraw: BehaviorSubject<now.Timestamp>
readonly commited: BehaviorSubject<now.Timestamp>
readonly commitQueueSize: BehaviorSubject<number>
readonly reprCount: BehaviorSubject<number>
readonly resized: BehaviorSubject<any>
@@ -307,7 +304,6 @@ namespace Canvas3D {
let startTime = now();
const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
const commited = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
const commitQueueSize = new BehaviorSubject<number>(0);
const { gl, contextRestored } = webgl;
@@ -332,12 +328,12 @@ namespace Canvas3D {
}, { x, y, width, height }, { pixelScale: attribs.pixelScale });
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
const controls = TrackballControls.create(input, camera, scene, p.trackball);
const controls = TrackballControls.create(input, camera, p.trackball);
const renderer = Renderer.create(webgl, p.renderer);
const helper = new Helper(webgl, scene, p);
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, controls, p.interaction);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
@@ -442,7 +438,7 @@ namespace Canvas3D {
cam = stereoCamera;
}
if (isTimingMode) webgl.timer.mark('Canvas3D.render', true);
if (isTimingMode) webgl.timer.mark('Canvas3D.render');
const ctx = { renderer, camera: cam, scene, helper };
if (MultiSamplePass.isEnabled(p.multiSample)) {
const forceOn = !cameraChanged && markingUpdated && !controls.isAnimating;
@@ -595,11 +591,7 @@ namespace Canvas3D {
// snapshot the current bounding sphere of visible objects
Sphere3D.copy(oldBoundingSphereVisible, scene.boundingSphereVisible);
if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) {
commitQueueSize.next(scene.commitQueueSize);
return false;
}
commitQueueSize.next(0);
if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
if (helper.debug.isEnabled) helper.debug.update();
if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
@@ -615,32 +607,20 @@ namespace Canvas3D {
}
function consoleStats() {
const items = scene.renderables.map(r => ({
console.table(scene.renderables.map(r => ({
drawCount: r.values.drawCount.ref.value,
instanceCount: r.values.instanceCount.ref.value,
materialId: r.materialId,
renderItemId: r.id,
}));
console.groupCollapsed(`${items.length} RenderItems`);
if (items.length < 50) {
console.table(items);
} else {
console.log(items);
}
console.log(JSON.stringify(webgl.stats, undefined, 4));
})));
console.log(webgl.stats);
const { texture, attribute, elements } = webgl.resources.getByteCounts();
console.log(JSON.stringify({
console.log({
texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
}, undefined, 4));
console.log(JSON.stringify(webgl.timer.formatedStats(), undefined, 4));
console.groupEnd();
});
}
function add(repr: Representation.Any) {
@@ -746,8 +726,6 @@ namespace Canvas3D {
resized.next(+new Date());
}
addConsoleStatsProvider(consoleStats);
return {
webgl,
@@ -810,7 +788,6 @@ namespace Canvas3D {
set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
didDraw,
commited,
commitQueueSize,
reprCount,
resized,
setProps: (properties, doNotRequestDraw = false) => {
@@ -918,7 +895,6 @@ namespace Canvas3D {
},
dispose: () => {
contextRestoredSub.unsubscribe();
cancelAnimationFrame(animationFrameHandle);
markBuffer = [];
@@ -927,8 +903,6 @@ namespace Canvas3D {
controls.dispose();
renderer.dispose();
interactionHelper.dispose();
removeConsoleStatsProvider(consoleStats);
}
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2022 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>
@@ -10,25 +10,20 @@
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
import { Viewport } from '../camera/util';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Camera } from '../camera';
import { absMax, degToRad } from '../../mol-math/misc';
import { Binding } from '../../mol-util/binding';
import { Scene } from '../../mol-gl/scene';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const Key = Binding.TriggerKey;
export const DefaultTrackballBindings = {
dragRotate: Binding([Trigger(B.Flag.Primary, M.create())], 'Rotate', 'Drag using ${triggers}'),
dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true, control: true }))], 'Rotate around z-axis (roll)', 'Drag using ${triggers}'),
dragPan: Binding([
Trigger(B.Flag.Secondary, M.create()),
Trigger(B.Flag.Primary, M.create({ control: true }))
], 'Pan', 'Drag using ${triggers}'),
dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Rotate around z-axis', 'Drag using ${triggers}'),
dragPan: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Pan', 'Drag using ${triggers}'),
dragZoom: Binding.Empty,
dragFocus: Binding([Trigger(B.Flag.Forth, M.create())], 'Focus', 'Drag using ${triggers}'),
dragFocusZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Focus and zoom', 'Drag using ${triggers}'),
@@ -36,22 +31,6 @@ export const DefaultTrackballBindings = {
scrollZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Zoom', 'Scroll using ${triggers}'),
scrollFocus: Binding([Trigger(B.Flag.Auxilary, M.create({ shift: true }))], 'Clip', 'Scroll using ${triggers}'),
scrollFocusZoom: Binding.Empty,
keyMoveForward: Binding([Key('KeyW')], 'Move forward', 'Press ${triggers}'),
keyMoveBack: Binding([Key('KeyS')], 'Move back', 'Press ${triggers}'),
keyMoveLeft: Binding([Key('KeyA')], 'Move left', 'Press ${triggers}'),
keyMoveRight: Binding([Key('KeyD')], 'Move right', 'Press ${triggers}'),
keyMoveUp: Binding([Key('KeyR')], 'Move up', 'Press ${triggers}'),
keyMoveDown: Binding([Key('KeyF')], 'Move down', 'Press ${triggers}'),
keyRollLeft: Binding([Key('KeyQ')], 'Roll left', 'Press ${triggers}'),
keyRollRight: Binding([Key('KeyE')], 'Roll right', 'Press ${triggers}'),
keyPitchUp: Binding([Key('ArrowUp', M.create({ shift: true }))], 'Pitch up', 'Press ${triggers}'),
keyPitchDown: Binding([Key('ArrowDown', M.create({ shift: true }))], 'Pitch down', 'Press ${triggers}'),
keyYawLeft: Binding([Key('ArrowLeft', M.create({ shift: true }))], 'Yaw left', 'Press ${triggers}'),
keyYawRight: Binding([Key('ArrowRight', M.create({ shift: true }))], 'Yaw right', 'Press ${triggers}'),
boostMove: Binding([Key('ShiftLeft')], 'Boost move', 'Press ${triggers}'),
enablePointerLock: Binding([Key('Space', M.create({ control: true }))], 'Enable pointer lock', 'Press ${triggers}'),
};
export const TrackballControlsParams = {
@@ -60,9 +39,6 @@ export const TrackballControlsParams = {
rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
moveSpeed: PD.Numeric(0.75, { min: 0.1, max: 3, step: 0.1 }),
boostMoveFactor: PD.Numeric(5.0, { min: 0.1, max: 10, step: 0.1 }),
flyMode: PD.Boolean(false),
animate: PD.MappedStatic('off', {
off: PD.EmptyGroup(),
@@ -106,7 +82,6 @@ export { TrackballControls };
interface TrackballControls {
readonly viewport: Viewport
readonly isAnimating: boolean
readonly isMoving: boolean
readonly props: Readonly<TrackballControlsProps>
setProps: (props: Partial<TrackballControlsProps>) => void
@@ -117,14 +92,8 @@ interface TrackballControls {
dispose: () => void
}
namespace TrackballControls {
export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}): TrackballControls {
const p: TrackballControlsProps = {
...PD.getDefaultValues(TrackballControlsParams),
...props,
// include default bindings for backwards state compatibility
bindings: { ...DefaultTrackballBindings, ...props.bindings }
};
const b = p.bindings;
export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props };
const viewport = Viewport.clone(camera.viewport);
@@ -135,11 +104,6 @@ namespace TrackballControls {
const wheelSub = input.wheel.subscribe(onWheel);
const pinchSub = input.pinch.subscribe(onPinch);
const gestureSub = input.gesture.subscribe(onGesture);
const keyDownSub = input.keyDown.subscribe(onKeyDown);
const keyUpSub = input.keyUp.subscribe(onKeyUp);
const moveSub = input.move.subscribe(onMove);
const lockSub = input.lock.subscribe(onLock);
const leaveSub = input.leave.subscribe(onLeave);
let _isInteracting = false;
@@ -153,12 +117,9 @@ namespace TrackballControls {
const _rotLastAxis = Vec3();
let _rotLastAngle = 0;
const _rollPrev = Vec2();
const _rollCurr = Vec2();
let _rollLastAngle = 0;
let _pitchLastAngle = 0;
let _yawLastAngle = 0;
const _zRotPrev = Vec2();
const _zRotCurr = Vec2();
let _zRotLastAngle = 0;
const _zoomStart = Vec2();
const _zoomEnd = Vec2();
@@ -188,7 +149,7 @@ namespace TrackballControls {
return Vec2.set(
mouseOnCircleVec2,
(pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
(viewport.height + 2 * (viewport.y - pageY)) / viewport.width // viewport.width intentional
(viewport.height + 2 * (viewport.y - pageY)) / viewport.width // screen.width intentional
);
}
@@ -242,74 +203,26 @@ namespace TrackballControls {
Vec2.copy(_rotPrev, _rotCurr);
}
const rollQuat = Quat();
const rollDir = Vec3();
const zRotQuat = Quat();
function rollCamera() {
const k = (keyState.rollRight - keyState.rollLeft) / 45;
const dx = (_rollCurr[0] - _rollPrev[0]) * -Math.sign(_rollCurr[1]);
const dy = (_rollCurr[1] - _rollPrev[1]) * -Math.sign(_rollCurr[0]);
const angle = -p.rotateSpeed * (-dx + dy) + k;
function zRotateCamera() {
const dx = _zRotCurr[0] - _zRotPrev[0];
const dy = _zRotCurr[1] - _zRotPrev[1];
const angle = p.rotateSpeed * (-dx + dy) * -0.05;
if (angle) {
Vec3.normalize(rollDir, _eye);
Quat.setAxisAngle(rollQuat, rollDir, angle);
Vec3.transformQuat(camera.up, camera.up, rollQuat);
_rollLastAngle = angle;
} else if (!p.staticMoving && _rollLastAngle) {
_rollLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.normalize(rollDir, _eye);
Quat.setAxisAngle(rollQuat, rollDir, _rollLastAngle);
Vec3.transformQuat(camera.up, camera.up, rollQuat);
Vec3.sub(_eye, camera.position, camera.target);
Quat.setAxisAngle(zRotQuat, _eye, angle);
Vec3.transformQuat(camera.up, camera.up, zRotQuat);
_zRotLastAngle = angle;
} else if (!p.staticMoving && _zRotLastAngle) {
_zRotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.sub(_eye, camera.position, camera.target);
Quat.setAxisAngle(zRotQuat, _eye, _zRotLastAngle);
Vec3.transformQuat(camera.up, camera.up, zRotQuat);
}
Vec2.copy(_rollPrev, _rollCurr);
}
const pitchQuat = Quat();
const pitchDir = Vec3();
function pitchCamera() {
const m = (keyState.pitchUp - keyState.pitchDown) / (p.flyMode ? 360 : 90);
const angle = -p.rotateSpeed * m;
if (angle) {
Vec3.cross(pitchDir, _eye, camera.up);
Vec3.normalize(pitchDir, pitchDir);
Quat.setAxisAngle(pitchQuat, pitchDir, angle);
Vec3.transformQuat(_eye, _eye, pitchQuat);
Vec3.transformQuat(camera.up, camera.up, pitchQuat);
_pitchLastAngle = angle;
} else if (!p.staticMoving && _pitchLastAngle) {
_pitchLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.cross(pitchDir, _eye, camera.up);
Vec3.normalize(pitchDir, pitchDir);
Quat.setAxisAngle(pitchQuat, pitchDir, _pitchLastAngle);
Vec3.transformQuat(_eye, _eye, pitchQuat);
Vec3.transformQuat(camera.up, camera.up, pitchQuat);
}
}
const yawQuat = Quat();
const yawDir = Vec3();
function yawCamera() {
const m = (keyState.yawRight - keyState.yawLeft) / (p.flyMode ? 360 : 90);
const angle = -p.rotateSpeed * m;
if (angle) {
Vec3.normalize(yawDir, camera.up);
Quat.setAxisAngle(yawQuat, yawDir, angle);
Vec3.transformQuat(_eye, _eye, yawQuat);
Vec3.transformQuat(camera.up, camera.up, yawQuat);
_yawLastAngle = angle;
} else if (!p.staticMoving && _yawLastAngle) {
_yawLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.normalize(yawDir, camera.up);
Quat.setAxisAngle(yawQuat, yawDir, _yawLastAngle);
Vec3.transformQuat(_eye, _eye, yawQuat);
Vec3.transformQuat(camera.up, camera.up, yawQuat);
}
Vec2.copy(_zRotPrev, _zRotCurr);
}
function zoomCamera() {
@@ -370,92 +283,6 @@ namespace TrackballControls {
}
}
const keyState = {
moveUp: 0, moveDown: 0, moveLeft: 0, moveRight: 0, moveForward: 0, moveBack: 0,
pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0,
boostMove: 0,
};
const moveDir = Vec3();
const moveEye = Vec3();
function moveCamera(deltaT: number) {
Vec3.sub(moveEye, camera.position, camera.target);
const minDistance = Math.max(camera.state.minNear, p.minDistance);
Vec3.setMagnitude(moveEye, moveEye, minDistance);
const moveSpeed = deltaT * (60 / 1000) * p.moveSpeed * (keyState.boostMove === 1 ? p.boostMoveFactor : 1);
if (keyState.moveForward === 1) {
Vec3.normalize(moveDir, moveEye);
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
const dt = Vec3.distance(camera.target, camera.position);
const ds = Vec3.distance(scene.boundingSphereVisible.center, camera.position);
if (p.flyMode || input.pointerLock || (dt < minDistance && ds < camera.state.radiusMax)) {
Vec3.sub(camera.target, camera.position, moveEye);
}
}
if (keyState.moveBack === 1) {
Vec3.normalize(moveDir, moveEye);
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
if (p.flyMode || input.pointerLock) {
Vec3.sub(camera.target, camera.position, moveEye);
}
}
if (keyState.moveLeft === 1) {
Vec3.cross(moveDir, moveEye, camera.up);
Vec3.normalize(moveDir, moveDir);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (keyState.moveRight === 1) {
Vec3.cross(moveDir, moveEye, camera.up);
Vec3.normalize(moveDir, moveDir);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (keyState.moveUp === 1) {
Vec3.normalize(moveDir, camera.up);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (keyState.moveDown === 1) {
Vec3.normalize(moveDir, camera.up);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (p.flyMode || input.pointerLock) {
const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
}
}
/**
* Ensure the distance between object and target is within the min/max distance
* and not too large compared to `camera.state.radiusMax`
@@ -492,19 +319,15 @@ namespace TrackballControls {
/** Update the object's position, direction and up vectors */
function update(t: number) {
if (lastUpdated === t) return;
const deltaT = t - lastUpdated;
if (lastUpdated > 0) {
if (p.animate.name === 'spin') spin(deltaT);
else if (p.animate.name === 'rock') rock(deltaT);
if (p.animate.name === 'spin') spin(t - lastUpdated);
else if (p.animate.name === 'rock') rock(t - lastUpdated);
}
Vec3.sub(_eye, camera.position, camera.target);
rotateCamera();
rollCamera();
pitchCamera();
yawCamera();
zRotateCamera();
zoomCamera();
focusCamera();
panCamera();
@@ -512,15 +335,6 @@ namespace TrackballControls {
Vec3.add(camera.position, camera.target, _eye);
checkDistances();
if (lastUpdated > 0) {
// clamp the maximum step size at 15 frames to avoid too big jumps
// TODO: make this a parameter?
moveCamera(Math.min(deltaT, 15 * 1000 / 60));
}
Vec3.sub(_eye, camera.position, camera.target);
checkDistances();
if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
Vec3.copy(lastPosition, camera.position);
}
@@ -549,28 +363,24 @@ namespace TrackballControls {
_isInteracting = true;
resetRock(); // start rocking from the center after interactions
const dragRotate = Binding.match(b.dragRotate, buttons, modifiers);
const dragRotateZ = Binding.match(b.dragRotateZ, buttons, modifiers);
const dragPan = Binding.match(b.dragPan, buttons, modifiers);
const dragZoom = Binding.match(b.dragZoom, buttons, modifiers);
const dragFocus = Binding.match(b.dragFocus, buttons, modifiers);
const dragFocusZoom = Binding.match(b.dragFocusZoom, buttons, modifiers);
const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
const dragPan = Binding.match(p.bindings.dragPan, buttons, modifiers);
const dragZoom = Binding.match(p.bindings.dragZoom, buttons, modifiers);
const dragFocus = Binding.match(p.bindings.dragFocus, buttons, modifiers);
const dragFocusZoom = Binding.match(p.bindings.dragFocusZoom, buttons, modifiers);
getMouseOnCircle(pageX, pageY);
getMouseOnScreen(pageX, pageY);
const pr = input.pixelRatio;
const vx = (x * pr - viewport.width / 2 - viewport.x) / viewport.width;
const vy = -(input.height - y * pr - viewport.height / 2 - viewport.y) / viewport.height;
if (isStart) {
if (dragRotate) {
Vec2.copy(_rotCurr, mouseOnCircleVec2);
Vec2.copy(_rotPrev, _rotCurr);
}
if (dragRotateZ) {
Vec2.set(_rollCurr, vx, vy);
Vec2.copy(_rollPrev, _rollCurr);
Vec2.copy(_zRotCurr, mouseOnCircleVec2);
Vec2.copy(_zRotPrev, _zRotCurr);
}
if (dragZoom || dragFocusZoom) {
Vec2.copy(_zoomStart, mouseOnScreenVec2);
@@ -587,7 +397,7 @@ namespace TrackballControls {
}
if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2);
if (dragRotateZ) Vec2.set(_rollCurr, vx, vy);
if (dragRotateZ) Vec2.copy(_zRotCurr, mouseOnCircleVec2);
if (dragZoom || dragFocusZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2);
if (dragFocus) Vec2.copy(_focusEnd, mouseOnScreenVec2);
if (dragFocusZoom) {
@@ -608,16 +418,16 @@ namespace TrackballControls {
if (delta < -p.maxWheelDelta) delta = -p.maxWheelDelta;
else if (delta > p.maxWheelDelta) delta = p.maxWheelDelta;
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
_zoomEnd[1] += delta;
}
if (Binding.match(b.scrollFocus, buttons, modifiers)) {
if (Binding.match(p.bindings.scrollFocus, buttons, modifiers)) {
_focusEnd[1] += delta;
}
}
function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
_isInteracting = true;
_zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
}
@@ -628,177 +438,6 @@ namespace TrackballControls {
_zoomEnd[1] += p.gestureScaleFactor * deltaScale;
}
function onMove({ movementX, movementY }: MoveInput) {
if (!input.pointerLock || movementX === undefined || movementY === undefined) return;
const cx = viewport.width * 0.5 - viewport.x;
const cy = viewport.height * 0.5 - viewport.y;
Vec2.copy(_rotPrev, getMouseOnCircle(cx, cy));
Vec2.copy(_rotCurr, getMouseOnCircle(movementX + cx, movementY + cy));
}
function onKeyDown({ modifiers, code, key, x, y }: KeyInput) {
if (outsideViewport(x, y)) return;
if (Binding.matchKey(b.keyMoveForward, code, modifiers, key)) {
keyState.moveForward = 1;
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers, key)) {
keyState.moveBack = 1;
} else if (Binding.matchKey(b.keyMoveLeft, code, modifiers, key)) {
keyState.moveLeft = 1;
} else if (Binding.matchKey(b.keyMoveRight, code, modifiers, key)) {
keyState.moveRight = 1;
} else if (Binding.matchKey(b.keyMoveUp, code, modifiers, key)) {
keyState.moveUp = 1;
} else if (Binding.matchKey(b.keyMoveDown, code, modifiers, key)) {
keyState.moveDown = 1;
} else if (Binding.matchKey(b.keyRollLeft, code, modifiers, key)) {
keyState.rollLeft = 1;
} else if (Binding.matchKey(b.keyRollRight, code, modifiers, key)) {
keyState.rollRight = 1;
} else if (Binding.matchKey(b.keyPitchUp, code, modifiers, key)) {
keyState.pitchUp = 1;
} else if (Binding.matchKey(b.keyPitchDown, code, modifiers, key)) {
keyState.pitchDown = 1;
} else if (Binding.matchKey(b.keyYawLeft, code, modifiers, key)) {
keyState.yawLeft = 1;
} else if (Binding.matchKey(b.keyYawRight, code, modifiers, key)) {
keyState.yawRight = 1;
}
if (Binding.matchKey(b.boostMove, code, modifiers, key)) {
keyState.boostMove = 1;
}
if (Binding.matchKey(b.enablePointerLock, code, modifiers, key)) {
input.requestPointerLock(viewport);
}
}
function onKeyUp({ modifiers, code, key, x, y }: KeyInput) {
if (outsideViewport(x, y)) return;
let isModifierCode = false;
if (code.startsWith('Alt')) {
isModifierCode = true;
modifiers.alt = true;
} else if (code.startsWith('Shift')) {
isModifierCode = true;
modifiers.shift = true;
} else if (code.startsWith('Control')) {
isModifierCode = true;
modifiers.control = true;
} else if (code.startsWith('Meta')) {
isModifierCode = true;
modifiers.meta = true;
}
const codes = [];
if (isModifierCode) {
if (keyState.moveForward) codes.push(b.keyMoveForward.triggers[0]?.code || '');
if (keyState.moveBack) codes.push(b.keyMoveBack.triggers[0]?.code || '');
if (keyState.moveLeft) codes.push(b.keyMoveLeft.triggers[0]?.code || '');
if (keyState.moveRight) codes.push(b.keyMoveRight.triggers[0]?.code || '');
if (keyState.moveUp) codes.push(b.keyMoveUp.triggers[0]?.code || '');
if (keyState.moveDown) codes.push(b.keyMoveDown.triggers[0]?.code || '');
if (keyState.rollLeft) codes.push(b.keyRollLeft.triggers[0]?.code || '');
if (keyState.rollRight) codes.push(b.keyRollRight.triggers[0]?.code || '');
if (keyState.pitchUp) codes.push(b.keyPitchUp.triggers[0]?.code || '');
if (keyState.pitchDown) codes.push(b.keyPitchDown.triggers[0]?.code || '');
if (keyState.yawLeft) codes.push(b.keyYawLeft.triggers[0]?.code || '');
if (keyState.yawRight) codes.push(b.keyYawRight.triggers[0]?.code || '');
} else {
codes.push(code);
}
for (const code of codes) {
if (Binding.matchKey(b.keyMoveForward, code, modifiers, key)) {
keyState.moveForward = 0;
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers, key)) {
keyState.moveBack = 0;
} else if (Binding.matchKey(b.keyMoveLeft, code, modifiers, key)) {
keyState.moveLeft = 0;
} else if (Binding.matchKey(b.keyMoveRight, code, modifiers, key)) {
keyState.moveRight = 0;
} else if (Binding.matchKey(b.keyMoveUp, code, modifiers, key)) {
keyState.moveUp = 0;
} else if (Binding.matchKey(b.keyMoveDown, code, modifiers, key)) {
keyState.moveDown = 0;
} else if (Binding.matchKey(b.keyRollLeft, code, modifiers, key)) {
keyState.rollLeft = 0;
} else if (Binding.matchKey(b.keyRollRight, code, modifiers, key)) {
keyState.rollRight = 0;
} else if (Binding.matchKey(b.keyPitchUp, code, modifiers, key)) {
keyState.pitchUp = 0;
} else if (Binding.matchKey(b.keyPitchDown, code, modifiers, key)) {
keyState.pitchDown = 0;
} else if (Binding.matchKey(b.keyYawLeft, code, modifiers, key)) {
keyState.yawLeft = 0;
} else if (Binding.matchKey(b.keyYawRight, code, modifiers, key)) {
keyState.yawRight = 0;
}
}
if (Binding.matchKey(b.boostMove, code, modifiers, key)) {
keyState.boostMove = 0;
}
}
function initCameraMove() {
Vec3.sub(moveEye, camera.position, camera.target);
const minDistance = Math.max(camera.state.minNear, p.minDistance);
Vec3.setMagnitude(moveEye, moveEye, minDistance);
Vec3.sub(camera.target, camera.position, moveEye);
const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
}
function resetCameraMove() {
const { center, radius } = scene.boundingSphereVisible;
const cameraDistance = Vec3.distance(camera.position, center);
if (cameraDistance > radius) {
const focus = camera.getFocus(center, radius);
camera.setState({ ...focus, minFar: 0 });
} else {
camera.setState({
minFar: 0,
radius: scene.boundingSphereVisible.radius,
});
}
}
function onLock(isLocked: boolean) {
if (isLocked) {
initCameraMove();
} else {
resetCameraMove();
}
}
function unsetKeyState() {
keyState.moveForward = 0;
keyState.moveBack = 0;
keyState.moveLeft = 0;
keyState.moveRight = 0;
keyState.moveUp = 0;
keyState.moveDown = 0;
keyState.rollLeft = 0;
keyState.rollRight = 0;
keyState.pitchUp = 0;
keyState.pitchDown = 0;
keyState.yawLeft = 0;
keyState.yawRight = 0;
keyState.boostMove = 0;
}
function onLeave() {
unsetKeyState();
}
function dispose() {
if (disposed) return;
disposed = true;
@@ -808,11 +447,6 @@ namespace TrackballControls {
pinchSub.unsubscribe();
gestureSub.unsubscribe();
interactionEndSub.unsubscribe();
keyDownSub.unsubscribe();
keyUpSub.unsubscribe();
moveSub.unsubscribe();
lockSub.unsubscribe();
leaveSub.unsubscribe();
}
const _spinSpeed = Vec2.create(0.005, 0);
@@ -855,31 +489,13 @@ namespace TrackballControls {
return {
viewport,
get isAnimating() { return p.animate.name !== 'off'; },
get isMoving() {
return (
keyState.moveForward === 1 || keyState.moveBack === 1 ||
keyState.moveLeft === 1 || keyState.moveRight === 1 ||
keyState.moveUp === 1 || keyState.moveDown === 1 ||
keyState.rollLeft === 1 || keyState.rollRight === 1 ||
keyState.pitchUp === 1 || keyState.pitchDown === 1 ||
keyState.yawLeft === 1 || keyState.yawRight === 1
);
},
get props() { return p as Readonly<TrackballControlsProps>; },
setProps: (props: Partial<TrackballControlsProps>) => {
if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
resetRock(); // start rocking from the center
}
if (props.flyMode !== undefined && props.flyMode !== p.flyMode) {
if (props.flyMode) {
initCameraMove();
} else {
resetCameraMove();
}
}
Object.assign(p, props);
Object.assign(b, props.bindings);
},
start,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -11,8 +11,6 @@ import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
import { PickingId } from '../../mol-geo/geometry/picking';
import { Text } from '../../mol-geo/geometry/text/text';
import { TextBuilder } from '../../mol-geo/geometry/text/text-builder';
import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
@@ -25,36 +23,19 @@ import { Visual } from '../../mol-repr/visual';
import { ColorNames } from '../../mol-util/color/names';
import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { Camera, ICamera } from '../camera';
import { Viewport } from '../camera/util';
// TODO add scale line/grid
const AxesParams = {
alpha: PD.Numeric(0.51, { min: 0, max: 1, step: 0.01 }, { isEssential: true, label: 'Opacity' }),
...Mesh.Params,
alpha: { ...Mesh.Params.alpha, defaultValue: 0.51 },
ignoreLight: { ...Mesh.Params.ignoreLight, defaultValue: true },
colorX: PD.Color(ColorNames.red, { isEssential: true }),
colorY: PD.Color(ColorNames.green, { isEssential: true }),
colorZ: PD.Color(ColorNames.blue, { isEssential: true }),
scale: PD.Numeric(0.33, { min: 0.1, max: 2, step: 0.1 }, { isEssential: true }),
location: PD.Select('bottom-left', PD.arrayToOptions(['bottom-left', 'bottom-right', 'top-left', 'top-right'] as const)),
locationOffsetX: PD.Numeric(0),
locationOffsetY: PD.Numeric(0),
originColor: PD.Color(ColorNames.grey),
radiusScale: PD.Numeric(0.075, { min: 0.01, max: 0.3, step: 0.001 }),
showPlanes: PD.Boolean(true),
planeColorXY: PD.Color(ColorNames.grey, { label: 'Plane Color XY' }),
planeColorXZ: PD.Color(ColorNames.grey, { label: 'Plane Color XZ' }),
planeColorYZ: PD.Color(ColorNames.grey, { label: 'Plane Color YZ' }),
showLabels: PD.Boolean(false),
labelX: PD.Text('X'),
labelY: PD.Text('Y'),
labelZ: PD.Text('Z'),
labelColorX: PD.Color(ColorNames.grey),
labelColorY: PD.Color(ColorNames.grey),
labelColorZ: PD.Color(ColorNames.grey),
labelOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
labelScale: PD.Numeric(0.25, { min: 0.1, max: 1.0, step: 0.01 }),
};
type AxesParams = typeof AxesParams
type AxesProps = PD.Values<AxesParams>
@@ -75,9 +56,7 @@ export class CameraHelper {
axes: { name: 'off', params: {} }
};
private meshRenderObject: GraphicsRenderObject | undefined;
private textRenderObject: GraphicsRenderObject | undefined;
private pixelRatio = 1;
private renderObject: GraphicsRenderObject | undefined;
constructor(private webgl: WebGLContext, props: Partial<CameraHelperProps> = {}) {
this.scene = Scene.create(webgl, GraphicsRenderVariantsBlended);
@@ -95,20 +74,9 @@ export class CameraHelper {
p.axes.name = props.axes.name;
if (props.axes.name === 'on') {
this.scene.clear();
this.pixelRatio = this.webgl.pixelRatio;
const params = {
...props.axes.params,
scale: props.axes.params.scale * this.pixelRatio,
labelScale: props.axes.params.labelScale * this.pixelRatio,
};
this.meshRenderObject = createMeshRenderObject(params);
this.scene.add(this.meshRenderObject);
if (props.axes.params.showLabels) {
this.textRenderObject = createTextRenderObject(params);
this.scene.add(this.textRenderObject);
} else {
this.textRenderObject = undefined;
}
const params = { ...props.axes.params, scale: props.axes.params.scale * this.webgl.pixelRatio };
this.renderObject = createAxesRenderObject(params);
this.scene.add(this.renderObject);
this.scene.commit();
Vec3.set(this.camera.position, 0, 0, params.scale * 200);
@@ -126,29 +94,19 @@ export class CameraHelper {
getLoci(pickingId: PickingId) {
const { objectId, groupId, instanceId } = pickingId;
if ((
(!this.meshRenderObject || objectId !== this.meshRenderObject.id) &&
(!this.textRenderObject || objectId !== this.textRenderObject.id)
) || groupId === CameraHelperAxis.None) return EmptyLoci;
if (!this.renderObject || objectId !== this.renderObject.id || groupId === CameraHelperAxis.None) return EmptyLoci;
return CameraAxesLoci(this, groupId, instanceId);
}
private eachGroup = (loci: Loci, apply: (interval: Interval) => boolean): boolean => {
if (!this.renderObject) return false;
if (!isCameraAxesLoci(loci)) return false;
let changed = false;
if (this.meshRenderObject) {
const groupCount = this.meshRenderObject.values.uGroupCount.ref.value;
for (const { groupId, instanceId } of loci.elements) {
const idx = instanceId * groupCount + groupId;
if (apply(Interval.ofSingleton(idx))) changed = true;
}
}
if (this.textRenderObject) {
const groupCount = this.textRenderObject.values.uGroupCount.ref.value;
for (const { groupId, instanceId } of loci.elements) {
const idx = instanceId * groupCount + groupId;
if (apply(Interval.ofSingleton(idx))) changed = true;
}
const groupCount = this.renderObject.values.uGroupCount.ref.value;
const { elements } = loci;
for (const { groupId, instanceId } of elements) {
const idx = instanceId * groupCount + groupId;
if (apply(Interval.ofSingleton(idx))) changed = true;
}
return changed;
};
@@ -159,82 +117,42 @@ export class CameraHelper {
if (!isCameraAxesLoci(loci)) return false;
if (loci.data !== this) return false;
}
return (
Visual.mark(this.meshRenderObject, loci, action, this.eachGroup) ||
Visual.mark(this.textRenderObject, loci, action, this.eachGroup)
);
return Visual.mark(this.renderObject, loci, action, this.eachGroup);
}
update(camera: ICamera) {
if (!this.meshRenderObject || this.props.axes.name === 'off') return;
if (this.pixelRatio !== this.webgl.pixelRatio) {
this.setProps(this.props);
}
if (!this.renderObject) return;
updateCamera(this.camera, camera.viewport, camera.viewOffset);
Mat4.extractRotation(this.scene.view, camera.view);
const r = this.textRenderObject
? this.textRenderObject.values.boundingSphere.ref.value.radius
: this.meshRenderObject.values.boundingSphere.ref.value.radius;
const l = this.props.axes.params.location;
const ox = this.props.axes.params.locationOffsetX * this.pixelRatio;
const oy = this.props.axes.params.locationOffsetY * this.pixelRatio;
if (l === 'bottom-left') {
Mat4.setTranslation(this.scene.view, Vec3.create(
-camera.viewport.width / 2 + r + ox,
-camera.viewport.height / 2 + r + oy,
0
));
} else if (l === 'bottom-right') {
Mat4.setTranslation(this.scene.view, Vec3.create(
camera.viewport.width / 2 - r - ox,
-camera.viewport.height / 2 + r + oy,
0
));
} else if (l === 'top-left') {
Mat4.setTranslation(this.scene.view, Vec3.create(
-camera.viewport.width / 2 + r + ox,
camera.viewport.height / 2 - r - oy,
0
));
} else if (l === 'top-right') {
Mat4.setTranslation(this.scene.view, Vec3.create(
camera.viewport.width / 2 - r - ox,
camera.viewport.height / 2 - r - oy,
0
));
} else {
assertUnreachable(l);
}
const r = this.renderObject.values.boundingSphere.ref.value.radius;
Mat4.setTranslation(this.scene.view, Vec3.create(
-camera.viewport.width / 2 + r,
-camera.viewport.height / 2 + r,
0
));
}
}
export enum CameraHelperAxis {
export const enum CameraHelperAxis {
None = 0,
X,
Y,
Z,
XY,
XZ,
YZ,
Origin
YZ
}
function getAxisLabel(axis: number, cameraHelper: CameraHelper) {
const a = cameraHelper.props.axes;
const x = a.name === 'on' ? a.params.labelX : 'X';
const y = a.name === 'on' ? a.params.labelY : 'Y';
const z = a.name === 'on' ? a.params.labelZ : 'Z';
function getAxisLabel(axis: number) {
switch (axis) {
case CameraHelperAxis.X: return `${x} Axis`;
case CameraHelperAxis.Y: return `${y} Axis`;
case CameraHelperAxis.Z: return `${z} Axis`;
case CameraHelperAxis.XY: return `${x}${y} Plane`;
case CameraHelperAxis.XZ: return `${x}${z} Plane`;
case CameraHelperAxis.YZ: return `${y}${z} Plane`;
case CameraHelperAxis.Origin: return 'Origin';
case CameraHelperAxis.X: return 'X Axis';
case CameraHelperAxis.Y: return 'Y Axis';
case CameraHelperAxis.Z: return 'Z Axis';
case CameraHelperAxis.XY: return 'XY Plane';
case CameraHelperAxis.XZ: return 'XZ Plane';
case CameraHelperAxis.YZ: return 'YZ Plane';
default: return 'Axes';
}
}
@@ -242,7 +160,7 @@ function getAxisLabel(axis: number, cameraHelper: CameraHelper) {
function CameraAxesLoci(cameraHelper: CameraHelper, groupId: number, instanceId: number) {
return DataLoci('camera-axes', cameraHelper, [{ groupId, instanceId }],
void 0 /** bounding sphere */,
() => getAxisLabel(groupId, cameraHelper));
() => getAxisLabel(groupId));
}
export type CameraAxesLoci = ReturnType<typeof CameraAxesLoci>
export function isCameraAxesLoci(x: Loci): x is CameraAxesLoci {
@@ -279,17 +197,15 @@ function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.Vie
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
}
function createAxesMesh(props: AxesProps, mesh?: Mesh) {
function createAxesMesh(scale: number, mesh?: Mesh) {
const state = MeshBuilder.createState(512, 256, mesh);
const scale = 100 * props.scale;
const radius = props.radiusScale * scale;
const textScale = props.showLabels ? 100 * props.labelScale / 3 : 0;
const x = Vec3.scale(Vec3(), Vec3.unitX, scale - textScale);
const y = Vec3.scale(Vec3(), Vec3.unitY, scale - textScale);
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale - textScale);
const radius = 0.075 * scale;
const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
state.currentGroup = CameraHelperAxis.Origin;
state.currentGroup = CameraHelperAxis.None;
addSphere(state, Vec3.origin, radius, 2);
state.currentGroup = CameraHelperAxis.X;
@@ -304,102 +220,50 @@ function createAxesMesh(props: AxesProps, mesh?: Mesh) {
addSphere(state, z, radius, 2);
addCylinder(state, Vec3.origin, z, 1, cylinderProps);
if (props.showPlanes) {
Vec3.scale(x, x, 0.5);
Vec3.scale(y, y, 0.5);
Vec3.scale(z, z, 0.5);
Vec3.scale(x, x, 0.5);
Vec3.scale(y, y, 0.5);
Vec3.scale(z, z, 0.5);
state.currentGroup = CameraHelperAxis.XY;
MeshBuilder.addTriangle(state, Vec3.origin, x, y);
MeshBuilder.addTriangle(state, Vec3.origin, y, x);
const xy = Vec3.add(Vec3(), x, y);
MeshBuilder.addTriangle(state, xy, x, y);
MeshBuilder.addTriangle(state, xy, y, x);
state.currentGroup = CameraHelperAxis.XY;
MeshBuilder.addTriangle(state, Vec3.origin, x, y);
MeshBuilder.addTriangle(state, Vec3.origin, y, x);
const xy = Vec3.add(Vec3(), x, y);
MeshBuilder.addTriangle(state, xy, x, y);
MeshBuilder.addTriangle(state, xy, y, x);
state.currentGroup = CameraHelperAxis.XZ;
MeshBuilder.addTriangle(state, Vec3.origin, x, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, x);
const xz = Vec3.add(Vec3(), x, z);
MeshBuilder.addTriangle(state, xz, x, z);
MeshBuilder.addTriangle(state, xz, z, x);
state.currentGroup = CameraHelperAxis.XZ;
MeshBuilder.addTriangle(state, Vec3.origin, x, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, x);
const xz = Vec3.add(Vec3(), x, z);
MeshBuilder.addTriangle(state, xz, x, z);
MeshBuilder.addTriangle(state, xz, z, x);
state.currentGroup = CameraHelperAxis.YZ;
MeshBuilder.addTriangle(state, Vec3.origin, y, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, y);
const yz = Vec3.add(Vec3(), y, z);
MeshBuilder.addTriangle(state, yz, y, z);
MeshBuilder.addTriangle(state, yz, z, y);
}
state.currentGroup = CameraHelperAxis.YZ;
MeshBuilder.addTriangle(state, Vec3.origin, y, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, y);
const yz = Vec3.add(Vec3(), y, z);
MeshBuilder.addTriangle(state, yz, y, z);
MeshBuilder.addTriangle(state, yz, z, y);
return MeshBuilder.getMesh(state);
}
function getAxesMeshShape(props: AxesProps, shape?: Shape<Mesh>) {
function getAxesShape(props: AxesProps, shape?: Shape<Mesh>) {
const scale = 100 * props.scale;
const mesh = createAxesMesh(props, shape?.geometry);
const mesh = createAxesMesh(scale, shape?.geometry);
mesh.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale + scale / 4));
const getColor = (groupId: number) => {
switch (groupId) {
case CameraHelperAxis.X: return props.colorX;
case CameraHelperAxis.Y: return props.colorY;
case CameraHelperAxis.Z: return props.colorZ;
case CameraHelperAxis.XY: return props.planeColorXY;
case CameraHelperAxis.XZ: return props.planeColorXZ;
case CameraHelperAxis.YZ: return props.planeColorYZ;
case CameraHelperAxis.Origin: return props.originColor;
case 1: return props.colorX;
case 2: return props.colorY;
case 3: return props.colorZ;
default: return ColorNames.grey;
}
};
return Shape.create('axes-mesh', {}, mesh, getColor, () => 1, () => '');
return Shape.create('axes', {}, mesh, getColor, () => 1, () => '');
}
function createMeshRenderObject(props: AxesProps) {
const shape = getAxesMeshShape(props);
return Shape.createRenderObject(shape, {
...PD.getDefaultValues(Mesh.Params),
...props,
ignoreLight: true,
});
}
//
function createAxesText(props: AxesProps, text?: Text) {
const builder = TextBuilder.create(props, 8, 8, text);
const scale = 100 * props.scale;
const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
const textScale = 100 * props.labelScale;
builder.add(props.labelX, x[0], x[1], x[2], 0.0, textScale, CameraHelperAxis.X);
builder.add(props.labelY, y[0], y[1], y[2], 0.0, textScale, CameraHelperAxis.Y);
builder.add(props.labelZ, z[0], z[1], z[2], 0.0, textScale, CameraHelperAxis.Z);
return builder.getText();
}
function getAxesTextShape(props: AxesProps, shape?: Shape<Text>) {
const scale = 100 * props.scale;
const text = createAxesText(props, shape?.geometry);
text.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale));
const getColor = (groupId: number) => {
switch (groupId) {
case CameraHelperAxis.X: return props.labelColorX;
case CameraHelperAxis.Y: return props.labelColorY;
case CameraHelperAxis.Z: return props.labelColorZ;
default: return ColorNames.grey;
}
};
return Shape.create('axes-text', {}, text, getColor, () => 1, () => '');
}
function createTextRenderObject(props: AxesProps) {
const shape = getAxesTextShape(props);
return Shape.createRenderObject(shape, {
...PD.getDefaultValues(Text.Params),
...props,
alpha: props.labelOpacity,
});
}
function createAxesRenderObject(props: AxesProps) {
const shape = getAxesShape(props);
return Shape.createRenderObject(shape, props);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -54,7 +54,6 @@ export class HandleHelper {
};
private renderObject: GraphicsRenderObject | undefined;
private pixelRatio = 1;
private _transform = Mat4();
getBoundingSphere(out: Sphere3D, instanceId: number) {
@@ -72,11 +71,7 @@ export class HandleHelper {
p.handle.name = props.handle.name;
if (props.handle.name === 'on') {
this.scene.clear();
this.pixelRatio = this.webgl.pixelRatio;
const params = {
...props.handle.params,
scale: props.handle.params.scale * this.webgl.pixelRatio
};
const params = { ...props.handle.params, scale: props.handle.params.scale * this.webgl.pixelRatio };
this.renderObject = createHandleRenderObject(params);
this.scene.add(this.renderObject);
this.scene.commit();
@@ -96,10 +91,6 @@ export class HandleHelper {
update(camera: Camera, position: Vec3, rotation: Mat3) {
if (!this.renderObject) return;
if (this.pixelRatio !== this.webgl.pixelRatio) {
this.setProps(this.props);
}
Mat4.setTranslation(this.renderObject.values.aTransform.ref.value as unknown as Mat4, position);
Mat4.fromMat3(this.renderObject.values.aTransform.ref.value as unknown as Mat4, rotation);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 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>
@@ -13,14 +13,13 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
import { Camera } from '../camera';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Bond } from '../../mol-model/structure';
import { TrackballControls } from '../controls/trackball';
type Canvas3D = import('../canvas3d').Canvas3D
type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
type DragEvent = import('../canvas3d').Canvas3D.DragEvent
type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
enum InputEvent { Move, Click, Drag }
const enum InputEvent { Move, Click, Drag }
const tmpPosA = Vec3();
const tmpPos = Vec3();
@@ -69,7 +68,7 @@ export class Canvas3dInteractionHelper {
}
private identify(e: InputEvent, t: number) {
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
if (e === InputEvent.Drag) {
if (xyChanged && !this.outsideViewport(this.startX, this.startY)) {
@@ -189,7 +188,7 @@ export class Canvas3dInteractionHelper {
this.ev.dispose();
}
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private controls: TrackballControls, props: Partial<Canvas3dInteractionHelperProps> = {}) {
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, props: Partial<Canvas3dInteractionHelperProps> = {}) {
this.props = { ...PD.getDefaultValues(Canvas3dInteractionHelperParams), ...props };
input.drag.subscribe(({ x, y, buttons, button, modifiers }) => {
@@ -198,12 +197,8 @@ export class Canvas3dInteractionHelper {
this.drag(x, y, buttons, button, modifiers);
});
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement }) => {
input.move.subscribe(({ x, y, inside, buttons, button, modifiers }) => {
if (!inside || this.isInteracting) return;
if (!onElement) {
this.leave();
return;
}
// console.log('move');
this.move(x, y, buttons, button, modifiers);
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -23,7 +23,6 @@ import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
import { Color } from '../../mol-util/color';
import { Asset, AssetManager } from '../../mol-util/assets';
import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
import { isPowerOfTwo } from '../../mol-math/misc';
const SharedParams = {
opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }),
@@ -60,7 +59,6 @@ const ImageParams = {
url: PD.Text(''),
file: PD.File({ accept: 'image/*' }),
}),
blur: PD.Numeric(0, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'Note, this only works in WebGL2 or with power-of-two images and when "EXT_shader_texture_lod" is available.' }),
...SharedParams,
coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
};
@@ -209,7 +207,6 @@ export class BackgroundPass {
}
if (!this.image) return;
ValueCell.updateIfChanged(this.renderable.values.uBlur, props.blur);
ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity);
ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation);
ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness);
@@ -397,9 +394,6 @@ function getImageTexture(ctx: WebGLContext, assetManager: AssetManager, source:
const img = new Image();
img.onload = () => {
texture.load(img);
if (ctx.isWebGL2 || (isPowerOfTwo(img.width) && isPowerOfTwo(img.height))) {
texture.mipmap();
}
onload?.();
};
img.onerror = () => {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -142,7 +142,7 @@ export class DrawPass {
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -150,7 +150,7 @@ export class DrawPass {
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
}
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
@@ -196,7 +196,7 @@ export class DrawPass {
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -204,7 +204,7 @@ export class DrawPass {
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
}
// render transparent primitives and volumes
@@ -260,7 +260,7 @@ export class DrawPass {
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
}
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -268,7 +268,7 @@ export class DrawPass {
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
if (!this.packedDepth) {
this.depthTextureOpaque.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
@@ -312,7 +312,7 @@ export class DrawPass {
const { x, y, width, height } = camera.viewport;
renderer.setViewport(x, y, width, height);
renderer.update(camera, scene);
renderer.update(camera);
if (transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
this.drawTarget.bind();
@@ -360,7 +360,7 @@ export class DrawPass {
}
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.update(helper.camera.camera);
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -27,8 +27,6 @@ export const MarkingParams = {
highlightEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(1.0, 0.4, 0.6), 1.0)),
selectEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(0.2, 1.0, 0.1), 1.0)),
edgeScale: PD.Numeric(1, { min: 1, max: 3, step: 1 }, { description: 'Thickness of the edge.' }),
highlightEdgeStrength: PD.Numeric(1.0, { min: 0, max: 1, step: 0.1 }),
selectEdgeStrength: PD.Numeric(1.0, { min: 0, max: 1, step: 0.1 }),
ghostEdgeStrength: PD.Numeric(0.3, { min: 0, max: 1, step: 0.1 }, { description: 'Opacity of the hidden edges that are covered by other geometry. When set to 1, one less geometry render pass is done.' }),
innerEdgeFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Factor to multiply the inner edge color with - for added contrast.' }),
};
@@ -103,10 +101,10 @@ export class MarkingPass {
}
update(props: MarkingProps) {
const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength, highlightEdgeStrength, selectEdgeStrength } = props;
const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength } = props;
const { values: edgeValues } = this.edge;
const _edgeScale = Math.max(1, Math.round(edgeScale * this.webgl.pixelRatio));
const _edgeScale = Math.round(edgeScale * this.webgl.pixelRatio);
if (edgeValues.dEdgeScale.ref.value !== _edgeScale) {
ValueCell.update(edgeValues.dEdgeScale, _edgeScale);
this.edge.update();
@@ -115,10 +113,8 @@ export class MarkingPass {
const { values: overlayValues } = this.overlay;
ValueCell.update(overlayValues.uHighlightEdgeColor, Color.toVec3Normalized(overlayValues.uHighlightEdgeColor.ref.value, highlightEdgeColor));
ValueCell.update(overlayValues.uSelectEdgeColor, Color.toVec3Normalized(overlayValues.uSelectEdgeColor.ref.value, selectEdgeColor));
ValueCell.updateIfChanged(overlayValues.uInnerEdgeFactor, innerEdgeFactor);
ValueCell.updateIfChanged(overlayValues.uGhostEdgeStrength, ghostEdgeStrength);
ValueCell.updateIfChanged(overlayValues.uHighlightEdgeStrength, highlightEdgeStrength);
ValueCell.updateIfChanged(overlayValues.uSelectEdgeStrength, selectEdgeStrength);
ValueCell.update(overlayValues.uInnerEdgeFactor, innerEdgeFactor);
ValueCell.update(overlayValues.uGhostEdgeStrength, ghostEdgeStrength);
}
render(viewport: Viewport, target: RenderTarget | undefined) {
@@ -174,8 +170,6 @@ const OverlaySchema = {
uTexSizeInv: UniformSpec('v2'),
uHighlightEdgeColor: UniformSpec('v3'),
uSelectEdgeColor: UniformSpec('v3'),
uHighlightEdgeStrength: UniformSpec('f'),
uSelectEdgeStrength: UniformSpec('f'),
uGhostEdgeStrength: UniformSpec('f'),
uInnerEdgeFactor: UniformSpec('f'),
};
@@ -192,8 +186,6 @@ function getOverlayRenderable(ctx: WebGLContext, edgeTexture: Texture): OverlayR
uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
uHighlightEdgeColor: ValueCell.create(Vec3()),
uSelectEdgeColor: ValueCell.create(Vec3()),
uHighlightEdgeStrength: ValueCell.create(1),
uSelectEdgeStrength: ValueCell.create(1),
uGhostEdgeStrength: ValueCell.create(0),
uInnerEdgeFactor: ValueCell.create(0),
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -172,7 +172,7 @@ export class PickPass {
private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: 'pick' | 'depth', pickType: number) {
renderer.clear(false);
renderer.update(camera, scene);
renderer.update(camera);
renderer.renderPick(scene.primitives, camera, variant, null, pickType);
if (helper.handle.isEnabled) {
@@ -181,7 +181,7 @@ export class PickPass {
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.update(helper.camera.camera);
renderer.renderPick(helper.camera.scene, helper.camera.camera, variant, null, pickType);
}
}
@@ -291,7 +291,7 @@ export class PickHelper {
}
private render(camera: Camera | StereoCamera) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', true);
if (isTimingMode) this.webgl.timer.mark('PickHelper.render');
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
const { renderer, scene, helper } = this;
@@ -358,7 +358,7 @@ export class PickHelper {
const z = this.getDepth(xp, yp);
// console.log('z', z);
const position = Vec3.create(x, y, z);
const position = Vec3.create(x, viewport.height - y, z);
if (StereoCamera.is(camera)) {
const halfWidth = Math.floor(viewport.width / 2);
if (x > viewport.x + halfWidth) {

View File

@@ -1,9 +1,8 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
@@ -11,7 +10,7 @@ import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/rende
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { deepEqual, ValueCell } from '../../mol-util';
import { ValueCell } from '../../mol-util';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
@@ -31,8 +30,6 @@ import { SmaaParams, SmaaPass } from './smaa';
import { isTimingMode } from '../../mol-util/debug';
import { BackgroundParams, BackgroundPass } from './background';
import { AssetManager } from '../../mol-util/assets';
import { Light } from '../../mol-gl/renderer';
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
const OutlinesSchema = {
...QuadSchema,
@@ -43,14 +40,12 @@ const OutlinesSchema = {
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
uMaxPossibleViewZDiff: UniformSpec('f'),
};
type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture): OutlinesRenderable {
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
@@ -63,10 +58,8 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uInvProjection: ValueCell.create(Mat4.identity()),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
};
const schema = { ...OutlinesSchema };
@@ -76,69 +69,9 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
return createComputeRenderable(renderItem, values);
}
const ShadowsSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
dSteps: DefineSpec('number'),
uMaxDistance: UniformSpec('f'),
uTolerance: UniformSpec('f'),
uBias: UniformSpec('f'),
uLightDirection: UniformSpec('v3[]'),
uLightColor: UniformSpec('v3[]'),
dLightCount: DefineSpec('number'),
};
type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
const width = depthTexture.getWidth();
const height = depthTexture.getHeight();
const values: Values<typeof ShadowsSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
uTexSize: ValueCell.create(Vec2.create(width, height)),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
dSteps: ValueCell.create(1),
uMaxDistance: ValueCell.create(3.0),
uTolerance: ValueCell.create(1.0),
uBias: ValueCell.create(0.6),
uLightDirection: ValueCell.create([]),
uLightColor: ValueCell.create([]),
dLightCount: ValueCell.create(0),
};
const schema = { ...ShadowsSchema };
const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const SsaoSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uSamples: UniformSpec('v3[]'),
dNSamples: DefineSpec('number'),
@@ -151,23 +84,14 @@ const SsaoSchema = {
uRadius: UniformSpec('f'),
uBias: UniformSpec('f'),
dMultiScale: DefineSpec('boolean'),
dLevels: DefineSpec('number'),
uLevelRadius: UniformSpec('f[]'),
uLevelBias: UniformSpec('f[]'),
uNearThreshold: UniformSpec('f'),
uFarThreshold: UniformSpec('f'),
};
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRenderable {
const values: Values<typeof SsaoSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
tDepthHalf: ValueCell.create(depthHalfTexture),
tDepthQuarter: ValueCell.create(depthQuarterTexture),
uSamples: ValueCell.create(getSamples(32)),
dNSamples: ValueCell.create(32),
@@ -178,15 +102,8 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTe
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
uRadius: ValueCell.create(Math.pow(2, 5)),
uBias: ValueCell.create(0.8),
dMultiScale: ValueCell.create(false),
dLevels: ValueCell.create(3),
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
uNearThreshold: ValueCell.create(10.0),
uFarThreshold: ValueCell.create(1500.0),
uRadius: ValueCell.create(8.0),
uBias: ValueCell.create(0.025),
};
const schema = { ...SsaoSchema };
@@ -207,7 +124,8 @@ const SsaoBlurSchema = {
uBlurDirectionX: UniformSpec('f'),
uBlurDirectionY: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uMaxPossibleViewZDiff: UniformSpec('f'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uBounds: UniformSpec('v4'),
@@ -228,7 +146,8 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
uInvProjection: ValueCell.create(Mat4.identity()),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
uNear: ValueCell.create(0.0),
uFar: ValueCell.create(10000.0),
uBounds: ValueCell.create(Vec4()),
@@ -285,7 +204,6 @@ const PostprocessingSchema = {
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tShadows: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
@@ -296,28 +214,26 @@ const PostprocessingSchema = {
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uOutlineColor: UniformSpec('v3'),
uOcclusionColor: UniformSpec('v3'),
uTransparentBackground: UniformSpec('b'),
uMaxPossibleViewZDiff: UniformSpec('f'),
dOcclusionEnable: DefineSpec('boolean'),
uOcclusionOffset: UniformSpec('v2'),
dShadowEnable: DefineSpec('boolean'),
dOutlineEnable: DefineSpec('boolean'),
dOutlineScale: DefineSpec('number'),
dTransparentOutline: DefineSpec('boolean'),
uOutlineThreshold: UniformSpec('f'),
};
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture): PostprocessingRenderable {
const values: Values<typeof PostprocessingSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
tColor: ValueCell.create(colorTexture),
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
tShadows: ValueCell.create(shadowsTexture),
tOutlines: ValueCell.create(outlinesTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
@@ -328,17 +244,16 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
uFogFar: ValueCell.create(10000),
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
uOcclusionColor: ValueCell.create(Vec3.create(0, 0, 0)),
uTransparentBackground: ValueCell.create(false),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
dOcclusionEnable: ValueCell.create(true),
uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
dShadowEnable: ValueCell.create(false),
dOutlineEnable: ValueCell.create(false),
dOutlineScale: ValueCell.create(1),
dTransparentOutline: ValueCell.create(transparentOutline),
uOutlineThreshold: ValueCell.create(0.33),
};
const schema = { ...PostprocessingSchema };
@@ -352,45 +267,18 @@ export const PostprocessingParams = {
occlusion: PD.MappedStatic('on', {
on: PD.Group({
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
multiScale: PD.MappedStatic('off', {
on: PD.Group({
levels: PD.ObjectList({
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
{ radius: 2, bias: 1 },
{ radius: 5, bias: 1 },
{ radius: 8, bias: 1 },
{ radius: 11, bias: 1 },
] }),
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
}),
off: PD.Group({})
}, { cycle: true }),
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
color: PD.Color(Color(0x000000)),
}),
off: PD.Group({})
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
shadow: PD.MappedStatic('off', {
on: PD.Group({
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
}),
off: PD.Group({})
}, { cycle: true, description: 'Simplistic shadows' }),
outline: PD.MappedStatic('off', {
on: PD.Group({
scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
color: PD.Color(Color(0x000000)),
includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
}),
off: PD.Group({})
}, { cycle: true, description: 'Draw outline around 3D objects' }),
@@ -401,37 +289,15 @@ export const PostprocessingParams = {
}, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
background: PD.Group(BackgroundParams, { isFlat: true }),
};
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
type Levels = {
count: number
radius: number[]
bias: number[]
}
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
const count = props.length;
const { radius, bias } = levels || {
radius: (new Array(count * 3)).fill(0),
bias: (new Array(count * 3)).fill(0),
};
props = props.slice().sort((a, b) => a.radius - b.radius);
for (let i = 0; i < count; ++i) {
const p = props[i];
radius[i] = Math.pow(2, p.radius);
bias[i] = p.bias;
}
return { count, radius, bias };
}
export class PostprocessingPass {
static isEnabled(props: PostprocessingProps) {
return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
return props.occlusion.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
}
static isTransparentOutlineEnabled(props: PostprocessingProps) {
return props.outline.name === 'on' && props.outline.params.includeTransparent;
static isOutlineEnabled(props: PostprocessingProps) {
return props.outline.name === 'on';
}
readonly target: RenderTarget;
@@ -439,9 +305,6 @@ export class PostprocessingPass {
private readonly outlinesTarget: RenderTarget;
private readonly outlinesRenderable: OutlinesRenderable;
private readonly shadowsTarget: RenderTarget;
private readonly shadowsRenderable: ShadowsRenderable;
private readonly ssaoFramebuffer: Framebuffer;
private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
@@ -449,12 +312,6 @@ export class PostprocessingPass {
private readonly downsampledDepthTarget: RenderTarget;
private readonly downsampleDepthRenderable: CopyRenderable;
private readonly depthHalfTarget: RenderTarget;
private readonly depthHalfRenderable: CopyRenderable;
private readonly depthQuarterTarget: RenderTarget;
private readonly depthQuarterRenderable: CopyRenderable;
private readonly ssaoDepthTexture: Texture;
private readonly ssaoDepthBlurProxyTexture: Texture;
@@ -464,17 +321,16 @@ export class PostprocessingPass {
private nSamples: number;
private blurKernelSize: number;
private downsampleFactor: number;
private readonly renderable: PostprocessingRenderable;
private ssaoScale: number;
private calcSsaoScale(resolutionScale: number) {
private calcSsaoScale() {
// downscale ssao for high pixel-ratios
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
}
private levels: { radius: number, bias: number }[];
private readonly bgColor = Vec3();
readonly background: BackgroundPass;
@@ -485,17 +341,14 @@ export class PostprocessingPass {
this.nSamples = 1;
this.blurKernelSize = 1;
this.ssaoScale = this.calcSsaoScale(1);
this.levels = [];
this.downsampleFactor = 1;
this.ssaoScale = this.calcSsaoScale();
// needs to be linear for anti-aliasing pass
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.outlinesTarget = webgl.createRenderTarget(width, height, false);
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
this.shadowsTarget = webgl.createRenderTarget(width, height, false);
this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent);
this.ssaoFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
@@ -504,29 +357,9 @@ export class PostprocessingPass {
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.downsampledDepthTarget = drawPass.packedDepth
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampledDepthTarget = webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
const depthTexture = this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture;
this.depthHalfTarget = drawPass.packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, depthTexture);
this.depthQuarterTarget = drawPass.packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
@@ -537,84 +370,59 @@ export class PostprocessingPass {
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture, this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
this.ssaoRenderable = getSsaoRenderable(webgl, this.downsampleFactor === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true);
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.outlinesTarget.texture, this.ssaoDepthTexture);
this.background = new BackgroundPass(webgl, assetManager, width, height);
}
setSize(width: number, height: number) {
const [w, h] = this.renderable.values.uTexSize.ref.value;
const ssaoScale = this.calcSsaoScale(1);
const ssaoScale = this.calcSsaoScale();
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
this.ssaoScale = ssaoScale;
this.target.setSize(width, height);
this.outlinesTarget.setSize(width, height);
this.shadowsTarget.setSize(width, height);
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
this.target.setSize(width, height);
this.outlinesTarget.setSize(width, height);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.depthQuarterTarget.setSize(qw, qh);
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
this.depthHalfRenderable.update();
this.ssaoRenderable.update();
this.background.setSize(width, height);
}
}
private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
let needsUpdateShadows = false;
private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
let needsUpdateMain = false;
let needsUpdateSsao = false;
let needsUpdateSsaoBlur = false;
let needsUpdateDepthHalf = false;
let needsUpdateOutlines = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const outlinesEnabled = props.outline.name === 'on';
const shadowsEnabled = props.shadow.name === 'on';
const occlusionEnabled = props.occlusion.name === 'on';
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.renderable.values.uTexSize.ref.value;
const v = camera.viewport;
if (props.occlusion.name === 'on') {
ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
const [w, h] = this.renderable.values.uTexSize.ref.value;
const b = this.ssaoRenderable.values.uBounds;
const v = camera.viewport;
const s = this.ssaoScale;
Vec4.set(b.ref.value,
Math.floor(v.x * s) / (w * s),
@@ -632,14 +440,11 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection);
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateSsaoBlur = true;
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
if (this.nSamples !== props.occlusion.params.samples) {
needsUpdateSsao = true;
@@ -648,30 +453,7 @@ export class PostprocessingPass {
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
}
const multiScale = props.occlusion.params.multiScale.name === 'on';
if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
needsUpdateSsao = true;
ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
}
if (props.occlusion.params.multiScale.name === 'on') {
const mp = props.occlusion.params.multiScale.params;
if (!deepEqual(this.levels, mp.levels)) {
needsUpdateSsao = true;
this.levels = mp.levels;
const levels = getLevels(mp.levels);
ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count);
ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius);
ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias);
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold);
ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold);
} else {
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
@@ -682,112 +464,54 @@ export class PostprocessingPass {
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
}
const ssaoScale = this.calcSsaoScale(props.occlusion.params.resolutionScale);
if (this.ssaoScale !== ssaoScale) {
if (this.downsampleFactor !== props.occlusion.params.resolutionScale) {
needsUpdateSsao = true;
needsUpdateDepthHalf = true;
this.ssaoScale = ssaoScale;
this.downsampleFactor = props.occlusion.params.resolutionScale;
this.ssaoScale = this.calcSsaoScale();
const sw = Math.floor(w * this.ssaoScale);
const sh = Math.floor(h * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.floor(sw * 0.5);
const hh = Math.floor(sh * 0.5);
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.floor(sw * 0.25);
const qh = Math.floor(sh * 0.25);
this.depthQuarterTarget.setSize(qw, qh);
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture);
ValueCell.update(this.ssaoRenderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
if (this.ssaoScale === 1) {
ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTextureTransparent);
} else {
ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture);
}
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
}
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color));
}
if (props.shadow.name === 'on') {
ValueCell.update(this.shadowsRenderable.values.uProjection, camera.projection);
ValueCell.update(this.shadowsRenderable.values.uInvProjection, invProjection);
Vec4.set(this.shadowsRenderable.values.uBounds.ref.value,
v.x / w,
v.y / h,
(v.x + v.width) / w,
(v.y + v.height) / h
);
ValueCell.update(this.shadowsRenderable.values.uBounds, this.shadowsRenderable.values.uBounds.ref.value);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic);
needsUpdateShadows = true;
}
ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.shadow.params.bias);
if (this.shadowsRenderable.values.dSteps.ref.value !== props.shadow.params.steps) {
ValueCell.update(this.shadowsRenderable.values.dSteps, props.shadow.params.steps);
needsUpdateShadows = true;
}
ValueCell.update(this.shadowsRenderable.values.uLightDirection, light.direction);
ValueCell.update(this.shadowsRenderable.values.uLightColor, light.color);
if (this.shadowsRenderable.values.dLightCount.ref.value !== light.count) {
ValueCell.update(this.shadowsRenderable.values.dLightCount, light.count);
needsUpdateShadows = true;
}
}
if (props.outline.name === 'on') {
const transparentOutline = props.outline.params.includeTransparent ?? true;
let { threshold } = props.outline.params;
// orthographic needs lower threshold
if (camera.state.mode === 'orthographic') threshold /= 5;
const factor = Math.pow(1000, threshold) / 1000;
// use radiusMax for stable outlines when zooming
const maxPossibleViewZDiff = factor * camera.state.radiusMax;
const outlineScale = props.outline.params.scale - 1;
const outlineThreshold = 50 * props.outline.params.threshold;
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
ValueCell.update(this.outlinesRenderable.values.uInvProjection, invProjection);
if (this.outlinesRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
}
if (this.outlinesRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.outlinesRenderable.values.uOutlineThreshold, outlineThreshold);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOutlineScale, outlineScale);
}
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline);
}
ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
}
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
@@ -796,31 +520,12 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor));
ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground);
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
}
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOutlineEnable, outlinesEnabled);
}
if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dShadowEnable, shadowsEnabled);
}
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
}
if (needsUpdateOutlines) {
this.outlinesRenderable.update();
}
if (needsUpdateShadows) {
this.shadowsRenderable.update();
}
if (this.renderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, outlinesEnabled);
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
if (needsUpdateSsao) {
this.ssaoRenderable.update();
@@ -831,10 +536,6 @@ export class PostprocessingPass {
this.ssaoBlurSecondPassRenderable.update();
}
if (needsUpdateDepthHalf) {
this.depthHalfRenderable.update();
}
if (needsUpdateMain) {
this.renderable.update();
}
@@ -845,6 +546,10 @@ export class PostprocessingPass {
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
const { x, y, width, height } = camera.viewport;
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
}
private occlusionOffset: [x: number, y: number] = [0, 0];
@@ -859,42 +564,23 @@ export class PostprocessingPass {
this.transparentBackground = value;
}
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
this.updateState(camera, transparentBackground, backgroundColor, props, light);
this.updateState(camera, transparentBackground, backgroundColor, props);
const { gl, state } = this.webgl;
const { x, y, width, height } = camera.viewport;
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
}
// don't render occlusion if offset is given,
// which will reuse the existing occlusion
if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) {
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
const sx = Math.floor(x * this.ssaoScale);
const sy = Math.floor(y * this.ssaoScale);
const sw = Math.ceil(width * this.ssaoScale);
const sh = Math.ceil(height * this.ssaoScale);
state.viewport(sx, sy, sw, sh);
state.scissor(sx, sy, sw, sh);
if (this.ssaoScale < 1) {
if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
this.downsampledDepthTarget.bind();
this.downsampleDepthRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
}
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
this.depthHalfTarget.bind();
this.depthHalfRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
this.depthQuarterTarget.bind();
this.depthQuarterRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
this.ssaoFramebuffer.bind();
this.ssaoRenderable.render();
@@ -903,20 +589,6 @@ export class PostprocessingPass {
this.ssaoBlurSecondPassFramebuffer.bind();
this.ssaoBlurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
}
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
}
if (props.shadow.name === 'on') {
this.shadowsTarget.bind();
this.shadowsRenderable.render();
}
if (toDrawingBuffer) {
@@ -925,6 +597,8 @@ export class PostprocessingPass {
this.target.bind();
}
const { gl, state } = this.webgl;
this.background.update(camera, props.background);
if (this.background.isEnabled(props.background)) {
if (this.transparentBackground) {
@@ -1010,3 +684,4 @@ export class AntialiasingPass {
}
}
}

View File

@@ -93,35 +93,20 @@ namespace Column {
return !!v && !!(v as Column<any>).schema && !!(v as Column<any>).value;
}
// Value kinds are accessed very of often
// Using a const enum is an internal optimization and is defined separately to better support
// compiling with isolatedModules flag in 3rd party use-cases of Mol*.
export const enum ValueKinds {
/** Defined value (= 0) */
export const enum ValueKind {
Present = 0,
/** Expressed in CIF as `.` (= 1) */
/** Expressed in CIF as `.` */
NotPresent = 1,
/** Expressed in CIF as `?` (= 2) */
/** Expressed in CIF as `?` */
Unknown = 2
}
export const ValueKind = {
/** Defined value (= 0) */
Present: ValueKinds.Present,
/** Expressed in CIF as `.` (= 1) */
NotPresent: ValueKinds.NotPresent,
/** Expressed in CIF as `?` (= 2) */
Unknown: ValueKinds.Unknown
} as const;
export type ValueKind = (typeof ValueKind)[keyof typeof ValueKinds];
export function Undefined<T extends Schema>(rowCount: number, schema: T): Column<T['T']> {
return constColumn(schema['T'], rowCount, schema, ValueKinds.NotPresent);
return constColumn(schema['T'], rowCount, schema, ValueKind.NotPresent);
}
export function ofConst<T extends Schema>(v: T['T'], rowCount: number, type: T): Column<T['T']> {
return constColumn(v, rowCount, type, ValueKinds.Present);
return constColumn(v, rowCount, type, ValueKind.Present);
}
export function ofLambda<T extends Schema>(spec: LambdaSpec<T>): Column<T['T']> {
@@ -271,7 +256,7 @@ function constColumn<T extends Column.Schema>(v: T['T'], rowCount: number, schem
return {
schema: schema,
__array: void 0,
isDefined: valueKind === Column.ValueKinds.Present,
isDefined: valueKind === Column.ValueKind.Present,
rowCount,
value,
valueKind: row => valueKind,
@@ -291,7 +276,7 @@ function lambdaColumn<T extends Column.Schema>({ value, valueKind, areValuesEqua
isDefined: true,
rowCount,
value,
valueKind: valueKind ? valueKind : row => Column.ValueKinds.Present,
valueKind: valueKind ? valueKind : row => Column.ValueKind.Present,
toArray: params => {
const { array, start } = ColumnHelpers.createArray(rowCount, params);
for (let i = 0, _i = array.length; i < _i; i++) array[i] = value(i + start);
@@ -319,7 +304,7 @@ function arrayColumn<T extends Column.Schema>({ array, schema, valueKind }: Colu
isDefined: true,
rowCount,
value,
valueKind: valueKind ? valueKind : row => Column.ValueKinds.Present,
valueKind: valueKind ? valueKind : row => Column.ValueKind.Present,
toArray: schema.valueType === 'str'
? (schema as Column.Schema.Str).transform === 'lowercase'
? params => {

Some files were not shown because too many files have changed in this diff Show More