mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 23:34:23 +08:00
Compare commits
2 Commits
support-sc
...
ssao-impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9591ca8a5 | ||
|
|
2c7ecca7fc |
84
CHANGELOG.md
84
CHANGELOG.md
@@ -5,94 +5,20 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Volume UI improvements
|
||||
- Render all volume entries instead of selecting them one-by-one
|
||||
- Toggle visibility of all volumes
|
||||
- More accessible iso value control
|
||||
- Support wheel event on sliders
|
||||
- MolViewSpec extension:
|
||||
- Add validation for discriminated union params
|
||||
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
|
||||
- Add `external-structure` theme that colors any geometry by structure properties
|
||||
- Support float and half-float data type for direct-volume rendering and GPU isosurface extraction
|
||||
- Minor documentation updates
|
||||
- Fix plugin mouse interactions when CSS `scale` transform is applied
|
||||
|
||||
## [v4.10.0] - 2024-12-15
|
||||
|
||||
- Add `ModelWithCoordinates` decorator transform.
|
||||
- Fix outlines on transparent background using illumination mode (#1364)
|
||||
- Fix transparent depth texture artifacts using illumination mode
|
||||
- Fix marking of consecutive gap elements (#876)
|
||||
- Allow React 19 in dependencies
|
||||
- Fix missing deflate header if `CompressionStream` is available
|
||||
- Fix is_iOS check for NodeJS
|
||||
- Added PluginCommands.Camera.FocusObject
|
||||
- Plugin state snapshot can have instructions to focus objects (PluginState.Snapshot.camera.focus)
|
||||
- MolViewSpec extension: Support for multi-state files (animations)
|
||||
- Fix units transform data not fully updated when structure child changes
|
||||
- Fix `addIndexPairBonds` quadratic runtime case
|
||||
- Use adjoint matrix to transform normals in shaders
|
||||
- Fix resize handling in `tests/browser`
|
||||
|
||||
## [v4.9.1] - 2024-12-05
|
||||
|
||||
- Fix iOS check when running on Node
|
||||
|
||||
## [v4.9.0] - 2024-12-01
|
||||
|
||||
- Fix artifacts when using xray shading with high xrayEdgeFalloff values
|
||||
- Enable double rounded capping on tubular helices
|
||||
- Fix single residue tubular helices not showing up
|
||||
- Fix outlines on volume and surface reps that do not disappear (#1326)
|
||||
- Add example `glb-export`
|
||||
- Membrane orientation: Improve `isApplicable` check and error handling (#1316)
|
||||
- Fix set fenceSync to null after deleteSync.
|
||||
- Fix operator key-based `IndexPairBonds` assignment
|
||||
- Don't add bonds twice
|
||||
- Add `IndexPairs.bySameOperator` to avoid looping over all bonds for each unit
|
||||
- Add `Structure.intraUnitBondMapping`
|
||||
- Add more structure-based visuals to avoid too many (small) render-objects
|
||||
- `structure-intra-bond`, `structure-ellipsoid-mesh`, `structure-element-point`, `structure-element-cross`
|
||||
- Upgrade to express v5 (#1311)
|
||||
- Fix occupancy check using wrong index for inter-unit bond computation (@rxht, #1321)
|
||||
- Fix transparent SSAO for image rendering, e.g., volumne slices (#1332)
|
||||
- Fix bonds not shown with `ignoreHydrogens` on (#1315)
|
||||
- Better handle mmCIF files with no entities defined by using `label_asym_id`
|
||||
- Show bonds in water chains when `ignoreHydorgensVariant` is `non-polar`
|
||||
- Add MembraneServer API, generating data to be consumed in the context of MolViewSpec
|
||||
- Fix `StructConn.isExhaustive` for partial models (e.g., returned by the model server)
|
||||
- Refactor value swapping in molstar-math to fix SWC (Next.js) build (#1345)
|
||||
- Fix transform data not updated when structure child changes
|
||||
- Fix `PluginStateSnapshotManager.syncCurrent` to work as expected on re-loaded states.
|
||||
- Fix do not compute implicit hydrogens when unit is explicitly protonated (#1257)
|
||||
- ModelServer and VolumeServer: support for input files from Google Cloud Storage (gs://)
|
||||
- Fix color of missing partial charges for SB partial charges extension
|
||||
|
||||
## [v4.8.0] - 2024-10-27
|
||||
|
||||
- Add SSAO support for transparent geometry
|
||||
- Fix SSAO color not updating
|
||||
- Improve blending of overlapping outlines from transparent & opaque geometries
|
||||
- Default to `blended` transparency on iOS due to `wboit` not being supported.
|
||||
- Fix direct-volume with fog off (and on with `dpoit`) and transparent background on (#1286)
|
||||
- Fix missing pre-multiplied alpha for `blended` & `wboit` with no fog (#1284)
|
||||
- Fix backfaces visible using blended transparency on impostors (#1285)
|
||||
- Fix StructureElement.Loci.isSubset() only considers common units (#1292)
|
||||
- Fix `Scene.opacityAverage` calculation never 1
|
||||
- Fix bloom in illumination mode
|
||||
- Fix `findPredecessorIndex` bug when repeating values
|
||||
- MolViewSpec: Support for transparency and custom properties
|
||||
- MolViewSpec: MVP Support for geometrical primitives (mesh, lines, line, label, distance measurement)
|
||||
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-Dev loader
|
||||
- Fix Sequence View in Safari 18
|
||||
- Improve performance of `IndexPairBonds` assignment when operator keys are available
|
||||
- ModelArchive QualityAssessment extension:
|
||||
- Add support for ma_qa_metric_local_pairwise mmCIF category
|
||||
- Add PAE plot component
|
||||
- Add new AlphaFoldDB-PAE example app
|
||||
- Add support for LAMMPS data and dump formats
|
||||
- Remove extra anti-aliasing from text shader (fixes #1208 & #1306)
|
||||
- SSAO improvements
|
||||
- Add `blurStepSize` to reduce blur cost. Works best with multi-sample enabled to mitigate artefacts. Defaults to 2 (since multi-sample is enabled by default). Set to 1 for screenshots.
|
||||
- Add `blurNormalBias` to improve creases between overlapping spheres. Quite expensive, use with care. Disabled by default (set to zero).
|
||||
- Remove `devicePixelRatio` adjustment of resolution. This is a bit more expensive but avoids ugly edge artefacts.
|
||||
- Reduce default `samples` from 32 to 24. Still at 128 for screenshots.
|
||||
|
||||
## [v4.7.1] - 2024-09-30
|
||||
|
||||
|
||||
@@ -876,17 +876,6 @@ ma_qa_metric_local.metric_value
|
||||
ma_qa_metric_local.model_id
|
||||
ma_qa_metric_local.ordinal_id
|
||||
|
||||
ma_qa_metric_local_pairwise.ordinal_id
|
||||
ma_qa_metric_local_pairwise.model_id
|
||||
ma_qa_metric_local_pairwise.label_asym_id_1
|
||||
ma_qa_metric_local_pairwise.label_comp_id_1
|
||||
ma_qa_metric_local_pairwise.label_seq_id_1
|
||||
ma_qa_metric_local_pairwise.label_asym_id_2
|
||||
ma_qa_metric_local_pairwise.label_comp_id_2
|
||||
ma_qa_metric_local_pairwise.label_seq_id_2
|
||||
ma_qa_metric_local_pairwise.metric_id
|
||||
ma_qa_metric_local_pairwise.metric_value
|
||||
|
||||
ma_software_group.group_id
|
||||
ma_software_group.ordinal_id
|
||||
ma_software_group.software_id
|
||||
|
||||
|
@@ -29,7 +29,7 @@ node lib/commonjs/servers/model/server --sourceMap pdb-bcif '/opt/data/bcif/${id
|
||||
| `--maxQueryManyQueries` | Maximum number of queries allowed by the query-many at a time |
|
||||
| `--defaultSource` | modifies which 'sourceMap' source to use by default |
|
||||
| `--sourceMap` | Map `id`s for a `source` to a file path. Example: `pdb-bcif '../../data/bcif/${id}.bcif'` - JS expressions can be used inside `${}`, e.g. `${id.substr(1, 2)}/${id}.mdb` Can be specified multiple times. The `SOURCE` variable (e.g. `pdb-bcif`) is arbitrary and depends on how you plan to use the server. Supported formats: cif, bcif, cif.gz, bcif.gz |
|
||||
| `--sourceMapUrl` | Same as `--sourceMap` but for URL. `--sourceMapUrl src url format` Example: `pdb-cif 'https://www.ebi.ac.uk/pdbe/entry-files/download/${id}_updated.cif' cif` Supported formats: cif, bcif, cif.gz, bcif.gz. Supported protocols: http://, https://, gs:// |
|
||||
| `--sourceMapUrl` | Same as `--sourceMap` but for URL. `--sourceMapUrl src url format` Example: `pdb-cif "https://www.ebi.ac.uk/pdbe/entry-files/download/${id}_updated.cif" cif` Supported formats: cif, bcif, cif.gz, bcif.gz |
|
||||
|
||||
```sh
|
||||
node lib/commonjs/servers/model/server [-h] [-v]
|
||||
|
||||
@@ -66,7 +66,7 @@ To achieve this, use the ``pack`` application (``node lib/commonjs/servers/volum
|
||||
|
||||
### Local Mode
|
||||
|
||||
The program ``lib/commonjs/servers/volume/query`` (``volume-server-query`` in NPM package) can be used to query the data without running a http server.
|
||||
The program ``lib/commonjs/servers/volume/pack`` (``volume-server-query`` in NPM package) can be used to query the data without running a http server.
|
||||
|
||||
### Navigating the Source Code
|
||||
|
||||
@@ -105,7 +105,7 @@ node lib/commonjs/servers/volume/server --idMap x-ray '/opt/data/xray/${id}.mdb'
|
||||
| `--defaultPort` | Specify the port the server is running on |
|
||||
| `--shutdownTimeoutMinutes` | Server will shut down after this amount of minutes, 0 for off. |
|
||||
| `--shutdownTimeoutVarianceMinutes` | Modifies the shutdown timer by +/- `timeoutVarianceMinutes` (to avoid multiple instances shutting at the same time) |
|
||||
| `--idMap` | Map `id`s for a `type` to a file path. Example: `x-ray '../../data/mdb/xray/${id}-ccp4.mdb'` - JS expressions can be used inside `${}`, e.g. `${id.substr(1, 2)}/${id}.mdb` - Can be specified multiple times. - The `TYPE` variable (e.g. `x-ray`) is arbitrary and depends on how you plan to use the server. By default, Mol* Viewer uses `x-ray` and `em`, but any particular use case may vary. - If using URL, it can be http://, https://, gs:// or file:// protocol.|
|
||||
| `--idMap` | Map `id`s for a `type` to a file path. Example: `x-ray '../../data/mdb/xray/${id}-ccp4.mdb'` - JS expressions can be used inside `${}`, e.g. `${id.substr(1, 2)}/${id}.mdb` - Can be specified multiple times. - The `TYPE` variable (e.g. `x-ray`) is arbitrary and depends on how you plan to use the server. By default, Mol* Viewer uses `x-ray` and `em`, but any particular use case may vary. |
|
||||
| `--maxRequestBlockCount` | Maximum number of blocks that could be read in 1 query. This is somewhat tied to the ``maxOutputSizeInVoxelCountByPrecisionLevel`` in that the `<maximum number of voxel> = maxRequestBlockCount * <block size>^3`. The default block size is 96 which corresponds to 28,311,552 voxels with 32 max blocks. |
|
||||
| `--maxFractionalBoxVolume` | The maximum fractional volume of the query box (to prevent queries that are too big). |
|
||||
| `--maxOutputSizeInVoxelCountByPrecisionLevel` | What is the (approximate) maximum desired size in voxel count by precision level - Rule of thumb: `<response gzipped size>` in `[<voxel count> / 8, <voxel count> / 4]`. The maximum number of voxels is tied to maxRequestBlockCount. |
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
What is a plugin? A plugin is a collection of modules that provide functionality to the `Mol*` UI. The plugin is responsible for managing the state of the viewer, internal and user interactions. It has been a previous point of confusion for new users of `Mol*` to associate the __viewer__ part of the library with what is further referred to as the __plugin__. These two are closely connected in the `molstar-plugin-ui` module, which is the user-facing part of the library and ultimately provides the viewer, but they are ultimately distinct.
|
||||
|
||||
|
||||
It is recommended that you inspect the general class structure of [`PluginInitWrapper`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/plugin.tsx#L41), [`PluginUIContext`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/context.ts#L12) and [`PluginUIComponent`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/base.tsx#L16) to better understand the flow of data and events in the plugin.
|
||||
It is recommended that you inspect the general class structure of [`PluginInitWrapper`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/plugin.tsx#L41), [`PluginUIContext`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin/context.ts#L71) and [`PluginUIComponent`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/base.tsx#L16) to better understand the flow of data and events in the plugin.
|
||||
A passing analogy is that a [ `PluginContext` ](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin/context.ts#L71) is the engine that powers computation, rendering, events and subscriptions inside the molstar UI. All UI components depend on `PluginContext`.
|
||||
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Assign custom conformation to a Model
|
||||
|
||||
This document shows how to update model conformation dynamically using the `ModelWithCoordinates` transforms. If this does not work well with your particular use case, it is suggested to write a custom version of `ModelWithCoordinates` with similar usage as outlined in this document.
|
||||
|
||||
```ts
|
||||
async function animateFirstXCoordinateExample(plugin: PluginContext, url: string, format: BuiltInTrajectoryFormat) {
|
||||
// Load data
|
||||
const _data = await plugin.builders.data.download({ url });
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(_data, format);
|
||||
const hierarchy = await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
|
||||
if (!hierarchy) return;
|
||||
|
||||
// Insert ModelWithCoordinates cell to be updated in the loop bellow
|
||||
const coordinatesNode = await plugin.build().to(hierarchy!.model).insert(ModelWithCoordinates).commit();
|
||||
|
||||
const x0 = hierarchy!.model.data!.atomicConformation.x[0];
|
||||
let xOffset = 0;
|
||||
async function animateFirstXCoord() {
|
||||
// Normally, the whole conformation would come from an API/library call, but here we fake it:
|
||||
const { x, y, z } = hierarchy!.model.data!.atomicConformation;
|
||||
const nextX = [...(x as number[])];
|
||||
nextX[0] = x0 + xOffset;
|
||||
xOffset += 0.05;
|
||||
if (xOffset > 1) xOffset = 0;
|
||||
|
||||
// Construct new coodinate frame from the data and commit the update.
|
||||
// Rest of the state tree will reconcile automatically.
|
||||
await plugin.build().to(coordinatesNode).update({
|
||||
atomicCoordinateFrame: {
|
||||
elementCount: x.length,
|
||||
time: { value: 0, unit: 'step' },
|
||||
xyzOrdering: { isIdentity: true },
|
||||
x: nextX,
|
||||
y,
|
||||
z,
|
||||
}
|
||||
}).commit();
|
||||
|
||||
requestAnimationFrame(animateFirstXCoord);
|
||||
}
|
||||
animateFirstXCoord();
|
||||
}
|
||||
|
||||
// animateFirstXCoordinateExample('https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/2244/record/SDF/?record_type=3d', 'sdf');
|
||||
```
|
||||
@@ -40,7 +40,6 @@ nav:
|
||||
- CIF Schemas: 'plugin/cif-schemas.md'
|
||||
- State Transforms:
|
||||
- Custom Trajectory: 'plugin/transforms/custom-trajectory.md'
|
||||
- Custom Conformation: 'plugin/transforms/custom-conformation.md'
|
||||
- Data Access Tools:
|
||||
- 'data-access-tools/model-server.md'
|
||||
- Volume Server:
|
||||
|
||||
3052
package-lock.json
generated
3052
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
57
package.json
57
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.10.0",
|
||||
"version": "4.7.1",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -112,24 +112,22 @@
|
||||
"Paul Pillot <paul.pillot@tandemai.com>",
|
||||
"Herman Bergwerf <post@hbergwerf.nl>",
|
||||
"Eric E <etongfu@outlook.com>",
|
||||
"Xavier Martinez <xavier.martinez.xm@gmail.com>",
|
||||
"Alex Chan <smalldirkalex@gmail.com>",
|
||||
"Simeon Borko <simeon.borko@gmail.com>"
|
||||
"Xavier Martinez <xavier.martinez.xm@gmail.com"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.16",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.1.0",
|
||||
"cpx2": "^8.0.0",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"cpx2": "^7.0.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^8.57.1",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
@@ -138,58 +136,55 @@
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"mini-css-extract-plugin": "^2.9.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.83.0",
|
||||
"sass-loader": "^16.0.4",
|
||||
"sass": "^1.79.4",
|
||||
"sass-loader": "^16.0.2",
|
||||
"simple-git": "^3.27.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"webpack": "^5.97.1",
|
||||
"typescript": "^5.6.2",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/argparse": "^2.0.16",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.7.5",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^18.19.68",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^18.19.54",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/swagger-ui-dist": "3.30.5",
|
||||
"argparse": "^2.0.1",
|
||||
"compression": "^1.7.5",
|
||||
"body-parser": "^1.20.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.0.1",
|
||||
"express": "^4.21.0",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^5.0.3",
|
||||
"io-ts": "^2.2.22",
|
||||
"immutable": "^4.3.7",
|
||||
"io-ts": "^2.2.21",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-dist": "^5.18.2",
|
||||
"tslib": "^2.8.1",
|
||||
"swagger-ui-dist": "^5.17.14",
|
||||
"tslib": "^2.7.0",
|
||||
"util.promisify": "^1.1.2",
|
||||
"xhr2": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
"canvas": "^2.11.2",
|
||||
"gl": "^6.0.2",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"pngjs": "^6.0.0",
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
"react": "^18.1.0 || ^17.0.2 || ^16.14.0",
|
||||
"react-dom": "^18.1.0 || ^17.0.2 || ^16.14.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@google-cloud/storage": {
|
||||
"optional": true
|
||||
},
|
||||
"canvas": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
@@ -61,13 +61,9 @@ function copyDemos() {
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
try {
|
||||
copyViewer();
|
||||
copyMe();
|
||||
copyDemos();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
copyViewer();
|
||||
copyMe();
|
||||
copyDemos();
|
||||
}
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
|
||||
@@ -44,6 +44,18 @@ function occlusionStyle(plugin: PluginContext) {
|
||||
},
|
||||
postprocessing: {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'on', params: {
|
||||
blurKernelSize: 15,
|
||||
blurStepSize: 2,
|
||||
blurDepthBias: 0.5,
|
||||
blurNormalBias: 0.0,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
samples: 24,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
} },
|
||||
outline: { name: 'on', params: {
|
||||
scale: 1.0,
|
||||
threshold: 0.33,
|
||||
|
||||
@@ -394,10 +394,7 @@ export function MesoViewportSnapshotDescription() {
|
||||
{showInfo}{increasePoliceSize}{decreasePoliceSize}
|
||||
</div>
|
||||
<div id='snapinfo' className={`msp-snapshot-description-me ${isShown ? 'shown' : 'hidden'}`} style={{ fontSize: `${textSize}px` }}>
|
||||
{e.descriptionFormat === 'plaintext'
|
||||
&& e.description
|
||||
|| <Markdown skipHtml={false} components={{ a: MesoMarkdownAnchor }}>{e.description}</Markdown>
|
||||
}
|
||||
{<Markdown skipHtml={false} components={{ a: MesoMarkdownAnchor }}>{e.description}</Markdown>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ function adjustPluginProps(ctx: PluginContext) {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
samples: 24,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
@@ -81,10 +81,11 @@ function adjustPluginProps(ctx: PluginContext) {
|
||||
radius: 5,
|
||||
bias: 1,
|
||||
blurKernelSize: 11,
|
||||
blurStepSize: 1,
|
||||
blurDepthBias: 0.5,
|
||||
blurNormalBias: 0.0,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
transparentThreshold: 0.4,
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
@@ -213,14 +214,8 @@ export async function loadPdb(ctx: PluginContext, id: string) {
|
||||
|
||||
export async function loadPdbDev(ctx: PluginContext, id: string) {
|
||||
await reset(ctx);
|
||||
let url: string;
|
||||
// 4 character PDB id, TODO: support extended PDB ID
|
||||
if (id.match(/^[1-9][A-Z0-9]{3}$/i) !== null) {
|
||||
url = `https://pdb-dev.wwpdb.org/bcif/${id.toLowerCase()}.bcif`;
|
||||
} else {
|
||||
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
|
||||
url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
|
||||
}
|
||||
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
|
||||
const url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
|
||||
const data = await ctx.builders.data.download({ url, isBinary: true });
|
||||
await createHierarchy(ctx, data.ref);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { DnatcoNtCs } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
import { MAQualityAssessment, MAQualityAssessmentConfig, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { MAQualityAssessment, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { ModelExport } from '../../extensions/model-export';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
@@ -48,7 +48,7 @@ import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { renderReact18 } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
@@ -125,8 +125,6 @@ const DefaultViewerOptions = {
|
||||
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
|
||||
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
|
||||
|
||||
config: [] as [PluginConfigItem, any][],
|
||||
};
|
||||
type ViewerOptions = typeof DefaultViewerOptions;
|
||||
|
||||
@@ -206,7 +204,6 @@ export class Viewer {
|
||||
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
|
||||
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
|
||||
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
|
||||
...(o.config ?? []),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -619,9 +616,4 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
mvs: { MVSData, loadMVS },
|
||||
modelArchive: {
|
||||
qualityAssessment: {
|
||||
config: MAQualityAssessmentConfig
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
|
||||
import { MVSDefaults } from '../../extensions/mvs/tree/mvs/mvs-defaults';
|
||||
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
@@ -31,9 +32,9 @@ function parseArguments(): Args {
|
||||
/** Main workflow for printing MolViewSpec tree schema. */
|
||||
function main(args: Args) {
|
||||
if (args.markdown) {
|
||||
console.log(treeSchemaToMarkdown(MVSTreeSchema));
|
||||
console.log(treeSchemaToMarkdown(MVSTreeSchema, MVSDefaults));
|
||||
} else {
|
||||
console.log(treeSchemaToString(MVSTreeSchema));
|
||||
console.log(treeSchemaToString(MVSTreeSchema, MVSDefaults));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,9 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*
|
||||
* Command-line application for rendering images from MolViewSpec files
|
||||
* From Molstar NPM package:
|
||||
* npm install molstar canvas gl jpeg-js pngjs
|
||||
* npx mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
|
||||
* From Molstar source code:
|
||||
* npm install
|
||||
* npm install --no-save canvas gl jpeg-js pngjs // these packages are not listed in Mol* dependencies for performance reasons
|
||||
* npm run build
|
||||
* node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
|
||||
* Build: npm install --no-save canvas gl jpeg-js pngjs // these packages are not listed in Mol* dependencies for performance reasons
|
||||
* npm run build
|
||||
* Run: node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
@@ -34,7 +29,6 @@ import { onelinerJsonString } from '../../mol-util/json';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
// MolViewSpec must be imported after HeadlessPluginContext
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { loadMVSX } from '../../extensions/mvs/components/formats';
|
||||
import { loadMVS } from '../../extensions/mvs/load';
|
||||
@@ -106,11 +100,7 @@ async function main(args: Args): Promise<void> {
|
||||
if (args.molj) {
|
||||
await plugin.saveStateSnapshot(withExtension(output, '.molj'));
|
||||
}
|
||||
if (output.toLowerCase().endsWith('.mp4')) {
|
||||
await plugin.saveAnimation(output);
|
||||
} else {
|
||||
await plugin.saveImage(output);
|
||||
}
|
||||
await plugin.saveImage(output);
|
||||
checkState(plugin);
|
||||
}
|
||||
await plugin.clear();
|
||||
@@ -122,7 +112,6 @@ async function createHeadlessPlugin(args: Pick<Args, 'size'>): Promise<HeadlessP
|
||||
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
|
||||
const spec = DefaultPluginSpec();
|
||||
spec.behaviors.push(PluginSpec.Behavior(MolViewSpec));
|
||||
spec.behaviors.push(PluginSpec.Behavior(Mp4Export));
|
||||
const headlessCanvasOptions = defaultCanvas3DParams();
|
||||
const canvasOptions = {
|
||||
...PD.getDefaultValues(Canvas3DParams),
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* AlphaFold DB Predicted Aligned Error Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
#plot {
|
||||
position: absolute;
|
||||
left: 680px;
|
||||
top: 20px;
|
||||
width: 480px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 520px;
|
||||
font-family: sans-serif;
|
||||
font-size: smaller;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='controls'>
|
||||
<input type='text' id='af-id' value='Q8W3K0' />
|
||||
<button id='af-load'>Load</button>
|
||||
</div>
|
||||
<div id='app'></div>
|
||||
<div id='plot'></div>
|
||||
<script>
|
||||
AlphaFoldPAEExample.init({ pluginContainerId: 'app', plotContainerId: 'plot' }).then(example => {
|
||||
example.load('Q8W3K0')
|
||||
});
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
$('af-load').onclick = () => AlphaFoldPAEExample.load($('af-id').value)
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Viewer } from '../../apps/viewer/app';
|
||||
import { MAPairwiseScorePlot } from '../../extensions/model-archive/quality-assessment/pairwise/ui';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { Model, ResidueIndex } from '../../mol-model/structure';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
export class AlphaFoldPAEExample {
|
||||
viewer: Viewer;
|
||||
plotContainerId: string;
|
||||
|
||||
|
||||
async init(options: { pluginContainerId: string, plotContainerId: string }) {
|
||||
this.plotContainerId = options.plotContainerId;
|
||||
this.viewer = await Viewer.create(options.pluginContainerId, {
|
||||
layoutIsExpanded: false,
|
||||
layoutShowControls: false,
|
||||
layoutShowLeftPanel: false,
|
||||
layoutShowLog: false,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async load(afId: string) {
|
||||
const id = afId.trim().toUpperCase();
|
||||
|
||||
const plotRoot = createRoot(document.getElementById(this.plotContainerId)!);
|
||||
plotRoot.render(<div>Loading...</div>);
|
||||
|
||||
await this.viewer.plugin.clear();
|
||||
await this.viewer.loadAlphaFoldDb(id);
|
||||
|
||||
try {
|
||||
const req = await fetch(`https://alphafold.ebi.ac.uk/files/AF-${id}-F1-predicted_aligned_error_v4.json`);
|
||||
const json = await req.json();
|
||||
|
||||
const model = this.viewer.plugin.managers.structure.hierarchy.current.models[0]?.cell.obj?.data!;
|
||||
const metric = pairwiseMetricFromAlphaFoldDbJson(model, json)!;
|
||||
|
||||
createRoot(document.getElementById(this.plotContainerId)!).render(
|
||||
<div className='msp-plugin' style={{ background: 'white' }}>
|
||||
<MAPairwiseScorePlot plugin={this.viewer.plugin} pairwiseMetric={metric} model={model} />
|
||||
</div>
|
||||
);
|
||||
} catch (err) {
|
||||
plotRoot.render(<div>Error: {String(err)}</div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pairwiseMetricFromAlphaFoldDbJson(model: Model, data: any): QualityAssessment.Pairwise | undefined {
|
||||
if (!Array.isArray(data) || !data[0]?.predicted_aligned_error) return undefined;
|
||||
|
||||
const { residues, residueAtomSegments, atomSourceIndex } = model.atomicHierarchy;
|
||||
const sortedResidueIndices = new Array(residues._rowCount).fill(0).map((_, i) => i);
|
||||
sortedResidueIndices.sort((a, b) => {
|
||||
const idxA = atomSourceIndex.value(residueAtomSegments.offsets[a]);
|
||||
const idxB = atomSourceIndex.value(residueAtomSegments.offsets[b]);
|
||||
return idxA - idxB;
|
||||
});
|
||||
|
||||
const metricData = data[0].predicted_aligned_error as number[][];
|
||||
|
||||
const metric: QualityAssessment.Pairwise = {
|
||||
id: 0,
|
||||
name: 'AlphaFold DB PAE',
|
||||
residueRange: [0 as ResidueIndex, (residues._rowCount - 1) as ResidueIndex],
|
||||
valueRange: [0, data[0].max_predicted_aligned_error],
|
||||
values: {}
|
||||
};
|
||||
|
||||
for (let i = 0; i < metricData.length; i++) {
|
||||
const rA = sortedResidueIndices[i];
|
||||
if (typeof rA !== 'number') continue;
|
||||
const row = metricData[i];
|
||||
const xs: any = (metric.values[rA as ResidueIndex] = {});
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
const rB = sortedResidueIndices[j];
|
||||
if (typeof rB !== 'number') continue;
|
||||
xs[rB] = row[j];
|
||||
}
|
||||
}
|
||||
|
||||
return metric;
|
||||
}
|
||||
|
||||
(window as any).AlphaFoldPAEExample = new AlphaFoldPAEExample();
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alex Chan <smalldirkalex@gmail.com>
|
||||
*
|
||||
* Thanks to @author Adam Midlik <midlik@gmail.com> for the example code ../image-renderer and https://github.com/midlik/surface-calculator i can make reference to,
|
||||
*
|
||||
* Example command-line application generating and exporting PubChem SDF structures
|
||||
* Build: npm install --no-save gl // these packages are not listed in dependencies for performance reasons
|
||||
* npm run build
|
||||
* Run: node lib/commonjs/examples/glb-export 2519 ../outputs_2519/
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import gl from 'gl';
|
||||
|
||||
import { Task } from '../../mol-task';
|
||||
import { Download } from '../../mol-plugin-state/transforms/data';
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { GlbExporter } from '../../extensions/geo-export/glb-exporter';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { ModelFromTrajectory, StructureFromModel, TrajectoryFromSDF } 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 } from '../../mol-plugin/util/headless-screenshot';
|
||||
import { setFSModule } from '../../mol-util/data-source';
|
||||
|
||||
setFSModule(fs);
|
||||
|
||||
// cid `2519` for Caffeine
|
||||
interface Args {
|
||||
cid: string,
|
||||
outDirectory: string
|
||||
}
|
||||
|
||||
function parseArguments(): Args {
|
||||
const parser = new ArgumentParser({ description: 'Example command-line application exporting .glb file of SDF structures from PubChem' });
|
||||
parser.add_argument('cid', { help: 'PubChem identifier' });
|
||||
parser.add_argument('outDirectory', { help: 'Directory for outputs' });
|
||||
const args = parser.parse_args();
|
||||
return { ...args };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArguments();
|
||||
const root = 'https://pubchem.ncbi.nlm.nih.gov/rest';
|
||||
const url = `${root}/pug/compound/cid/${args.cid}/sdf?record_type=3d`;
|
||||
|
||||
console.log('PubChem CID:', args.cid);
|
||||
console.log('Source URL:', url);
|
||||
console.log('Outputs:', args.outDirectory);
|
||||
|
||||
// Create a headless plugin
|
||||
const externalModules: ExternalModules = { gl };
|
||||
const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec());
|
||||
await plugin.init();
|
||||
|
||||
// Download and visualize data in the plugin
|
||||
const update = plugin.build();
|
||||
const structure = await update.toRoot()
|
||||
.apply(Download, { url, isBinary: false })
|
||||
.apply(TrajectoryFromSDF)
|
||||
.apply(ModelFromTrajectory)
|
||||
.apply(StructureFromModel)
|
||||
.apply(StructureRepresentation3D, {
|
||||
type: { name: 'ball-and-stick', params: { size: 'physical' } },
|
||||
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
|
||||
sizeTheme: { name: 'physical', params: {} },
|
||||
})
|
||||
.commit();
|
||||
|
||||
const meshes = structure.data!.repr.renderObjects.filter(obj => obj.type === 'mesh') as GraphicsRenderObject<'mesh'>[];
|
||||
|
||||
const boundingSphere = plugin.canvas3d?.boundingSphereVisible!;
|
||||
const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
|
||||
|
||||
const renderObjectExporter = new GlbExporter(boundingBox);
|
||||
|
||||
await plugin.runTask(Task.create('Export Geometry', async ctx => {
|
||||
for (let i = 0, il = meshes.length; i < il; ++i) {
|
||||
await renderObjectExporter.add(meshes[i], plugin.canvas3d?.webgl!, ctx);
|
||||
}
|
||||
|
||||
const blob = await renderObjectExporter.getBlob(ctx);
|
||||
const buffer = await blob.arrayBuffer();
|
||||
await fs.promises.writeFile(path.join(args.outDirectory, `${args.cid}.glb`), Buffer.from(buffer));
|
||||
}));
|
||||
|
||||
// Cleanup
|
||||
await plugin.clear();
|
||||
plugin.dispose();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -28,15 +28,17 @@ const Canvas3DPresets = {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
samples: 24,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
blurStepStart: 0,
|
||||
blurStepSize: 2,
|
||||
blurDepthBias: 0.5,
|
||||
blurNormalBias: 0.0,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
transparentThreshold: 0.4,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -67,33 +67,13 @@ export const MembraneOrientationProvider: CustomStructureProperty.Provider<Membr
|
||||
type: 'root',
|
||||
defaultParams: MembraneOrientationParams,
|
||||
getParams: (data: Structure) => MembraneOrientationParams,
|
||||
isApplicable,
|
||||
isApplicable: (data: Structure) => true,
|
||||
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<MembraneOrientationProps>) => {
|
||||
const p = { ...PD.getDefaultValues(MembraneOrientationParams), ...props };
|
||||
try {
|
||||
return { value: await computeAnvil(ctx, data, p) };
|
||||
} catch (e) {
|
||||
// the "Residues Embedded in Membrane" symbol may bypass isApplicable() checks
|
||||
console.warn('Failed to predict membrane orientation. This happens for short peptides and entries without amino acids.');
|
||||
return { value: undefined };
|
||||
}
|
||||
return { value: await computeAnvil(ctx, data, p) };
|
||||
}
|
||||
});
|
||||
|
||||
function isApplicable(structure: Structure) {
|
||||
if (!structure.isAtomic) return false;
|
||||
|
||||
for (const model of structure.models) {
|
||||
const { byEntityKey } = model.sequence;
|
||||
for (const key of Object.keys(byEntityKey)) {
|
||||
const { kind, length } = byEntityKey[+key].sequence;
|
||||
if (kind !== 'protein') continue; // can only process protein chains
|
||||
if (length >= 15) return true; // short peptides might fail
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function computeAnvil(ctx: CustomProperty.Context, data: Structure, props: Partial<ANVILProps>): Promise<MembraneOrientation> {
|
||||
const p = { ...PD.getDefaultValues(ANVILParams), ...props };
|
||||
return await computeANVIL(data, p).runInContext(ctx.runtime);
|
||||
|
||||
@@ -81,7 +81,7 @@ export const MembraneOrientationRepresentationProvider = StructureRepresentation
|
||||
defaultValues: PD.getDefaultValues(MembraneOrientationParams),
|
||||
defaultColorTheme: { name: 'shape-group' },
|
||||
defaultSizeTheme: { name: 'shape-group' },
|
||||
isApplicable(structure: Structure) { return MembraneOrientationProvider.isApplicable(structure); },
|
||||
isApplicable: (structure: Structure) => structure.elementCount > 0,
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, structure: Structure) => MembraneOrientationProvider.attach(ctx, structure, void 0, true),
|
||||
detach: (data) => MembraneOrientationProvider.ref(data, false)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2021-24 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>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -18,12 +17,6 @@ import { cantorPairing } from '../../../mol-data/util';
|
||||
import { QmeanScoreColorThemeProvider } from './color/qmean';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
import { MAPairwiseScorePlotPanel } from './pairwise/ui';
|
||||
import { PluginConfigItem } from '../../../mol-plugin/config';
|
||||
|
||||
export const MAQualityAssessmentConfig = {
|
||||
EnablePairwiseScorePlot: new PluginConfigItem('ma-quality-assessment-prop.enable-pairwise-score-plot', true),
|
||||
};
|
||||
|
||||
export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
|
||||
name: 'ma-quality-assessment-prop',
|
||||
@@ -59,10 +52,6 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
|
||||
|
||||
this.ctx.builders.structure.representation.registerPreset(QualityAssessmentPLDDTPreset);
|
||||
this.ctx.builders.structure.representation.registerPreset(QualityAssessmentQmeanPreset);
|
||||
|
||||
if (this.ctx.config.get(MAQualityAssessmentConfig.EnablePairwiseScorePlot)) {
|
||||
this.ctx.customStructureControls.set('ma-quality-assessment-pairwise-plot', MAPairwiseScorePlotPanel as any);
|
||||
}
|
||||
}
|
||||
|
||||
update(p: { autoAttach: boolean, showTooltip: boolean }) {
|
||||
@@ -87,8 +76,6 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
|
||||
|
||||
this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentPLDDTPreset);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentQmeanPreset);
|
||||
|
||||
this.ctx.customStructureControls.delete('ma-quality-assessment-pairwise-plot');
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Model, ResidueIndex } from '../../../../mol-model/structure';
|
||||
import { AtomicHierarchy } from '../../../../mol-model/structure/model/properties/atomic';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { QualityAssessment } from '../prop';
|
||||
|
||||
|
||||
const DefaultMetricColorRange = [0x00441B, 0xF7FCF5] as [Color, Color];
|
||||
|
||||
export type MAResidueRangeInfo = { startOffset: number, endOffset: number, label: string };
|
||||
|
||||
function drawMetricPNG(model: Model, metric: QualityAssessment.Pairwise, colorRange: [Color, Color], noDataColor: Color) {
|
||||
const [minResidueIndex, maxResidueIndex] = metric.residueRange;
|
||||
const [minMetric, maxMetric] = metric.valueRange;
|
||||
const [minColor, maxColor] = colorRange;
|
||||
const range = maxResidueIndex - minResidueIndex + 1;
|
||||
const valueRange = maxMetric - minMetric;
|
||||
const values = metric.values;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = range;
|
||||
canvas.height = range;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.fillStyle = Color.toStyle(noDataColor);
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let rA = minResidueIndex; rA <= maxResidueIndex; rA++) {
|
||||
const row = values[rA];
|
||||
if (!row) continue;
|
||||
|
||||
for (let rB = minResidueIndex; rB <= maxResidueIndex; rB++) {
|
||||
const value = row[rB];
|
||||
if (typeof value !== 'number') continue;
|
||||
|
||||
const x = rA - minResidueIndex;
|
||||
const y = rB - minResidueIndex;
|
||||
const t = (value - minMetric) / valueRange;
|
||||
|
||||
const color = Color.interpolate(minColor, maxColor, t);
|
||||
ctx.fillStyle = Color.toStyle(color);
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
ctx.fillRect(y, x, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const chains: MAResidueRangeInfo[] = [];
|
||||
const hierarchy = model.atomicHierarchy;
|
||||
const { label_asym_id } = hierarchy.chains;
|
||||
|
||||
let cI = AtomicHierarchy.residueChainIndex(hierarchy, minResidueIndex as ResidueIndex);
|
||||
let currentChain: MAResidueRangeInfo = { startOffset: 0, endOffset: 1, label: label_asym_id.value(cI) };
|
||||
chains.push(currentChain);
|
||||
|
||||
for (let i = 1; i < range; i++) {
|
||||
cI = AtomicHierarchy.residueChainIndex(hierarchy, (minResidueIndex + i) as ResidueIndex);
|
||||
const asym_id = label_asym_id.value(cI);
|
||||
if (asym_id === currentChain.label) {
|
||||
currentChain.endOffset = i + 1;
|
||||
} else {
|
||||
currentChain = { startOffset: i, endOffset: i + 1, label: asym_id };
|
||||
chains.push(currentChain);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
metric,
|
||||
chains,
|
||||
colorRange: [Color.toStyle(colorRange[0]), Color.toStyle(colorRange[1])] as const,
|
||||
png: canvas.toDataURL('png')
|
||||
};
|
||||
}
|
||||
|
||||
export function maDrawPairwiseMetricPNG(model: Model, metric: QualityAssessment.Pairwise) {
|
||||
return drawMetricPNG(model, metric, DefaultMetricColorRange, Color(0xE2E2E2));
|
||||
}
|
||||
|
||||
export type MAPairwiseMetricDrawing = ReturnType<typeof drawMetricPNG>
|
||||
@@ -1,504 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { CSSProperties, Fragment, memo, ReactNode, useEffect, useRef } from 'react';
|
||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, throttleTime } from 'rxjs';
|
||||
import { clamp } from '../../../../mol-math/interpolate';
|
||||
import { Model, ResidueIndex, StructureElement, StructureProperties, StructureQuery } from '../../../../mol-model/structure';
|
||||
import { AtomicHierarchy } from '../../../../mol-model/structure/model/properties/atomic';
|
||||
import { atoms } from '../../../../mol-model/structure/query/queries/generators';
|
||||
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
|
||||
import { OverpaintStructureRepresentation3DFromBundle } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { CollapsableControls, CollapsableState } from '../../../../mol-plugin-ui/base';
|
||||
import { ScatterPlotSvg } from '../../../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../../../mol-plugin-ui/controls/parameters';
|
||||
import { useBehavior } from '../../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
import { StateBuilder, StateTransform } from '../../../../mol-state';
|
||||
import { round } from '../../../../mol-util';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { SingleAsyncQueue } from '../../../../mol-util/single-async-queue';
|
||||
import { QualityAssessment } from '../prop';
|
||||
import { maDrawPairwiseMetricPNG, MAPairwiseMetricDrawing } from './plot';
|
||||
|
||||
type State = ReturnType<typeof getPropsAndValues>
|
||||
|
||||
export class MAPairwiseScorePlotPanel extends CollapsableControls<{}, State> {
|
||||
protected defaultState(): State & CollapsableState {
|
||||
return {
|
||||
header: 'Predicted Aligned Error',
|
||||
isCollapsed: false,
|
||||
isHidden: true,
|
||||
brand: { accent: 'purple', svg: ScatterPlotSvg },
|
||||
params: {} as any,
|
||||
values: undefined as any,
|
||||
dataSources: [],
|
||||
};
|
||||
}
|
||||
|
||||
toggleCollapsed() {
|
||||
if (!this.state.isCollapsed) {
|
||||
this.setState({ isCollapsed: true });
|
||||
} else {
|
||||
const state = getPropsAndValues(this.plugin, this.state.values);
|
||||
this.setState({
|
||||
...state,
|
||||
isCollapsed: false,
|
||||
isHidden: state.params.data.options.length === 0 || state.params.model.options.length === 0
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
interactivity = new BehaviorSubject<PlotInteractivityState>({});
|
||||
queue = new SingleAsyncQueue();
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribe(combineLatest([
|
||||
this.plugin.state.data.events.changed,
|
||||
this.plugin.behaviors.state.isAnimating
|
||||
]), ([_, anim]) => {
|
||||
if (anim || this.state.isCollapsed) return;
|
||||
const state = getPropsAndValues(this.plugin, this.state.values);
|
||||
this.setState({
|
||||
...state,
|
||||
isHidden: state.params.data.options.length === 0 || state.params.model.options.length === 0
|
||||
});
|
||||
});
|
||||
|
||||
this.subscribe(filterHighlightState(this.interactivity), state => {
|
||||
highlightState(this.plugin, state);
|
||||
});
|
||||
this.subscribe(filterOverpaintState(this.interactivity), state => {
|
||||
this.queue.enqueue(() => overpaintState(this.plugin, state));
|
||||
});
|
||||
}
|
||||
|
||||
protected renderControls(): JSX.Element | null {
|
||||
const { params, values, dataSources } = this.state;
|
||||
return <>
|
||||
<ParameterControls params={params} values={values} onChangeValues={values => this.setState({ values })} />
|
||||
<PlotWrapper plugin={this.plugin} values={values} dataSources={dataSources} interactivity={this.interactivity} />
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export function MAPairwiseScorePlot({ plugin, model, pairwiseMetric }: { plugin: PluginContext, model: Model, pairwiseMetric: QualityAssessment.Pairwise }) {
|
||||
const _interactivity = useRef<BehaviorSubject<PlotInteractivityState>>();
|
||||
const interactivity = _interactivity.current ??= new BehaviorSubject<PlotInteractivityState>({});
|
||||
|
||||
useEffect(() => {
|
||||
const queue = new SingleAsyncQueue();
|
||||
|
||||
const highlight = filterHighlightState(interactivity).subscribe(state => highlightState(plugin, state));
|
||||
const paint = filterOverpaintState(interactivity).subscribe(state => queue.enqueue(() => overpaintState(plugin, state)));
|
||||
|
||||
return () => {
|
||||
highlight.unsubscribe();
|
||||
paint.unsubscribe();
|
||||
queue.enqueue(() => overpaintState(plugin, interactivity.value));
|
||||
};
|
||||
}, [model, pairwiseMetric]);
|
||||
|
||||
return <MAPairwiseScorePlotBase model={model} pairwiseMetric={pairwiseMetric} interactivity={interactivity} />;
|
||||
}
|
||||
|
||||
function filterHighlightState(state: BehaviorSubject<PlotInteractivityState>) {
|
||||
return state.pipe(
|
||||
throttleTime(16, undefined, { leading: true, trailing: true }),
|
||||
distinctUntilChanged((a, b) => a.crosshairOffset === b.crosshairOffset)
|
||||
);
|
||||
}
|
||||
|
||||
function filterOverpaintState(state: BehaviorSubject<PlotInteractivityState>) {
|
||||
return state.pipe(
|
||||
throttleTime(66, undefined, { leading: true, trailing: true }),
|
||||
distinctUntilChanged((a, b) => a.boxStart === b.boxStart && (a.mouseDown ? a.crosshairOffset : a.boxEnd) === (b.mouseDown ? b.crosshairOffset : b.boxEnd))
|
||||
);
|
||||
}
|
||||
|
||||
const PlotWrapper = memo(({ plugin, values, dataSources, interactivity }: { plugin: PluginContext, values: State['values'], dataSources: State['dataSources'], interactivity: BehaviorSubject<PlotInteractivityState> }) => {
|
||||
const model: Model | undefined = plugin.managers.structure.hierarchy.current.models.find(m => m.cell.transform.ref === values.model)?.cell.obj?.data;
|
||||
const src = dataSources.find(src => src.id === values.data);
|
||||
const cif: PluginStateObject.Format.Cif | undefined = plugin.state.data.cells.get(src?.dataRef!)?.obj;
|
||||
const block = cif?.data.blocks[src?.blockIndex!];
|
||||
|
||||
if (!model || !block || !src) return <div className='msp-description'>Data not available</div>;
|
||||
|
||||
const metric = QualityAssessment.pairwiseMetricFromModelArchiveCIF(model, block, src.metridId);
|
||||
if (!metric) return <div className='msp-description'>Data not available</div>;
|
||||
|
||||
return <MAPairwiseScorePlotBase interactivity={interactivity} model={model} pairwiseMetric={metric} />;
|
||||
}, (prev, next) => prev.values.data === next.values.data && prev.values.model === next.values.model);
|
||||
|
||||
function getPropsAndValues(plugin: PluginContext, current?: { model?: string, data?: string }) {
|
||||
const models = plugin.managers.structure.hierarchy.current.models;
|
||||
const cifs = plugin.state.data.selectQ(q => q.root.subtree().ofType(PluginStateObject.Format.Cif));
|
||||
|
||||
const dataSources: {
|
||||
id: string,
|
||||
label: string,
|
||||
metridId: number,
|
||||
dataRef: StateTransform.Ref,
|
||||
blockIndex: number,
|
||||
}[] = [];
|
||||
|
||||
for (const cif of cifs) {
|
||||
if (!cif.obj?.data.blocks) continue;
|
||||
let blockIndex = 0;
|
||||
for (const block of cif.obj.data.blocks) {
|
||||
for (const pae of QualityAssessment.findModelArchiveCIFPAEMetrics(block)) {
|
||||
dataSources.push({
|
||||
id: `${cif.transform.ref}:${blockIndex}:${pae.id}`,
|
||||
metridId: pae.id,
|
||||
label: `${block.header}: ${pae.name}`,
|
||||
dataRef: cif.transform.ref,
|
||||
blockIndex,
|
||||
});
|
||||
}
|
||||
blockIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
model: PD.Select(models[0]?.cell.transform.ref, models.map(m => [m.cell.transform.ref, m.cell.obj?.data.label!]), { isHidden: models.length <= 1 }),
|
||||
data: PD.Select(dataSources[0]?.id, dataSources.map(o => [o.id, o.label]), { isHidden: dataSources.length <= 1 })
|
||||
};
|
||||
|
||||
const values = {
|
||||
model: params.model.options.find(o => o[0] === current?.model)?.[0] ?? params.model.options[0]?.[0],
|
||||
data: params.data.options.find(o => o[0] === current?.data)?.[0] ?? params.data.options[0]?.[0],
|
||||
};
|
||||
|
||||
return { params, values, dataSources };
|
||||
}
|
||||
|
||||
const PlotSize = 1000;
|
||||
const PlotOffset = 120;
|
||||
|
||||
const PlotColors = {
|
||||
ScoredOverpaint: Color(0xFFA500),
|
||||
ScoredLabel: Color(0xBC7100),
|
||||
AlignedOverpaint: Color(0x1AFFBB),
|
||||
AlignedLabel: Color(0x0F8E68),
|
||||
};
|
||||
|
||||
interface PlotInteractivityState {
|
||||
model?: Model;
|
||||
drawing?: MAPairwiseMetricDrawing;
|
||||
crosshairOffset?: [number, number];
|
||||
inside?: boolean;
|
||||
mouseDown?: boolean;
|
||||
boxStart?: [number, number];
|
||||
boxEnd?: [number, number];
|
||||
}
|
||||
|
||||
export const MAPairwiseScorePlotBase = memo(({ model, pairwiseMetric, interactivity }: { model: Model, pairwiseMetric: QualityAssessment.Pairwise, interactivity: BehaviorSubject<PlotInteractivityState> }) => {
|
||||
const interactivityRect = useRef<SVGRectElement>();
|
||||
const drawing = maDrawPairwiseMetricPNG(model, pairwiseMetric);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawing) {
|
||||
interactivity.next({});
|
||||
return;
|
||||
}
|
||||
interactivity.next({ model, drawing });
|
||||
const moveEvent = (ev: MouseEvent) => {
|
||||
const current = interactivity.value;
|
||||
if (!current.inside && !current.mouseDown) return;
|
||||
|
||||
const offset = getPlotMouseOffsetBase(interactivityRect.current!, ev.clientX, ev.clientY);
|
||||
interactivity.next({ ...current, crosshairOffset: offset });
|
||||
};
|
||||
const mouseUpEvent = (ev: MouseEvent) => {
|
||||
if (!interactivity.value.mouseDown) return;
|
||||
const offset = getPlotMouseOffsetBase(interactivityRect.current!, ev.clientX, ev.clientY);
|
||||
interactivity.next({ ...interactivity.value, mouseDown: false, boxEnd: offset });
|
||||
};
|
||||
window.addEventListener('mousemove', moveEvent);
|
||||
window.addEventListener('mouseup', mouseUpEvent);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', moveEvent);
|
||||
window.removeEventListener('mouseup', mouseUpEvent);
|
||||
};
|
||||
}, [model, interactivity, drawing]);
|
||||
|
||||
if (!drawing) return <>Not available</>;
|
||||
|
||||
|
||||
const { metric, colorRange, chains, png } = drawing;
|
||||
const nResidues = metric.residueRange[1] - metric.residueRange[0];
|
||||
|
||||
const border = '#333';
|
||||
const line = '#000';
|
||||
|
||||
const legendHeight = 80;
|
||||
const legendOffsetY = PlotOffset + PlotSize + 50;
|
||||
|
||||
const viewBox = '0 0 1140 1270';
|
||||
|
||||
return <div style={{ margin: '8px 8px 0 8px', position: 'relative' }}>
|
||||
<svg viewBox={viewBox} width='100%'>
|
||||
<image x={PlotOffset + 1} y={PlotOffset + 1} width={PlotSize - 1} height={PlotSize - 1} href={png} />
|
||||
<line x1={PlotOffset} x2={PlotOffset + PlotSize} y1={PlotOffset} y2={PlotOffset + PlotSize} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<linearGradient id='legend-gradient' x1={0} x2={1} y1={0} y2={0}>
|
||||
<stop offset='0%' stopColor={colorRange[0]} />
|
||||
<stop offset='100%' stopColor={colorRange[1]} />
|
||||
</linearGradient>
|
||||
<rect x={PlotOffset} y={legendOffsetY} width={PlotSize} height={legendHeight} style={{ fill: 'url(#legend-gradient)', strokeWidth: 1, stroke: border }} />
|
||||
<text x={PlotOffset + 20} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'white', fontWeight: 'bold' }}>{round(metric.valueRange[0], 2)} Å</text>
|
||||
<text x={PlotOffset + PlotSize - 20} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'black', fontWeight: 'bold' }} textAnchor='end'>{round(metric.valueRange[1], 2)} Å</text>
|
||||
<text x={PlotOffset + PlotSize / 2} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'black' }} textAnchor='middle'>Predicted Aligned Error</text>
|
||||
|
||||
<text x={PlotOffset + PlotSize / 2} y={50} style={{ fontSize: '45px', fontWeight: 'bold', fill: Color.toStyle(PlotColors.ScoredLabel) }} textAnchor='middle'>Scored Residue</text>
|
||||
<text className='msp-svg-text' style={{ fontSize: '50px', fontWeight: 'bold', fill: Color.toStyle(PlotColors.AlignedLabel) }} transform={`translate(50, ${PlotOffset + PlotSize / 2}) rotate(270)`} textAnchor='middle'>Aligned Residue</text>
|
||||
|
||||
{chains.map(({ startOffset, endOffset, label }) => {
|
||||
const textOffset = PlotOffset + PlotSize * (startOffset + (endOffset - startOffset) / 2) / nResidues;
|
||||
const endLineOffset = PlotOffset + PlotSize * endOffset / nResidues;
|
||||
const startLineOffset = PlotOffset + PlotSize * startOffset / nResidues;
|
||||
|
||||
const seq_id = model.atomicHierarchy.residues.label_seq_id;
|
||||
const startIndex = seq_id.value(metric.residueRange[0] + startOffset);
|
||||
const endIndex = seq_id.value(metric.residueRange[0] + endOffset - 1);
|
||||
|
||||
return <Fragment key={startOffset}>
|
||||
<text x={textOffset} y={PlotOffset - 15} className='msp-svg-text' style={{ fontSize: '40px' }} textAnchor='middle'>{label} {startIndex}-{endIndex}</text>
|
||||
<text className='msp-svg-text' style={{ fontSize: '40px' }} transform={`translate(${PlotOffset - 15}, ${textOffset}) rotate(270)`} textAnchor='middle'>{label} {startIndex}-{endIndex}</text>
|
||||
<line x1={startLineOffset} x2={startLineOffset} y1={PlotOffset - 20} y2={PlotOffset + PlotSize + 20} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<line x1={endLineOffset} x2={endLineOffset} y1={PlotOffset - 20} y2={PlotOffset + PlotSize + 20} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<line x1={PlotOffset - 20} x2={PlotOffset + PlotSize + 20} y1={startLineOffset} y2={startLineOffset} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
<line x1={PlotOffset - 20} x2={PlotOffset + PlotSize + 20} y1={endLineOffset} y2={endLineOffset} style={{ stroke: line, strokeDasharray: '15,15' }} />
|
||||
</Fragment>;
|
||||
})}
|
||||
</svg>
|
||||
<svg viewBox={viewBox} style={{ position: 'absolute', inset: 0 }}>
|
||||
<rect x={PlotOffset} y={PlotOffset} width={PlotSize} height={PlotSize} style={{ fill: 'transparent', cursor: 'crosshair' }}
|
||||
ref={interactivityRect as any}
|
||||
onMouseMove={(ev) => {
|
||||
interactivity.next({ ...interactivity.value, inside: true });
|
||||
ev.currentTarget.style.stroke = 'black';
|
||||
ev.currentTarget.style.strokeWidth = '4px';
|
||||
}}
|
||||
onMouseDown={(ev) => {
|
||||
interactivity.next({ ...interactivity.value, mouseDown: true, boxStart: getPlotMouseOffset(ev) });
|
||||
}}
|
||||
onMouseLeave={(ev) => {
|
||||
interactivity.next({ ...interactivity.value, inside: false, crosshairOffset: undefined });
|
||||
ev.currentTarget.style.stroke = '#333';
|
||||
ev.currentTarget.style.strokeWidth = '1px';
|
||||
}} />
|
||||
<PlotInteractivity drawing={drawing} interactity={interactivity} />
|
||||
</svg>
|
||||
</div>;
|
||||
}, (prev, next) => prev.model === next.model && prev.pairwiseMetric === next.pairwiseMetric);
|
||||
|
||||
function PlotInteractivity({ drawing, interactity }: { drawing: MAPairwiseMetricDrawing, interactity: BehaviorSubject<PlotInteractivityState> }) {
|
||||
const state = useBehavior(interactity);
|
||||
const { crosshairOffset, inside } = state;
|
||||
const box = getBox(state);
|
||||
const label = getCrosshairLabel(state);
|
||||
|
||||
let labelNode: ReactNode | undefined;
|
||||
if (label) {
|
||||
const labelStyle: CSSProperties | undefined = label ? { fontSize: '45px', fill: 'black', fontWeight: 'bold', pointerEvents: 'none', userSelect: 'none' } : undefined;
|
||||
let x: number, y: number, anchor: string;
|
||||
if (crosshairOffset![0] < PlotSize / 2) {
|
||||
x = PlotOffset + crosshairOffset![0] + 20;
|
||||
anchor = 'start';
|
||||
} else {
|
||||
x = PlotOffset + crosshairOffset![0] - 20;
|
||||
anchor = 'end';
|
||||
}
|
||||
|
||||
if (crosshairOffset![1] < PlotSize / 2) {
|
||||
y = PlotOffset + crosshairOffset![1] + 65;
|
||||
} else {
|
||||
y = PlotOffset + crosshairOffset![1] - (label[2] ? 3 * 45 : 2 * 45) + 20;
|
||||
}
|
||||
|
||||
labelNode = <text y={y} style={labelStyle} textAnchor={anchor}>
|
||||
<tspan x={x}>S: {label[0]}</tspan>
|
||||
<tspan x={x} dy={45}>A: {label[1]}</tspan>
|
||||
{label[2] && <tspan x={x} dy={45}>{label[2]}</tspan>}
|
||||
</text>;
|
||||
}
|
||||
|
||||
return <>
|
||||
{inside && crosshairOffset && <line x1={crosshairOffset[0] + PlotOffset} x2={crosshairOffset[0] + PlotOffset} y1={PlotOffset} y2={PlotOffset + PlotSize} style={{ pointerEvents: 'none', stroke: 'black', strokeDasharray: '5,5' }} />}
|
||||
{inside && crosshairOffset && <line x1={PlotOffset} x2={PlotOffset + PlotSize} y1={crosshairOffset[1] + PlotOffset} y2={crosshairOffset[1] + PlotOffset} style={{ pointerEvents: 'none', stroke: 'black', strokeDasharray: '5,5' }} />}
|
||||
{box && <rect x={PlotOffset + box[0]} y={PlotOffset + box[1]} width={box[2]} height={box[3]} style={{ stroke: '#eee', strokeWidth: 4, fill: 'rgba(0, 0, 0, 0.15)', pointerEvents: 'none' }} />}
|
||||
{labelNode}
|
||||
</>;
|
||||
}
|
||||
|
||||
function getCrosshairLabel(state: PlotInteractivityState) {
|
||||
if (!state.drawing || !state.crosshairOffset || !state.inside) return;
|
||||
|
||||
const { drawing } = state;
|
||||
const rA = getResidueIndex(drawing, clamp(state.crosshairOffset[0], 0, PlotSize));
|
||||
const rB = getResidueIndex(drawing, clamp(state.crosshairOffset[1], 0, PlotSize));
|
||||
|
||||
const value = drawing.metric.values[rA]?.[rB] ?? drawing.metric.values[rB]?.[rA];
|
||||
const valueLabel = typeof value === 'number' ? `${round(value, 2)} Å` : '';
|
||||
|
||||
return [getResidueLabel(drawing, rA), getResidueLabel(drawing, rB), valueLabel];
|
||||
}
|
||||
|
||||
function getResidueIndex(drawing: MAPairwiseMetricDrawing, offset: number) {
|
||||
const rI = drawing.metric.residueRange[0] + Math.round(offset / PlotSize * (drawing.metric.residueRange[1] - drawing.metric.residueRange[0] + 1)) as ResidueIndex;
|
||||
return clamp(rI, drawing.metric.residueRange[0], drawing.metric.residueRange[1]) as ResidueIndex;
|
||||
}
|
||||
|
||||
function getResidueLabel(drawing: MAPairwiseMetricDrawing, rI: ResidueIndex) {
|
||||
const hierarchy = drawing.model.atomicHierarchy;
|
||||
const asym_id = hierarchy.chains.label_asym_id;
|
||||
const seq_id = hierarchy.residues.label_seq_id;
|
||||
const comp_id = hierarchy.atoms.label_comp_id;
|
||||
|
||||
return `${asym_id.value(AtomicHierarchy.residueChainIndex(hierarchy, rI))} ${seq_id.value(rI)} ${comp_id.value(AtomicHierarchy.residueFirstAtomIndex(hierarchy, rI))}`;
|
||||
}
|
||||
|
||||
function getBox(state: PlotInteractivityState) {
|
||||
const start = state.boxStart;
|
||||
const end = state.mouseDown ? state.crosshairOffset : state.boxEnd;
|
||||
if (!start || !end) return undefined;
|
||||
|
||||
const x = clamp(Math.min(start[0], end[0]), 0, PlotSize);
|
||||
const width = clamp(Math.max(start[0], end[0]), 0, PlotSize) - x;
|
||||
const y = clamp(Math.min(start[1], end[1]), 0, PlotSize);
|
||||
const height = clamp(Math.max(start[1], end[1]), 0, PlotSize) - y;
|
||||
|
||||
if (width < 1 && height < 1) return undefined;
|
||||
|
||||
return [x, y, width, height];
|
||||
}
|
||||
|
||||
function getPlotMouseOffset(ev: React.MouseEvent<SVGRectElement, MouseEvent>) {
|
||||
return getPlotMouseOffsetBase(ev.currentTarget, ev.clientX, ev.clientY);
|
||||
}
|
||||
|
||||
function getPlotMouseOffsetBase(target: HTMLElement | SVGRectElement, clientX: number, clientY: number) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const offsetX = PlotSize * (clientX - rect.left) / rect.width;
|
||||
const offsetY = PlotSize * (clientY - rect.top) / rect.height;
|
||||
return [offsetX, offsetY] as [number, number];
|
||||
}
|
||||
|
||||
function findModelRef(plugin: PluginContext, model: Model | undefined) {
|
||||
if (!model) return undefined;
|
||||
for (const m of plugin.managers.structure.hierarchy.current.models) {
|
||||
if (m.cell.obj?.data === model) return m;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function highlightState(plugin: PluginContext, state: PlotInteractivityState) {
|
||||
const structure = findModelRef(plugin, state.model)?.structures[0]?.cell.obj?.data;
|
||||
if (!state.drawing || !state.crosshairOffset || !state.inside || !structure) {
|
||||
plugin.managers.interactivity.lociHighlights.clearHighlights();
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawing } = state;
|
||||
const rA = getResidueIndex(drawing, clamp(state.crosshairOffset[0], 0, PlotSize));
|
||||
const rB = getResidueIndex(drawing, clamp(state.crosshairOffset[1], 0, PlotSize));
|
||||
|
||||
const resIdx = StructureProperties.residue.key;
|
||||
const loci = StructureQuery.loci(atoms({
|
||||
residueTest: ctx => {
|
||||
const rI = resIdx(ctx.element);
|
||||
return rI === rA || rI === rB;
|
||||
},
|
||||
}), structure);
|
||||
|
||||
plugin.managers.interactivity.lociHighlights.highlightOnly({ loci });
|
||||
}
|
||||
|
||||
async function overpaintState(plugin: PluginContext, state: PlotInteractivityState) {
|
||||
const tag = 'modelarchive-pae-overpaint';
|
||||
|
||||
const overpaints = plugin.state.data.selectQ(q => q.root.subtree().withTag(tag));
|
||||
const update = plugin.build();
|
||||
for (const overpaint of overpaints) update.delete(overpaint);
|
||||
|
||||
const model = findModelRef(plugin, state.model);
|
||||
const structure = model?.structures[0]?.cell.obj?.data;
|
||||
if (!state.drawing || !state.boxStart || !(state.boxEnd || state.crosshairOffset) || !structure) {
|
||||
if (!overpaints) return;
|
||||
return reApplyRepresentationStates(plugin, update);
|
||||
}
|
||||
|
||||
const start = state.boxStart;
|
||||
const end = state.mouseDown ? state.crosshairOffset! : state.boxEnd!;
|
||||
|
||||
const x0 = clamp(Math.min(start[0], end[0]), 0, PlotSize);
|
||||
const x1 = clamp(Math.max(start[0], end[0]), 0, PlotSize);
|
||||
const y0 = clamp(Math.min(start[1], end[1]), 0, PlotSize);
|
||||
const y1 = clamp(Math.max(start[1], end[1]), 0, PlotSize);
|
||||
|
||||
if (x1 - x0 <= 1 || y1 - y0 <= 1) {
|
||||
if (!overpaints) return;
|
||||
return reApplyRepresentationStates(plugin, update);
|
||||
}
|
||||
|
||||
const representations = plugin.state.data.selectQ(q =>
|
||||
q.byRef(model.cell.transform.ref!)
|
||||
.subtree()
|
||||
.ofType(PluginStateObject.Molecule.Structure.Representation3D)
|
||||
);
|
||||
|
||||
const resIdx = StructureProperties.residue.key;
|
||||
|
||||
const startScored = getResidueIndex(state.drawing, x0);
|
||||
const endScored = getResidueIndex(state.drawing, x1);
|
||||
const lociScored = StructureQuery.loci(atoms({
|
||||
residueTest: ctx => {
|
||||
const rI = resIdx(ctx.element);
|
||||
return rI >= startScored && rI <= endScored;
|
||||
},
|
||||
}), structure);
|
||||
|
||||
const startAligned = getResidueIndex(state.drawing, y0);
|
||||
const endAligned = getResidueIndex(state.drawing, y1);
|
||||
const lociAligned = StructureQuery.loci(atoms({
|
||||
residueTest: ctx => {
|
||||
const rI = resIdx(ctx.element);
|
||||
return rI >= startAligned && rI <= endAligned;
|
||||
},
|
||||
}), structure);
|
||||
|
||||
const layers = [{
|
||||
bundle: StructureElement.Bundle.fromSubStructure(structure, structure),
|
||||
color: Color(0x777777),
|
||||
clear: false,
|
||||
}, {
|
||||
bundle: StructureElement.Bundle.fromLoci(lociScored),
|
||||
color: PlotColors.ScoredOverpaint,
|
||||
clear: false,
|
||||
}, {
|
||||
bundle: StructureElement.Bundle.fromLoci(lociAligned),
|
||||
color: PlotColors.AlignedOverpaint,
|
||||
clear: false,
|
||||
}];
|
||||
|
||||
for (const repr of representations) {
|
||||
update.to(repr).apply(OverpaintStructureRepresentation3DFromBundle, { layers }, { tags: [tag], state: { isGhost: true } });
|
||||
}
|
||||
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
async function reApplyRepresentationStates(plugin: PluginContext, update: StateBuilder.Root) {
|
||||
await update.commit();
|
||||
const states = plugin.state.data.selectQ(q => q.root.subtree().ofType(PluginStateObject.Molecule.Structure.Representation3DState));
|
||||
for (const state of states) {
|
||||
const data = state.obj?.data;
|
||||
if (!data) continue;
|
||||
data.repr.setState(data.state);
|
||||
plugin.canvas3d?.update(data.repr);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2021-24 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>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { CifFrame } from '../../../mol-io/reader/cif';
|
||||
import { toDatabase } from '../../../mol-io/reader/cif/schema';
|
||||
import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Unit } from '../../../mol-model/structure';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
|
||||
import { Model, ResidueIndex } from '../../../mol-model/structure/model';
|
||||
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
|
||||
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
|
||||
import { CustomPropSymbol } from '../../../mol-script/language/symbol';
|
||||
import { Type } from '../../../mol-script/language/type';
|
||||
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { 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 };
|
||||
|
||||
@@ -29,19 +26,10 @@ interface QualityAssessment {
|
||||
}
|
||||
|
||||
namespace QualityAssessment {
|
||||
export interface Pairwise {
|
||||
id: number
|
||||
name: string
|
||||
|
||||
residueRange: [ResidueIndex, ResidueIndex]
|
||||
valueRange: [number, number]
|
||||
values: Record<ResidueIndex, Record<ResidueIndex, number | undefined> | undefined>
|
||||
}
|
||||
|
||||
const Empty = {
|
||||
value: {
|
||||
localMetrics: new Map(),
|
||||
} satisfies QualityAssessment
|
||||
localMetrics: new Map()
|
||||
}
|
||||
};
|
||||
|
||||
export function isApplicable(model?: Model, localMetricName?: 'pLDDT' | 'qmean'): boolean {
|
||||
@@ -118,101 +106,6 @@ namespace QualityAssessment {
|
||||
};
|
||||
}
|
||||
|
||||
const PairwiseSchema = {
|
||||
ma_qa_metric: mmCIF_Schema.ma_qa_metric,
|
||||
ma_qa_metric_local_pairwise: mmCIF_Schema.ma_qa_metric_local_pairwise
|
||||
};
|
||||
|
||||
export function findModelArchiveCIFPAEMetrics(frame: CifFrame) {
|
||||
const { ma_qa_metric, ma_qa_metric_local_pairwise } = toDatabase(PairwiseSchema, frame);
|
||||
const result: { id: number, name: string }[] = [];
|
||||
if (ma_qa_metric_local_pairwise._rowCount === 0) return result;
|
||||
|
||||
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
|
||||
if (ma_qa_metric.mode.value(i) !== 'local-pairwise') continue;
|
||||
const id = ma_qa_metric.id.value(i);
|
||||
const name = ma_qa_metric.name.value(i);
|
||||
if (!name.toLowerCase().includes('pae')) continue;
|
||||
result.push({ id, name });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function pairwiseMetricFromModelArchiveCIF(model: Model, frame: CifFrame, metricId: number): Pairwise | undefined {
|
||||
const db = toDatabase(PairwiseSchema, frame);
|
||||
if (!db.ma_qa_metric_local_pairwise._rowCount) return undefined;
|
||||
|
||||
const { ma_qa_metric, ma_qa_metric_local_pairwise } = db;
|
||||
const { model_id, label_asym_id_1, label_seq_id_1, label_asym_id_2, label_seq_id_2, metric_id, metric_value } = db.ma_qa_metric_local_pairwise;
|
||||
const { index } = model.atomicHierarchy;
|
||||
|
||||
let metric: Pairwise | undefined;
|
||||
|
||||
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
|
||||
if (ma_qa_metric.mode.value(i) !== 'local-pairwise') continue;
|
||||
const id = ma_qa_metric.id.value(i);
|
||||
if (id !== metricId) continue;
|
||||
|
||||
const name = ma_qa_metric.name.value(i);
|
||||
metric = {
|
||||
id,
|
||||
name,
|
||||
residueRange: [Number.MAX_SAFE_INTEGER as ResidueIndex, Number.MIN_SAFE_INTEGER as ResidueIndex],
|
||||
valueRange: [Number.MAX_VALUE, -Number.MAX_VALUE],
|
||||
values: {}
|
||||
};
|
||||
}
|
||||
|
||||
if (!metric) return undefined;
|
||||
|
||||
const { values, residueRange, valueRange } = metric;
|
||||
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_pairwise._rowCount; i < il; i++) {
|
||||
if (model_id.value(i) !== model.modelNum || metric_id.value(i) !== metricId) continue;
|
||||
|
||||
let labelAsymId = label_asym_id_1.value(i);
|
||||
let 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_1.value(i);
|
||||
|
||||
const rI_1 = index.findResidueLabel(residueKey);
|
||||
if (rI_1 < 0) continue;
|
||||
|
||||
labelAsymId = label_asym_id_2.value(i);
|
||||
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_2.value(i);
|
||||
|
||||
const rI_2 = index.findResidueLabel(residueKey);
|
||||
if (rI_1 < 0) continue;
|
||||
|
||||
let r1 = values[rI_1];
|
||||
if (!r1) {
|
||||
r1 = {};
|
||||
values[rI_1] = r1;
|
||||
}
|
||||
const value = metric_value.value(i);
|
||||
r1[rI_2] = value;
|
||||
|
||||
if (rI_1 < residueRange[0]) residueRange[0] = rI_1;
|
||||
if (rI_2 < residueRange[0]) residueRange[0] = rI_2;
|
||||
if (rI_1 > residueRange[1]) residueRange[1] = rI_1;
|
||||
if (rI_2 > residueRange[1]) residueRange[1] = rI_2;
|
||||
if (value < valueRange[0]) valueRange[0] = value;
|
||||
if (value > valueRange[1]) valueRange[1] = value;
|
||||
}
|
||||
|
||||
return metric;
|
||||
}
|
||||
|
||||
export const symbols = {
|
||||
pLDDT: QuerySymbolRuntime.Dynamic(CustomPropSymbol('ma', 'quality-assessment.pLDDT', Type.Num),
|
||||
ctx => {
|
||||
|
||||
@@ -6,24 +6,28 @@
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
|
||||
import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { getFocusSnapshot, getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { Loci } from '../../mol-model/loci';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { MVSPrimitivesData } from './components/primitives';
|
||||
import { decodeColor } from './helpers/utils';
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { SnapshotMetadata } from './mvs-data';
|
||||
import { MolstarNodeParams } from './tree/molstar/molstar-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
import { MVSDefaults } from './tree/mvs/mvs-defaults';
|
||||
|
||||
|
||||
const DefaultFocusOptions = {
|
||||
minRadius: 5,
|
||||
extraRadius: 0,
|
||||
extraRadiusForFocus: 0,
|
||||
extraRadiusForZoomAll: 0,
|
||||
};
|
||||
const DefaultCanvasBackgroundColor = ColorNames.white;
|
||||
|
||||
@@ -39,48 +43,72 @@ export async function suppressCameraAutoreset(plugin: PluginContext) {
|
||||
|
||||
/** Set the camera based on a camera node params. */
|
||||
export async function setCamera(plugin: PluginContext, params: MolstarNodeParams<'camera'>) {
|
||||
const snapshot = cameraParamsToCameraSnapshot(plugin, params);
|
||||
const target = Vec3.create(...params.target);
|
||||
let position = Vec3.create(...params.position);
|
||||
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
|
||||
const up = Vec3.create(...params.up);
|
||||
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
|
||||
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius: Infinity }; // `radius: Infinity` avoids clipping (ensures covering the whole scene)
|
||||
adjustSceneRadiusFactor(plugin, snapshot.target);
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
|
||||
export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: MolstarNodeParams<'camera'>): Partial<Camera.Snapshot> {
|
||||
const target = Vec3.create(...params.target);
|
||||
let position = Vec3.create(...params.position);
|
||||
const radius = Vec3.distance(target, position) / 2;
|
||||
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
|
||||
const up = Vec3.create(...params.up);
|
||||
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
|
||||
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius, radiusMax: radius };
|
||||
return snapshot;
|
||||
}
|
||||
async function focusBoundingSphere(plugin: PluginContext, params: MolstarNodeParams<'focus'>, boundingSphere: Sphere3D | undefined, extraRadius: number) {
|
||||
if (!plugin.canvas3d || !boundingSphere) return;
|
||||
|
||||
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is undefined).
|
||||
* Orient the camera based on a focus node params. **/
|
||||
export async function setFocus(plugin: PluginContext, focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]) {
|
||||
const snapshot = getFocusSnapshot(plugin, {
|
||||
...snapshotFocusInfoFromMvsFocuses(focuses),
|
||||
minRadius: DefaultFocusOptions.minRadius,
|
||||
const direction = Vec3.create(...params.direction);
|
||||
const up = Vec3.create(...params.up);
|
||||
Vec3.orthogonalize(up, direction, up);
|
||||
const snapshot = snapshotFromSphereAndDirections(plugin.canvas3d.camera, {
|
||||
center: boundingSphere.center,
|
||||
radius: boundingSphere.radius + extraRadius,
|
||||
up,
|
||||
direction,
|
||||
});
|
||||
if (!snapshot) return;
|
||||
resetSceneRadiusFactor(plugin);
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
|
||||
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]): PluginState.SnapshotFocusInfo {
|
||||
const lastFocus = (focuses.length > 0) ? focuses[focuses.length - 1] : undefined;
|
||||
const direction = lastFocus?.params.direction ?? MVSTreeSchema.nodes.focus.params.fields.direction.default;
|
||||
const up = lastFocus?.params.up ?? MVSTreeSchema.nodes.focus.params.fields.up.default;
|
||||
return {
|
||||
targets: focuses.map<PluginState.SnapshotFocusTargetInfo>(f => ({
|
||||
targetRef: f.target.ref === '-=root=-' ? undefined : f.target.ref, // need to treat root separately so it does not include invisible structure parts etc.
|
||||
radius: f.params.radius ?? undefined,
|
||||
radiusFactor: f.params.radius_factor,
|
||||
extraRadius: f.params.radius_extent,
|
||||
})),
|
||||
direction: Vec3.create(...direction),
|
||||
up: Vec3.create(...up),
|
||||
};
|
||||
function getRenderObjectsBoundary(objects: ReadonlyArray<GraphicsRenderObject>) {
|
||||
const spheres: Sphere3D[] = [];
|
||||
for (const o of objects) {
|
||||
const s = o.values.boundingSphere.ref.value;
|
||||
if (s.radius === 0) continue;
|
||||
spheres.push(s);
|
||||
}
|
||||
if (spheres.length === 0) return;
|
||||
if (spheres.length === 1) return spheres[0];
|
||||
return boundingSphereOfSpheres(spheres);
|
||||
}
|
||||
|
||||
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is null).
|
||||
* Orient the camera based on a focus node params.
|
||||
**/
|
||||
export async function setFocus(plugin: PluginContext, structureNodeSelector: StateObjectSelector | undefined, params: MolstarNodeParams<'focus'> = MVSDefaults.focus) {
|
||||
let boundingSphere: Sphere3D | undefined = undefined;
|
||||
if (structureNodeSelector) {
|
||||
const cell = plugin.state.data.cells.get(structureNodeSelector.ref);
|
||||
const data = cell?.obj?.data;
|
||||
if (!data) console.warn('Focus: no structure');
|
||||
if (data instanceof Structure) {
|
||||
boundingSphere = Loci.getBoundingSphere(Structure.Loci(data));
|
||||
} else if (PluginStateObject.isRepresentation3D(cell?.obj)) {
|
||||
boundingSphere = getRenderObjectsBoundary(cell.obj.data.repr.renderObjects);
|
||||
} else if (MVSPrimitivesData.is(cell?.obj)) {
|
||||
const representations = plugin.state.data.selectQ(q =>
|
||||
q.byRef(cell.transform.ref).subtree().filter(c => PluginStateObject.isRepresentation3D(c?.obj))
|
||||
);
|
||||
const renderObjects = representations.flatMap(r => r.obj?.data?.repr?.renderObjects ?? []);
|
||||
if (renderObjects.length) {
|
||||
boundingSphere = getRenderObjectsBoundary(renderObjects);
|
||||
}
|
||||
} else {
|
||||
console.warn('Focus: cannot apply to the specified node type');
|
||||
}
|
||||
}
|
||||
const extraRadius = boundingSphere ? DefaultFocusOptions.extraRadiusForFocus : DefaultFocusOptions.extraRadiusForZoomAll;
|
||||
boundingSphere ??= getPluginBoundingSphere(plugin);
|
||||
return focusBoundingSphere(plugin, params, boundingSphere, extraRadius);
|
||||
}
|
||||
|
||||
/** Adjust `sceneRadiusFactor` property so that the current scene is not cropped */
|
||||
@@ -98,6 +126,19 @@ function resetSceneRadiusFactor(plugin: PluginContext) {
|
||||
plugin.canvas3d?.setProps({ sceneRadiusFactor });
|
||||
}
|
||||
|
||||
/** Return camera snapshot for focusing a sphere with given `center` and `radius`,
|
||||
* while ensuring given view `direction` (aligns with vector position->target)
|
||||
* and `up` (aligns with screen Y axis). */
|
||||
function snapshotFromSphereAndDirections(camera: Camera, options: { center: Vec3, radius: number, direction: Vec3, up: Vec3 }): Partial<Camera.Snapshot> {
|
||||
// This might seem to repeat `plugin.canvas3d.camera.getFocus` but avoid flipping
|
||||
const { center, direction, up } = options;
|
||||
const radius = Math.max(options.radius, DefaultFocusOptions.minRadius);
|
||||
const distance = camera.getTargetDistance(radius);
|
||||
const deltaDirection = Vec3.setMagnitude(_tmpVec, direction, distance);
|
||||
const position = Vec3.sub(Vec3(), center, deltaDirection);
|
||||
return { target: center, position, up, radius };
|
||||
}
|
||||
|
||||
/** Return the distance adjustment ratio for conversion from the "reference camera"
|
||||
* to a camera with an arbitrary field of view `fov`. */
|
||||
function distanceAdjustment(mode: Camera.Mode, fov: number) {
|
||||
@@ -116,35 +157,41 @@ function fovAdjustedPosition(target: Vec3, refPosition: Vec3, mode: Camera.Mode,
|
||||
return Vec3.scaleAndAdd(delta, target, delta, adjustment); // return target + delta * adjustment
|
||||
}
|
||||
|
||||
/** Create object for PluginState.Snapshot.camera based on tree loading context and MVS snapshot metadata */
|
||||
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }): PluginState.Snapshot['camera'] {
|
||||
const camera: PluginState.Snapshot['camera'] = {
|
||||
transitionStyle: 'animate',
|
||||
transitionDurationInMs: metadata.previousTransitionDurationMs ?? 0,
|
||||
};
|
||||
if (context.camera.cameraParams !== undefined) {
|
||||
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
|
||||
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, context.camera.cameraParams);
|
||||
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
|
||||
} else {
|
||||
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses);
|
||||
}
|
||||
return camera;
|
||||
/** Compute the bounding sphere of the whole scene. */
|
||||
function getPluginBoundingSphere(plugin: PluginContext) {
|
||||
const renderObjects = getRenderObjects(plugin, false);
|
||||
const spheres = renderObjects.map(r => r.values.boundingSphere.ref.value).filter(sphere => sphere.radius > 0);
|
||||
return boundingSphereOfSpheres(spheres);
|
||||
}
|
||||
|
||||
function getRenderObjects(plugin: PluginContext, includeHidden: boolean): GraphicsRenderObject[] {
|
||||
let reprCells = Array.from(plugin.state.data.cells.values()).filter(cell => cell.obj && PluginStateObject.isRepresentation3D(cell.obj));
|
||||
if (!includeHidden) reprCells = reprCells.filter(cell => !cell.state.isHidden);
|
||||
const renderables = reprCells.flatMap(cell => cell.obj!.data.repr.renderObjects);
|
||||
return renderables;
|
||||
}
|
||||
|
||||
let boundaryHelper: BoundaryHelper | undefined = undefined;
|
||||
|
||||
function boundingSphereOfSpheres(spheres: Sphere3D[]): Sphere3D {
|
||||
boundaryHelper ??= new BoundaryHelper('98');
|
||||
boundaryHelper.reset();
|
||||
for (const s of spheres) boundaryHelper.includeSphere(s);
|
||||
boundaryHelper.finishedIncludeStep();
|
||||
for (const s of spheres) boundaryHelper.radiusSphere(s);
|
||||
return boundaryHelper.getSphere();
|
||||
}
|
||||
|
||||
/** Set canvas properties based on a canvas node params. */
|
||||
export function setCanvas(plugin: PluginContext, params: MolstarNodeParams<'canvas'> | undefined) {
|
||||
plugin.canvas3d?.setProps(old => modifyCanvasProps(old, params));
|
||||
}
|
||||
|
||||
/** Create a deep copy of `oldCanvasProps` with values modified according to a canvas node params. */
|
||||
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, params: MolstarNodeParams<'canvas'> | undefined): Canvas3DProps {
|
||||
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
|
||||
return {
|
||||
...oldCanvasProps,
|
||||
renderer: {
|
||||
...oldCanvasProps.renderer,
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
if (backgroundColor !== plugin.canvas3d?.props.renderer.backgroundColor) {
|
||||
plugin.canvas3d?.setProps(old => ({
|
||||
...old,
|
||||
renderer: {
|
||||
...old.renderer,
|
||||
backgroundColor: backgroundColor,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Lines } from '../../../mol-geo/geometry/lines/lines';
|
||||
@@ -23,7 +22,6 @@ import { Expression } from '../../../mol-script/language/expression';
|
||||
import { StateObject } from '../../../mol-state';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { round } from '../../../mol-util';
|
||||
import { range } from '../../../mol-util/array';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -31,23 +29,17 @@ import { capitalize } from '../../../mol-util/string';
|
||||
import { rowsToExpression, rowToExpression } from '../helpers/selections';
|
||||
import { collectMVSReferences, decodeColor } from '../helpers/utils';
|
||||
import { MolstarNode, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSPrimitive, MVSPrimitiveOptions, MVSPrimitiveParams } from '../tree/mvs/mvs-primitives';
|
||||
import { MVSNode } from '../tree/mvs/mvs-tree';
|
||||
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
|
||||
type PrimitivesParams = MolstarNode<'primitives'>['params']
|
||||
|
||||
type _PrimitiveParams = MolstarNode<'primitive'>['params']
|
||||
type PrimitiveKind = _PrimitiveParams['kind']
|
||||
type PrimitiveParams<T extends PrimitiveKind = PrimitiveKind> = Extract<_PrimitiveParams, { kind: T }>
|
||||
|
||||
export function getPrimitiveStructureRefs(primitives: MolstarSubtree<'primitives'>) {
|
||||
const refs = new Set<string>();
|
||||
for (const c of primitives.children ?? []) {
|
||||
if (c.kind !== 'primitive') continue;
|
||||
const p = c.params;
|
||||
Builders[p.kind].resolveRefs?.(p, refs);
|
||||
const p = c.params as unknown as MVSPrimitive;
|
||||
Builders[p.kind]?.[3].refs?.(p, refs);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
@@ -132,7 +124,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
params: PD.withDefaults(Mesh.Params, { alpha: a.data.options?.transparency ?? 1 }),
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveMesh(data, prev?.geometry),
|
||||
geometryUtils: Mesh.Utils,
|
||||
}, { label });
|
||||
@@ -142,7 +134,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: PD.withDefaults(DefaultLabelParams, { alpha: a.data.options?.label_opacity ?? 1 }),
|
||||
params: PD.withDefaults(DefaultLabelParams, { alpha: a.data.options?.label_transparency ?? 1 }),
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveLabels(data, prev?.geometry),
|
||||
geometryUtils: Text.Utils,
|
||||
}, { label });
|
||||
@@ -152,7 +144,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
params: PD.withDefaults(Lines.Params, { alpha: a.data.options?.transparency ?? 1 }),
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveLines(data, prev?.geometry),
|
||||
geometryUtils: Lines.Utils,
|
||||
}, { label });
|
||||
@@ -209,7 +201,7 @@ interface PrimitiveBuilderContext {
|
||||
defaultStructure?: Structure;
|
||||
structureRefs: Record<string, Structure | undefined>;
|
||||
primitives: MolstarNode<'primitive'>[];
|
||||
options: PrimitivesParams;
|
||||
options: MVSPrimitiveOptions;
|
||||
positionCache: Map<string, [Sphere3D, Box3D]>;
|
||||
instances: Mat4[] | undefined;
|
||||
}
|
||||
@@ -242,60 +234,30 @@ const BaseLabelProps: PD.Values<Text.Params> = {
|
||||
};
|
||||
const DefaultLabelParams = PD.withDefaults(Text.Params, BaseLabelProps);
|
||||
|
||||
interface PrimitiveBuilder {
|
||||
builders: {
|
||||
mesh?: (context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: any) => void,
|
||||
line?: (context: PrimitiveBuilderContext, state: LineBuilderState, node: MVSNode<'primitive'>, params: any) => void,
|
||||
label?: (context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: any) => void,
|
||||
}
|
||||
isApplicable?: {
|
||||
mesh?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
|
||||
line?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
|
||||
label?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
|
||||
},
|
||||
resolveRefs?: (params: any, refs: Set<string>) => void,
|
||||
}
|
||||
|
||||
const Builders: Record<PrimitiveParams['kind'], PrimitiveBuilder> = {
|
||||
mesh: {
|
||||
builders: {
|
||||
mesh: addMesh,
|
||||
line: addMeshWireframe,
|
||||
},
|
||||
isApplicable: {
|
||||
mesh: (m: PrimitiveParams<'mesh'>) => m.show_triangles,
|
||||
line: (m: PrimitiveParams<'mesh'>) => m.show_wireframe,
|
||||
},
|
||||
},
|
||||
lines: {
|
||||
builders: {
|
||||
line: addLines,
|
||||
},
|
||||
},
|
||||
tube: {
|
||||
builders: {
|
||||
mesh: addTubeMesh,
|
||||
},
|
||||
resolveRefs: resolveLineRefs,
|
||||
},
|
||||
label: {
|
||||
builders: {
|
||||
label: addPrimitiveLabel,
|
||||
},
|
||||
resolveRefs: resolveLabelRefs,
|
||||
},
|
||||
distance_measurement: {
|
||||
builders: {
|
||||
mesh: addDistanceMesh,
|
||||
label: addDistanceLabel,
|
||||
},
|
||||
resolveRefs: resolveLineRefs,
|
||||
const Builders: Record<MVSPrimitive['kind'], [
|
||||
mesh: (context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: any) => void,
|
||||
line: (context: PrimitiveBuilderContext, state: LineBuilderState, node: MVSNode<'primitive'>, params: any) => void,
|
||||
label: (context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: any) => void,
|
||||
features: {
|
||||
mesh?: boolean | ((primitive: any, context: PrimitiveBuilderContext) => boolean),
|
||||
line?: boolean | ((primitive: any, context: PrimitiveBuilderContext) => boolean),
|
||||
label?: boolean | ((primitive: any, context: PrimitiveBuilderContext) => boolean),
|
||||
refs?: (params: any, refs: Set<string>) => void
|
||||
},
|
||||
]> = {
|
||||
mesh: [addMesh, addMeshWireframe, noOp, {
|
||||
mesh: (m: MVSPrimitiveParams<'mesh'>) => m.show_triangles ?? true,
|
||||
line: (m: MVSPrimitiveParams<'mesh'>) => m.show_wireframe ?? false,
|
||||
}],
|
||||
lines: [addMesh, addLines, noOp, { line: true }],
|
||||
line: [addLineMesh, noOp, noOp, { mesh: true, refs: resolveLineRefs }],
|
||||
label: [noOp, noOp, addPrimitiveLabel, { label: true, refs: resolveLabelRefs }],
|
||||
distance_measurement: [addDistanceMesh, noOp, addDistanceLabel, { mesh: true, label: true, refs: resolveLineRefs }],
|
||||
};
|
||||
|
||||
|
||||
function getPrimitives(primitives: MolstarSubtree<'primitives'>) {
|
||||
return (primitives.children ?? []).filter(c => c.kind === 'primitive') as MolstarNode<'primitive'>[];
|
||||
return (primitives.children ?? []).filter(c => c.kind === 'primitive') as unknown as MolstarNode<'primitive'>[];
|
||||
}
|
||||
|
||||
function addRef(position: PrimitivePositionT, refs: Set<string>) {
|
||||
@@ -306,14 +268,12 @@ function addRef(position: PrimitivePositionT, refs: Set<string>) {
|
||||
|
||||
function hasPrimitiveKind(context: PrimitiveBuilderContext, kind: 'mesh' | 'line' | 'label') {
|
||||
for (const c of context.primitives) {
|
||||
const params = c.params;
|
||||
const b = Builders[params.kind];
|
||||
const builderFunction = b.builders[kind];
|
||||
if (builderFunction) {
|
||||
const test = b.isApplicable?.[kind];
|
||||
if (test === undefined || test(params, context)) {
|
||||
return true;
|
||||
}
|
||||
const p = c.params as unknown as MVSPrimitive;
|
||||
const test = Builders[p.kind]?.[3]?.[kind];
|
||||
if (typeof test === 'boolean') {
|
||||
if (test) return true;
|
||||
} else if (test?.(p, context)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -387,7 +347,7 @@ function resolvePosition(context: PrimitiveBuilderContext, position: PrimitivePo
|
||||
context.positionCache.set(cackeKey, [sphere, box]);
|
||||
}
|
||||
|
||||
function getInstances(options: PrimitivesParams | undefined): Mat4[] | undefined {
|
||||
function getInstances(options: MVSPrimitiveOptions | undefined): Mat4[] | undefined {
|
||||
if (!options?.instances?.length) return undefined;
|
||||
return options.instances.map(i => Mat4.fromArray(Mat4(), i, 0));
|
||||
}
|
||||
@@ -399,18 +359,18 @@ function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shap
|
||||
meshBuilder.currentGroup = -1;
|
||||
|
||||
for (const c of context.primitives) {
|
||||
const p = c.params;
|
||||
const p = c.params as unknown as MVSPrimitive;
|
||||
const b = Builders[p.kind];
|
||||
if (!b) {
|
||||
console.warn(`Primitive ${p.kind} not supported`);
|
||||
continue;
|
||||
}
|
||||
b.builders.mesh?.(context, state, c, p);
|
||||
b[0](context, state, c, p);
|
||||
}
|
||||
|
||||
const { colors, tooltips } = state.groups;
|
||||
const tooltip = context.options?.tooltip ?? '';
|
||||
const color = decodeColor(context.options?.color) ?? Color(0);
|
||||
const color = decodeColor(context.options?.color) ?? 0x0;
|
||||
|
||||
return Shape.create(
|
||||
'Mesh',
|
||||
@@ -420,7 +380,7 @@ function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shap
|
||||
groupToNode: state.groups.groupToNodeMap,
|
||||
},
|
||||
MeshBuilder.getMesh(meshBuilder),
|
||||
(g) => colors.get(g) as Color ?? color,
|
||||
(g) => colors.get(g) as Color ?? color as Color,
|
||||
(g) => 1,
|
||||
(g) => tooltips.get(g) ?? tooltip,
|
||||
context.instances,
|
||||
@@ -432,18 +392,17 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
|
||||
const state: LineBuilderState = { groups: new GroupManager(), lines: linesBuilder };
|
||||
|
||||
for (const c of context.primitives) {
|
||||
const p = c.params;
|
||||
const p = c.params as unknown as MVSPrimitive;
|
||||
const b = Builders[p.kind];
|
||||
if (!b) {
|
||||
console.warn(`Primitive ${p.kind} not supported`);
|
||||
continue;
|
||||
}
|
||||
b.builders.line?.(context, state, c, p);
|
||||
b[1](context, state, c, p);
|
||||
}
|
||||
|
||||
const color = decodeColor(context.options?.color) ?? 0x0;
|
||||
const { colors, sizes, tooltips } = state.groups;
|
||||
const tooltip = context.options?.tooltip ?? '';
|
||||
const color = decodeColor(context.options?.color) ?? Color(0);
|
||||
|
||||
return Shape.create(
|
||||
'Lines',
|
||||
@@ -453,9 +412,9 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
|
||||
groupToNode: state.groups.groupToNodeMap,
|
||||
},
|
||||
linesBuilder.getLines(),
|
||||
(g) => colors.get(g) as Color ?? color,
|
||||
(g) => colors.get(g) as Color ?? color as Color,
|
||||
(g) => sizes.get(g) ?? 1,
|
||||
(g) => tooltips.get(g) ?? tooltip,
|
||||
(g) => tooltips.get(g) ?? '',
|
||||
context.instances,
|
||||
);
|
||||
}
|
||||
@@ -465,16 +424,16 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev?: Text): Sh
|
||||
const state: LabelBuilderState = { groups: new GroupManager(), labels: labelsBuilder };
|
||||
|
||||
for (const c of context.primitives) {
|
||||
const p = c.params;
|
||||
const p = c.params as unknown as MVSPrimitive;
|
||||
const b = Builders[p.kind];
|
||||
if (!b) {
|
||||
console.warn(`Primitive ${p.kind} not supported`);
|
||||
continue;
|
||||
}
|
||||
b.builders.label?.(context, state, c, p);
|
||||
b[2](context, state, c, p);
|
||||
}
|
||||
|
||||
const color = decodeColor(context.options?.label_color) ?? Color(0);
|
||||
const color = decodeColor(context.options?.label_color) ?? 0x0;
|
||||
const { colors, sizes, tooltips } = state.groups;
|
||||
|
||||
return Shape.create(
|
||||
@@ -485,89 +444,114 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev?: Text): Sh
|
||||
groupToNode: state.groups.groupToNodeMap,
|
||||
},
|
||||
labelsBuilder.getText(),
|
||||
(g) => colors.get(g) as Color ?? color,
|
||||
(g) => colors.get(g) as Color ?? color as Color,
|
||||
(g) => sizes.get(g) ?? 1,
|
||||
(g) => tooltips.get(g) ?? '',
|
||||
context.instances,
|
||||
);
|
||||
}
|
||||
|
||||
function addMeshFaces(context: PrimitiveBuilderContext, groups: GroupManager, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>, addFace: (mvsGroup: number, builderGroup: number, a: Vec3, b: Vec3, c: Vec3) => void) {
|
||||
function noOp() { }
|
||||
|
||||
function addMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'mesh'>) {
|
||||
if (!params.show_triangles) return;
|
||||
|
||||
const a = Vec3.zero();
|
||||
const b = Vec3.zero();
|
||||
const c = Vec3.zero();
|
||||
|
||||
let { indices, vertices, triangle_groups } = params;
|
||||
const nTriangles = Math.floor(indices.length / 3);
|
||||
triangle_groups ??= range(nTriangles); // implicit grouping (triangle i = group i)
|
||||
const groupSet = groups.allocateMany(node, triangle_groups);
|
||||
const { indices, vertices, triangle_colors, triangle_groups, group_colors, group_tooltips } = params;
|
||||
|
||||
const groupSet: Map<number, number> | undefined = triangle_groups?.length ? groups.allocateMany(node, triangle_groups) : undefined;
|
||||
|
||||
for (let i = 0, _i = indices.length / 3; i < _i; i++) {
|
||||
if (groupSet) {
|
||||
const grp = triangle_groups![i];
|
||||
mesh.currentGroup = groupSet.get(grp)!;
|
||||
groups.updateColor(mesh.currentGroup, group_colors?.[grp] ?? params.color);
|
||||
groups.updateTooltip(mesh.currentGroup, group_tooltips?.[grp]);
|
||||
} else {
|
||||
mesh.currentGroup = groups.allocateSingle(node);
|
||||
groups.updateColor(mesh.currentGroup, triangle_colors?.[i] ?? params.color);
|
||||
groups.updateTooltip(mesh.currentGroup, params.tooltip);
|
||||
}
|
||||
|
||||
for (let i = 0; i < nTriangles; i++) {
|
||||
const mvsGroup = triangle_groups[i];
|
||||
const builderGroup = groupSet.get(mvsGroup)!;
|
||||
Vec3.fromArray(a, vertices, 3 * indices[3 * i]);
|
||||
Vec3.fromArray(b, vertices, 3 * indices[3 * i + 1]);
|
||||
Vec3.fromArray(c, vertices, 3 * indices[3 * i + 2]);
|
||||
|
||||
addFace(mvsGroup, builderGroup, a, b, c);
|
||||
MeshBuilder.addTriangle(mesh, a, b, c);
|
||||
}
|
||||
}
|
||||
|
||||
function addMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>) {
|
||||
if (!params.show_triangles) return;
|
||||
|
||||
const { group_colors, group_tooltips, color, tooltip } = params;
|
||||
|
||||
addMeshFaces(context, groups, node, params, (mvsGroup, builderGroup, a, b, c) => {
|
||||
groups.updateColor(builderGroup, group_colors[mvsGroup] ?? color);
|
||||
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? tooltip);
|
||||
mesh.currentGroup = builderGroup;
|
||||
MeshBuilder.addTriangle(mesh, a, b, c);
|
||||
});
|
||||
// this could be slightly improved by only updating color and tooltip once per group instead of once per triangle
|
||||
}
|
||||
|
||||
function addMeshWireframe(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>) {
|
||||
function addMeshWireframe(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'mesh'>) {
|
||||
if (!params.show_wireframe) return;
|
||||
const width = params.wireframe_width;
|
||||
|
||||
const { group_colors, group_tooltips, wireframe_color, color, tooltip } = params;
|
||||
const a = Vec3.zero();
|
||||
const b = Vec3.zero();
|
||||
const c = Vec3.zero();
|
||||
|
||||
addMeshFaces(context, groups, node, params, (mvsGroup, builderGroup, a, b, c) => {
|
||||
groups.updateColor(builderGroup, wireframe_color ?? group_colors[mvsGroup] ?? color);
|
||||
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? tooltip);
|
||||
groups.updateSize(builderGroup, width);
|
||||
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], builderGroup);
|
||||
lines.add(b[0], b[1], b[2], c[0], c[1], c[2], builderGroup);
|
||||
lines.add(c[0], c[1], c[2], a[0], a[1], a[2], builderGroup);
|
||||
});
|
||||
const { indices, vertices, triangle_colors, triangle_groups, group_colors, group_tooltips } = params;
|
||||
|
||||
const groupSet: Map<number, number> | undefined = triangle_groups?.length ? groups.allocateMany(node, triangle_groups) : undefined;
|
||||
const radius = params.wireframe_radius ?? 1;
|
||||
|
||||
for (let i = 0, _i = indices.length / 3; i < _i; i++) {
|
||||
let group: number;
|
||||
if (groupSet) {
|
||||
const grp = triangle_groups![i];
|
||||
group = groupSet.get(grp)!;
|
||||
groups.updateColor(group, params.wireframe_color ?? group_colors?.[grp]);
|
||||
groups.updateTooltip(group, group_tooltips?.[grp]);
|
||||
} else {
|
||||
group = groups.allocateSingle(node);
|
||||
groups.updateColor(group, params.wireframe_color ?? triangle_colors?.[i]);
|
||||
groups.updateTooltip(group, params.tooltip);
|
||||
}
|
||||
|
||||
groups.updateSize(group, radius);
|
||||
|
||||
Vec3.fromArray(a, vertices, 3 * indices[3 * i]);
|
||||
Vec3.fromArray(b, vertices, 3 * indices[3 * i + 1]);
|
||||
Vec3.fromArray(c, vertices, 3 * indices[3 * i + 2]);
|
||||
|
||||
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], group);
|
||||
lines.add(b[0], b[1], b[2], c[0], c[1], c[2], group);
|
||||
lines.add(c[0], c[1], c[2], a[0], a[1], a[2], group);
|
||||
}
|
||||
}
|
||||
|
||||
function addLines(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'lines'>) {
|
||||
function addLines(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'lines'>) {
|
||||
const a = Vec3.zero();
|
||||
const b = Vec3.zero();
|
||||
|
||||
let { indices, vertices, line_groups, group_colors, group_tooltips, group_widths } = params;
|
||||
const width = params.width;
|
||||
const { indices, vertices, line_colors, line_groups, group_colors, group_tooltips, group_radius } = params;
|
||||
|
||||
const nLines = Math.floor(indices.length / 2);
|
||||
line_groups ??= range(nLines); // implicit grouping (line i = group i)
|
||||
const groupSet = groups.allocateMany(node, line_groups);
|
||||
const groupSet: Map<number, number> | undefined = line_groups?.length ? groups.allocateMany(node, line_groups) : undefined;
|
||||
const radius = params.line_radius ?? 1;
|
||||
|
||||
for (let i = 0; i < nLines; i++) {
|
||||
const mvsGroup = line_groups[i];
|
||||
const builderGroup = groupSet.get(mvsGroup)!;
|
||||
groups.updateColor(builderGroup, group_colors[mvsGroup] ?? params.color);
|
||||
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? params.tooltip);
|
||||
groups.updateSize(builderGroup, group_widths[mvsGroup] ?? width);
|
||||
for (let i = 0, _i = indices.length / 2; i < _i; i++) {
|
||||
let group: number;
|
||||
if (groupSet) {
|
||||
const grp = line_groups![i];
|
||||
group = groupSet.get(grp)!;
|
||||
groups.updateColor(group, group_colors?.[grp] ?? params.color);
|
||||
groups.updateTooltip(group, group_tooltips?.[grp]);
|
||||
groups.updateSize(group, group_radius?.[grp] ?? radius);
|
||||
} else {
|
||||
group = groups.allocateSingle(node);
|
||||
groups.updateColor(group, line_colors?.[i] ?? params.color);
|
||||
groups.updateSize(group, radius);
|
||||
groups.updateTooltip(group, params.tooltip);
|
||||
}
|
||||
|
||||
Vec3.fromArray(a, vertices, 3 * indices[2 * i]);
|
||||
Vec3.fromArray(b, vertices, 3 * indices[2 * i + 1]);
|
||||
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], builderGroup);
|
||||
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], group);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLineRefs(params: PrimitiveParams<'tube' | 'distance_measurement'>, refs: Set<string>) {
|
||||
function resolveLineRefs(params: MVSPrimitiveParams<'line' | 'distance_measurement'>, refs: Set<string>) {
|
||||
addRef(params.start, refs);
|
||||
addRef(params.end, refs);
|
||||
}
|
||||
@@ -575,12 +559,12 @@ function resolveLineRefs(params: PrimitiveParams<'tube' | 'distance_measurement'
|
||||
const lStart = Vec3.zero();
|
||||
const lEnd = Vec3.zero();
|
||||
|
||||
function addTubeMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'tube'>, options?: { skipResolvePosition?: boolean }) {
|
||||
function addLineMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'line'>, options?: { skipResolvePosition?: boolean }) {
|
||||
if (!options?.skipResolvePosition) {
|
||||
resolveBasePosition(context, params.start, lStart);
|
||||
resolveBasePosition(context, params.end, lEnd);
|
||||
}
|
||||
const radius = params.radius;
|
||||
const radius = params.thickness ?? 0.05;
|
||||
|
||||
const cylinderProps: BasicCylinderProps = {
|
||||
radiusBottom: radius,
|
||||
@@ -602,7 +586,7 @@ function addTubeMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBui
|
||||
}
|
||||
}
|
||||
|
||||
function getDistanceLabel(context: PrimitiveBuilderContext, params: PrimitiveParams<'distance_measurement'>) {
|
||||
function getDistanceLabel(context: PrimitiveBuilderContext, params: MVSPrimitiveParams<'distance_measurement'>) {
|
||||
resolveBasePosition(context, params.start, lStart);
|
||||
resolveBasePosition(context, params.end, lEnd);
|
||||
|
||||
@@ -613,14 +597,14 @@ function getDistanceLabel(context: PrimitiveBuilderContext, params: PrimitivePar
|
||||
return label;
|
||||
}
|
||||
|
||||
function addDistanceMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'distance_measurement'>) {
|
||||
function addDistanceMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'distance_measurement'>) {
|
||||
const tooltip = getDistanceLabel(context, params);
|
||||
addTubeMesh(context, state, node, { ...params, tooltip } as any, { skipResolvePosition: true });
|
||||
addLineMesh(context, state, node, { ...params, tooltip } as any, { skipResolvePosition: true });
|
||||
}
|
||||
|
||||
const labelPos = Vec3.zero();
|
||||
|
||||
function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'distance_measurement'>) {
|
||||
function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'distance_measurement'>) {
|
||||
const { labels, groups } = state;
|
||||
resolveBasePosition(context, params.start, lStart);
|
||||
resolveBasePosition(context, params.end, lEnd);
|
||||
@@ -630,10 +614,10 @@ function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderS
|
||||
const label = typeof params.label_template === 'string' ? params.label_template.replace('{{distance}}', distance) : distance;
|
||||
|
||||
let size: number | undefined;
|
||||
if (typeof params.label_size === 'number') {
|
||||
if (params.label_size === 'auto') {
|
||||
size = Math.max(dist * (params.label_auto_size_scale ?? 0.2), params.label_auto_size_min ?? 0.01);
|
||||
} else if (typeof params.label_size === 'number') {
|
||||
size = params.label_size;
|
||||
} else {
|
||||
size = Math.max(dist * (params.label_auto_size_scale), params.label_auto_size_min);
|
||||
}
|
||||
|
||||
Vec3.add(labelPos, lStart, lEnd);
|
||||
@@ -643,14 +627,14 @@ function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderS
|
||||
groups.updateColor(group, params.label_color);
|
||||
groups.updateSize(group, size);
|
||||
|
||||
labels.add(label, labelPos[0], labelPos[1], labelPos[2], 1.05 * (params.radius), 1, group);
|
||||
labels.add(label, labelPos[0], labelPos[1], labelPos[2], 1.05 * (params.thickness ?? 0.05), 1, group);
|
||||
}
|
||||
|
||||
function resolveLabelRefs(params: PrimitiveParams<'label'>, refs: Set<string>) {
|
||||
function resolveLabelRefs(params: MVSPrimitiveParams<'label'>, refs: Set<string>) {
|
||||
addRef(params.position, refs);
|
||||
}
|
||||
|
||||
function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'label'>) {
|
||||
function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: MVSPrimitiveParams<'label'>) {
|
||||
const { labels, groups } = state;
|
||||
resolveBasePosition(context, params.position, labelPos);
|
||||
|
||||
@@ -658,5 +642,5 @@ function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilder
|
||||
groups.updateColor(group, params.label_color);
|
||||
groups.updateSize(group, params.label_size);
|
||||
|
||||
labels.add(params.text, labelPos[0], labelPos[1], labelPos[2], params.label_offset, 1, group);
|
||||
labels.add(params.text, labelPos[0], labelPos[1], labelPos[2], params.label_offset ?? 0, 1, group);
|
||||
}
|
||||
|
||||
@@ -128,16 +128,6 @@ export const HexColor = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Named color string, e.g. 'red' */
|
||||
export type ColorName = keyof ColorNames
|
||||
|
||||
export const ColorName = {
|
||||
/** Decide if a string is a valid named color string */
|
||||
is(str: any): str is ColorName {
|
||||
return str in ColorNames;
|
||||
},
|
||||
};
|
||||
|
||||
export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject>): Record<string, StateObject.From<T>['data']> {
|
||||
const ret: any = {};
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { State, StateBuilder, StateObject, StateObjectSelector, StateTransform, StateTransformer, StateTree } from '../../mol-state';
|
||||
import { UUID } from '../../mol-util';
|
||||
import { StateBuilder, StateObject, StateObjectSelector, StateTransform, StateTransformer } from '../../mol-state';
|
||||
import { stringHash } from './helpers/utils';
|
||||
import { Kind, Subtree, SubtreeOfKind, Tree } from './tree/generic/tree-schema';
|
||||
import { dfs } from './tree/generic/tree-utils';
|
||||
@@ -45,36 +43,9 @@ export async function loadTree<TTree extends Tree, TContext>(
|
||||
loadingActions: LoadingActions<TTree, TContext>,
|
||||
context: TContext,
|
||||
options?: { replaceExisting?: boolean, extensions?: LoadingExtension<TTree, TContext, any>[] }
|
||||
) {
|
||||
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
|
||||
loadTreeInUpdate(updateRoot, tree, loadingActions, context, options);
|
||||
await UpdateTarget.commit(updateRoot);
|
||||
}
|
||||
|
||||
|
||||
export function loadTreeVirtual<TTree extends Tree, TContext>(
|
||||
plugin: PluginContext,
|
||||
tree: TTree,
|
||||
loadingActions: LoadingActions<TTree, TContext>,
|
||||
context: TContext,
|
||||
options?: { replaceExisting?: boolean, extensions?: LoadingExtension<TTree, TContext, any>[] }
|
||||
) {
|
||||
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
|
||||
loadTreeInUpdate(updateRoot, tree, loadingActions, context, options);
|
||||
const stateTree: StateTree = updateRoot.update.getTree();
|
||||
const stateSnapshot: State.Snapshot = { tree: StateTree.toJSON(stateTree) };
|
||||
const pluginStateSnapshot: PluginState.Snapshot = { id: UUID.create22(), data: stateSnapshot };
|
||||
return pluginStateSnapshot;
|
||||
}
|
||||
|
||||
|
||||
function loadTreeInUpdate<TTree extends Tree, TContext>(updateRoot: UpdateTarget,
|
||||
tree: TTree,
|
||||
loadingActions: LoadingActions<TTree, TContext>,
|
||||
context: TContext,
|
||||
options?: { replaceExisting?: boolean, extensions?: LoadingExtension<TTree, TContext, any>[] }
|
||||
) {
|
||||
const mapping = new Map<Subtree<TTree>, UpdateTarget | undefined>();
|
||||
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
|
||||
if (options?.replaceExisting) {
|
||||
UpdateTarget.deleteChildren(updateRoot);
|
||||
}
|
||||
@@ -110,6 +81,7 @@ function loadTreeInUpdate<TTree extends Tree, TContext>(updateRoot: UpdateTarget
|
||||
}
|
||||
|
||||
extensionContexts.forEach(e => e.ext.disposeExtensionContext?.(e.extCtx, tree, context));
|
||||
await UpdateTarget.commit(updateRoot);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ import { ElementOfSet, decodeColor, isDefined, stringHash } from './helpers/util
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { Subtree, getChildren } from './tree/generic/tree-schema';
|
||||
import { dfs, formatObject } from './tree/generic/tree-utils';
|
||||
import { MolstarKind, MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree } from './tree/molstar/molstar-tree';
|
||||
import { MolstarKind, MolstarNode, MolstarSubtree, MolstarTree, MolstarNodeParams } from './tree/molstar/molstar-tree';
|
||||
import { DefaultColor } from './tree/mvs/mvs-defaults';
|
||||
|
||||
|
||||
export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri'] satisfies MolstarKind[]);
|
||||
@@ -35,9 +36,6 @@ export type AnnotationFromUriKind = ElementOfSet<typeof AnnotationFromUriKinds>
|
||||
export const AnnotationFromSourceKinds = new Set(['color_from_source', 'component_from_source', 'label_from_source', 'tooltip_from_source'] satisfies MolstarKind[]);
|
||||
export type AnnotationFromSourceKind = ElementOfSet<typeof AnnotationFromSourceKinds>
|
||||
|
||||
/** Color to be used e.g. for representations without 'color' node */
|
||||
export const DefaultColor = 'white';
|
||||
|
||||
|
||||
/** Return a 4x4 matrix representing a rotation followed by a translation */
|
||||
export function transformFromRotationTranslation(rotation: number[] | null | undefined, translation: number[] | null | undefined): Mat4 {
|
||||
@@ -99,7 +97,7 @@ export function collectAnnotationReferences(tree: Subtree<MolstarTree>, context:
|
||||
if (spec) {
|
||||
const key = canonicalJsonString(spec as any);
|
||||
distinctSpecs[key] ??= { ...spec, id: stringHash(key) };
|
||||
context.annotationMap.set(node as MolstarNode<AnnotationFromUriKind | AnnotationFromSourceKind>, distinctSpecs[key].id);
|
||||
(context.annotationMap ??= new Map()).set(node, distinctSpecs[key].id);
|
||||
}
|
||||
});
|
||||
return Object.values(distinctSpecs);
|
||||
@@ -117,7 +115,7 @@ export function collectAnnotationTooltips(tree: MolstarSubtree<'structure'>, con
|
||||
const annotationTooltips: MVSAnnotationTooltipsProps['tooltips'] = [];
|
||||
dfs(tree, node => {
|
||||
if (node.kind === 'tooltip_from_uri' || node.kind === 'tooltip_from_source') {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
const annotationId = context.annotationMap?.get(node);
|
||||
if (annotationId) {
|
||||
annotationTooltips.push({ annotationId, fieldName: node.params.field_name });
|
||||
};
|
||||
@@ -261,7 +259,7 @@ export function prettyNameFromSelector(selector?: MolstarNodeParams<'component'>
|
||||
|
||||
/** Create props for `StructureRepresentation3D` transformer from a label_from_* node. */
|
||||
export function labelFromXProps(node: MolstarNode<'label_from_uri' | 'label_from_source'>, context: MolstarLoadingContext): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
const annotationId = context.annotationMap?.get(node);
|
||||
const fieldName = node.params.field_name;
|
||||
const nearestReprNode = context.nearestReprMap?.get(node);
|
||||
return {
|
||||
@@ -272,7 +270,7 @@ export function labelFromXProps(node: MolstarNode<'label_from_uri' | 'label_from
|
||||
|
||||
/** Create props for `AnnotationStructureComponent` transformer from a component_from_* node. */
|
||||
export function componentFromXProps(node: MolstarNode<'component_from_uri' | 'component_from_source'>, context: MolstarLoadingContext): Partial<MVSAnnotationStructureComponentProps> {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
const annotationId = context.annotationMap?.get(node);
|
||||
const { field_name, field_values } = node.params;
|
||||
return {
|
||||
annotationId,
|
||||
@@ -304,11 +302,12 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
|
||||
}
|
||||
}
|
||||
|
||||
/** Create value for `type.params.alpha` prop for `StructureRepresentation3D` transformer from a representation node based on 'opacity' nodes in its subtree. */
|
||||
/** Create value for `type.params.alpha` prop for `StructureRepresentation3D` transformer from a representation node based on 'transparency' nodes in its subtree. */
|
||||
export function alphaForNode(node: MolstarSubtree<'representation'>): number {
|
||||
const children = getChildren(node).filter(c => c.kind === 'opacity');
|
||||
const children = getChildren(node).filter(c => c.kind === 'transparency');
|
||||
if (children.length > 0) {
|
||||
return children[children.length - 1].params.opacity;
|
||||
const transparency = children[children.length - 1].params.transparency;
|
||||
return 1 - transparency;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
@@ -340,7 +339,7 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
switch (node?.kind) {
|
||||
case 'color_from_uri':
|
||||
case 'color_from_source':
|
||||
annotationId = context.annotationMap.get(node);
|
||||
annotationId = context.annotationMap?.get(node);
|
||||
fieldName = node.params.field_name;
|
||||
break;
|
||||
case 'color':
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @author Aliaksei Chareshneu <chareshneu.tech@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
|
||||
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
@@ -14,7 +13,7 @@ import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { MolViewSpec } from './behavior';
|
||||
import { createPluginStateSnapshotCamera, modifyCanvasProps, setCamera, setCanvas, setFocus, suppressCameraAutoreset } from './camera';
|
||||
import { setCamera, setCanvas, setFocus, suppressCameraAutoreset } from './camera';
|
||||
import { MVSAnnotationsProvider } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponent } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
|
||||
@@ -23,9 +22,9 @@ import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
|
||||
import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-prop';
|
||||
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData } from './components/primitives';
|
||||
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
|
||||
import { LoadingActions, LoadingExtension, loadTree, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { LoadingActions, LoadingExtension, loadTree, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps } from './load-helpers';
|
||||
import { MVSData, SnapshotMetadata } from './mvs-data';
|
||||
import { MVSData } from './mvs-data';
|
||||
import { validateTree } from './tree/generic/tree-schema';
|
||||
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
|
||||
@@ -41,36 +40,13 @@ import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[], sanityChecks?: boolean, sourceUrl?: string, doNotReportErrors?: boolean } = {}) {
|
||||
plugin.errorContext.clear('mvs');
|
||||
try {
|
||||
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
|
||||
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
|
||||
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
|
||||
if (data.kind === 'multiple') {
|
||||
const entries: PluginStateSnapshotManager.Entry[] = [];
|
||||
for (let i = 0; i < data.snapshots.length; i++) {
|
||||
const snapshot = data.snapshots[i];
|
||||
const previousSnapshot = i > 0 ? data.snapshots[i - 1] : data.snapshots[data.snapshots.length - 1];
|
||||
validateTree(MVSTreeSchema, snapshot.root, 'MVS');
|
||||
if (options.sanityChecks) mvsSanityCheck(snapshot.root);
|
||||
const molstarTree = convertMvsToMolstar(snapshot.root, options.sourceUrl);
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
|
||||
const entry = molstarTreeToEntry(plugin, molstarTree, { ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms }, options);
|
||||
entries.push(entry);
|
||||
}
|
||||
plugin.managers.snapshot.clear();
|
||||
for (const entry of entries) {
|
||||
plugin.managers.snapshot.add(entry);
|
||||
}
|
||||
if (entries.length > 0) {
|
||||
await PluginCommands.State.Snapshots.Apply(plugin, { id: entries[0].snapshot.id });
|
||||
}
|
||||
} else {
|
||||
validateTree(MVSTreeSchema, data.root, 'MVS');
|
||||
if (options.sanityChecks) mvsSanityCheck(data.root);
|
||||
const molstarTree = convertMvsToMolstar(data.root, options.sourceUrl);
|
||||
// console.log(`Converted MolStar tree:\n${MVSData.toPrettyString({ root: molstarTree, metadata: { version: 'x', timestamp: 'x' } })}`)
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
|
||||
await loadMolstarTree(plugin, molstarTree, options);
|
||||
}
|
||||
validateTree(MVSTreeSchema, data.root, 'MVS');
|
||||
if (options.sanityChecks) mvsSanityCheck(data.root);
|
||||
const molstarTree = convertMvsToMolstar(data.root, options.sourceUrl);
|
||||
// console.log(`Converted MolStar tree:\n${MVSData.toPrettyString({ root: molstarTree as any, metadata: { version: 'x', timestamp: 'x' } })}`)
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
|
||||
await loadMolstarTree(plugin, molstarTree, options);
|
||||
} catch (err) {
|
||||
plugin.log.error(`${err}`);
|
||||
throw err;
|
||||
@@ -96,7 +72,7 @@ async function loadMolstarTree(plugin: PluginContext, tree: MolstarTree, options
|
||||
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
|
||||
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
|
||||
|
||||
const context = MolstarLoadingContext.create();
|
||||
const context: MolstarLoadingContext = {};
|
||||
|
||||
await loadTree(plugin, tree, MolstarLoadingActions, context, { ...options, extensions: options?.extensions ?? BuiltinLoadingExtensions });
|
||||
|
||||
@@ -105,53 +81,25 @@ async function loadMolstarTree(plugin: PluginContext, tree: MolstarTree, options
|
||||
if (options?.keepCamera) {
|
||||
await suppressCameraAutoreset(plugin);
|
||||
} else {
|
||||
if (context.camera.cameraParams !== undefined) {
|
||||
await setCamera(plugin, context.camera.cameraParams);
|
||||
if (context.focus?.kind === 'camera') {
|
||||
await setCamera(plugin, context.focus.params);
|
||||
} else if (context.focus?.kind === 'focus') {
|
||||
await setFocus(plugin, context.focus.focusTarget, context.focus.params);
|
||||
} else {
|
||||
await setFocus(plugin, context.camera.focuses); // This includes implicit camera (i.e. no 'camera' or 'focus' nodes)
|
||||
await setFocus(plugin, undefined, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function molstarTreeToEntry(plugin: PluginContext, tree: MolstarTree, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }, options?: { replaceExisting?: boolean, keepCamera?: boolean }) {
|
||||
const context = MolstarLoadingContext.create();
|
||||
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, options);
|
||||
snapshot.canvas3d = {
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas) : undefined,
|
||||
};
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
|
||||
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
|
||||
|
||||
const entryParams: PluginStateSnapshotManager.EntryParams = {
|
||||
key: metadata.key,
|
||||
name: metadata.title,
|
||||
description: metadata.description,
|
||||
descriptionFormat: metadata.description_format ?? 'markdown',
|
||||
};
|
||||
const entry: PluginStateSnapshotManager.Entry = PluginStateSnapshotManager.Entry(snapshot, entryParams);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Mutable context for loading a `MolstarTree`, available throughout the loading. */
|
||||
export interface MolstarLoadingContext {
|
||||
/** Maps `*_from_[uri|source]` nodes to annotationId they should reference */
|
||||
annotationMap: Map<MolstarNode<AnnotationFromUriKind | AnnotationFromSourceKind>, string>,
|
||||
annotationMap?: Map<MolstarNode<AnnotationFromUriKind | AnnotationFromSourceKind>, string>,
|
||||
/** Maps each node (on 'structure' or lower level) to its nearest 'representation' node */
|
||||
nearestReprMap?: Map<MolstarNode, MolstarNode<'representation'>>,
|
||||
camera: {
|
||||
cameraParams?: MolstarNodeParams<'camera'>,
|
||||
focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[],
|
||||
},
|
||||
focus?: { kind: 'camera', params: MolstarNodeParams<'camera'> } | { kind: 'focus', focusTarget: StateObjectSelector, params: MolstarNodeParams<'focus'> },
|
||||
canvas?: MolstarNodeParams<'canvas'>,
|
||||
}
|
||||
export const MolstarLoadingContext = {
|
||||
create(): MolstarLoadingContext {
|
||||
return {
|
||||
annotationMap: new Map(),
|
||||
camera: { focuses: [] },
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/** Loading actions for loading a `MolstarTree`, per node kind. */
|
||||
@@ -285,11 +233,11 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
return UpdateTarget.apply(updateParent, StructureRepresentation3D, props);
|
||||
},
|
||||
focus(updateParent: UpdateTarget, node: MolstarNode<'focus'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
context.camera.focuses.push({ target: updateParent.selector, params: node.params });
|
||||
context.focus = { kind: 'focus', focusTarget: updateParent.selector, params: node.params };
|
||||
return updateParent;
|
||||
},
|
||||
camera(updateParent: UpdateTarget, node: MolstarNode<'camera'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
context.camera.cameraParams = node.params;
|
||||
context.focus = { kind: 'camera', params: node.params };
|
||||
return updateParent;
|
||||
},
|
||||
canvas(updateParent: UpdateTarget, node: MolstarNode<'canvas'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
@@ -303,7 +251,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
},
|
||||
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
|
||||
return applyPrimitiveVisuals(data, new Set(tree.params.references));
|
||||
return applyPrimitiveVisuals(data, new Set(tree.params.references ?? []));
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -9,73 +9,27 @@ import { treeToString } from './tree/generic/tree-utils';
|
||||
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
|
||||
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
/** Top-level metadata for a MVS file (single-state or multi-state). */
|
||||
export interface GlobalMetadata {
|
||||
/** Name of this MVSData */
|
||||
|
||||
/** Top level of the MolViewSpec (MVS) data format. */
|
||||
export interface MVSData {
|
||||
/** MolViewSpec tree */
|
||||
root: MVSTree,
|
||||
/** Associated metadata */
|
||||
metadata: MVSMetadata,
|
||||
}
|
||||
|
||||
interface MVSMetadata {
|
||||
/** Version of the spec used to write this tree */
|
||||
version: string,
|
||||
/** Name of this view */
|
||||
title?: string,
|
||||
/** Detailed description of this view */
|
||||
description?: string,
|
||||
/** Format of `description`. Default is 'markdown'. */
|
||||
/** Format of the description */
|
||||
description_format?: 'markdown' | 'plaintext',
|
||||
/** Timestamp when this view was exported. */
|
||||
/** Timestamp when this view was exported */
|
||||
timestamp: string,
|
||||
/** Version of MolViewSpec used to write this file. */
|
||||
version: string,
|
||||
}
|
||||
export const GlobalMetadata = {
|
||||
create(metadata?: Pick<GlobalMetadata, 'title' | 'description' | 'description_format'>): GlobalMetadata {
|
||||
return {
|
||||
...metadata,
|
||||
version: `${MVSData.SupportedVersion}`,
|
||||
timestamp: utcNowISO(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/** Metadata for an individual snapshot. */
|
||||
export interface SnapshotMetadata {
|
||||
/** Name of this snapshot. */
|
||||
title?: string,
|
||||
/** Detailed description of this snapshot. */
|
||||
description?: string,
|
||||
/** Format of `description`. Default is 'markdown'. */
|
||||
description_format?: 'markdown' | 'plaintext',
|
||||
/** Unique identifier of this state, useful when working with collections of states. */
|
||||
key?: string,
|
||||
/** Timespan for snapshot. */
|
||||
linger_duration_ms: number,
|
||||
/** Timespan for the animation to the next snapshot. Leave empty to skip animations. */
|
||||
transition_duration_ms?: number,
|
||||
}
|
||||
|
||||
export interface Snapshot {
|
||||
/** Root of the node tree */
|
||||
root: MVSTree,
|
||||
/** Associated metadata */
|
||||
metadata: SnapshotMetadata,
|
||||
}
|
||||
|
||||
/** MVSData with a single state */
|
||||
export interface MVSData_State {
|
||||
kind?: 'single',
|
||||
/** Root of the node tree */
|
||||
root: MVSTree,
|
||||
/** Associated metadata */
|
||||
metadata: GlobalMetadata,
|
||||
}
|
||||
|
||||
/** MVSData with multiple states (snapshots) */
|
||||
export interface MVSData_States {
|
||||
kind: 'multiple',
|
||||
/** Ordered collection of individual states */
|
||||
snapshots: Snapshot[],
|
||||
/** Associated metadata */
|
||||
metadata: GlobalMetadata,
|
||||
}
|
||||
|
||||
/** Top level of the MolViewSpec (MVS) data format. */
|
||||
export type MVSData = MVSData_State | MVSData_States
|
||||
|
||||
|
||||
export const MVSData = {
|
||||
/** Currently supported major version of MolViewSpec format (e.g. 1 for version '1.0.8') */
|
||||
@@ -108,31 +62,15 @@ export const MVSData = {
|
||||
* If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */
|
||||
validationIssues(mvsData: MVSData, options: { noExtra?: boolean } = {}): string[] | undefined {
|
||||
const version = mvsData?.metadata?.version;
|
||||
if (typeof version !== 'string') return [`MVSData.metadata.version must be a string, not ${typeof version}: ${version}`];
|
||||
if (mvsData.kind === 'single' || mvsData.kind === undefined) {
|
||||
return snapshotValidationIssues(mvsData, options);
|
||||
} else if (mvsData.kind === 'multiple') {
|
||||
if (mvsData.snapshots === undefined) return [`"snapshots" missing in MVS`];
|
||||
const issues: string[] = [];
|
||||
for (const snapshot of mvsData.snapshots) { // would use .flatMap if it didn't work in a completely unpredictable way
|
||||
const snapshotIssues = snapshotValidationIssues(snapshot, options);
|
||||
if (snapshotIssues) issues.push(...snapshotIssues);
|
||||
}
|
||||
if (issues.length > 0) return issues;
|
||||
else return undefined;
|
||||
} else {
|
||||
return [`MVSData.kind must be 'single' or 'multiple', not ${mvsData.kind}`];
|
||||
}
|
||||
if (typeof version !== 'string') return [`"version" in MVS must be a string, not ${typeof version}: ${version}`];
|
||||
if (mvsData.root === undefined) return [`"root" missing in MVS`];
|
||||
return treeValidationIssues(MVSTreeSchema, mvsData.root, options);
|
||||
},
|
||||
|
||||
/** Return a human-friendly textual representation of `mvsData`. */
|
||||
toPrettyString(mvsData: MVSData): string {
|
||||
const type = mvsData.kind === 'multiple' ? 'multiple states' : 'single state';
|
||||
const title = mvsData.metadata.title !== undefined ? ` "${mvsData.metadata.title}"` : '';
|
||||
const trees = mvsData.kind === 'multiple' ?
|
||||
mvsData.snapshots.map((s, i) => `[Snapshot #${i}]\n${treeToString(s.root)}`).join('\n')
|
||||
: treeToString(mvsData.root);
|
||||
return `MolViewSpec ${type}${title} (version ${mvsData.metadata.version}, created ${mvsData.metadata.timestamp}):\n${trees}`;
|
||||
return `MolViewSpec tree${title} (version ${mvsData.metadata.version}, created ${mvsData.metadata.timestamp}):\n${treeToString(mvsData.root)}`;
|
||||
},
|
||||
|
||||
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
|
||||
@@ -148,15 +86,6 @@ export const MVSData = {
|
||||
createBuilder(): Root {
|
||||
return createMVSBuilder();
|
||||
},
|
||||
|
||||
/** Create a multi-state MVS data from a list of snapshots. */
|
||||
createMultistate(snapshots: Snapshot[], metadata?: Pick<GlobalMetadata, 'title' | 'description' | 'description_format'>): MVSData_States {
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots: [...snapshots],
|
||||
metadata: GlobalMetadata.create(metadata),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -167,13 +96,3 @@ function majorVersion(semanticVersion: string | number): number | undefined {
|
||||
console.error(`Version should be a string, not ${typeof semanticVersion}: ${semanticVersion}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: { noExtra?: boolean } = {}): string[] | undefined {
|
||||
if (snapshot.root === undefined) return [`"root" missing in snapshot`];
|
||||
return treeValidationIssues(MVSTreeSchema, snapshot.root, options);
|
||||
}
|
||||
|
||||
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
|
||||
function utcNowISO(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { RequiredField, fieldValidationIssues, float, int, literal, nullable, str, union } from '../field-schema';
|
||||
|
||||
|
||||
describe('fieldValidationIssues', () => {
|
||||
it('fieldValidationIssues string', async () => {
|
||||
const stringField = RequiredField(str, 'Testing required field stringField');
|
||||
expect(fieldValidationIssues(stringField, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringField, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringField, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringField, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringField, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues string choice', async () => {
|
||||
const colorParam = RequiredField(literal('red', 'green', 'blue', 'yellow'), 'Testing required field colorParam');
|
||||
expect(fieldValidationIssues(colorParam, 'red')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'green')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'blue')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'yellow')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'banana')).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues number choice', async () => {
|
||||
const numberParam = RequiredField(literal(1, 2, 3, 4), 'Testing required field numberParam');
|
||||
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 2)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 3)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 4)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues int', async () => {
|
||||
const numberParam = RequiredField(int, 'Testing required field numberParam');
|
||||
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 0)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 0.5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues union', async () => {
|
||||
const stringOrNumberParam = RequiredField(union([str, float]), 'Testing required field stringOrNumberParam');
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, true)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues nullable', async () => {
|
||||
const stringOrNullParam = RequiredField(nullable(str), 'Testing required field stringOrNullParam');
|
||||
expect(fieldValidationIssues(stringOrNullParam, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, null)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, 1)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNullParam, true)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNullParam, undefined)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,85 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { OptionalField, RequiredField, bool, float, int, str } from '../field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema, paramsValidationIssues } from '../params-schema';
|
||||
import * as iots from 'io-ts';
|
||||
|
||||
import { fieldValidationIssues, RequiredField, literal, nullable, paramsValidationIssues, OptionalField } from '../params-schema';
|
||||
|
||||
|
||||
const simpleSchema = SimpleParamsSchema({
|
||||
name: OptionalField(str, 'Anonymous', 'Testing optional field name'),
|
||||
surname: RequiredField(str, 'Testing optional field surname'),
|
||||
lunch: RequiredField(bool, 'Testing optional field lunch'),
|
||||
age: OptionalField(int, 0, 'Testing optional field age'),
|
||||
describe('fieldValidationIssues', () => {
|
||||
it('fieldValidationIssues string', async () => {
|
||||
const stringField = RequiredField(iots.string);
|
||||
expect(fieldValidationIssues(stringField, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringField, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringField, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringField, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringField, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues string choice', async () => {
|
||||
const colorParam = RequiredField(literal('red', 'green', 'blue', 'yellow'));
|
||||
expect(fieldValidationIssues(colorParam, 'red')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'green')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'blue')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'yellow')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'banana')).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues number choice', async () => {
|
||||
const numberParam = RequiredField(literal(1, 2, 3, 4));
|
||||
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 2)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 3)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 4)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues int', async () => {
|
||||
const numberParam = RequiredField(iots.Integer);
|
||||
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 0)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 0.5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues union', async () => {
|
||||
const stringOrNumberParam = RequiredField(iots.union([iots.string, iots.number]));
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, true)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues nullable', async () => {
|
||||
const stringOrNullParam = RequiredField(nullable(iots.string));
|
||||
expect(fieldValidationIssues(stringOrNullParam, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, null)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, 1)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNullParam, true)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNullParam, undefined)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
const schema = {
|
||||
name: OptionalField(iots.string),
|
||||
surname: RequiredField(iots.string),
|
||||
lunch: RequiredField(iots.boolean),
|
||||
age: OptionalField(iots.number),
|
||||
};
|
||||
|
||||
describe('validateParams', () => {
|
||||
it('validateParams', async () => {
|
||||
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(simpleSchema, {}, { noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
|
||||
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
|
||||
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, {}, { noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('validateFullParams', () => {
|
||||
it('validateFullParams', async () => {
|
||||
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(simpleSchema, {}, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { requireAll: true, noExtra: true })).toBeTruthy(); // wrong type of `age`
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: true })).toBeTruthy(); // extra param `married`
|
||||
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: false })).toBeUndefined(); // extra param `married`
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, {}, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { requireAll: true, noExtra: true })).toBeTruthy(); // wrong type of `age`
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: true })).toBeTruthy(); // extra param `married`
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: false })).toBeUndefined(); // extra param `married`
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const unionSchema = UnionParamsSchema(
|
||||
'kind',
|
||||
'Description for "kind"',
|
||||
{
|
||||
person: SimpleParamsSchema({
|
||||
name: OptionalField(str, 'Anonymous', 'Testing optional field name'),
|
||||
surname: RequiredField(str, 'Testing optional field surname'),
|
||||
lunch: RequiredField(bool, 'Testing optional field lunch'),
|
||||
age: OptionalField(int, 0, 'Testing optional field age'),
|
||||
}),
|
||||
object: SimpleParamsSchema({
|
||||
weight: RequiredField(float, 'Testing optional field weight'),
|
||||
color: OptionalField(str, 'colorless', 'Testing optional field color'),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
describe('validateUnionParams', () => {
|
||||
it('validateUnionParams', async () => {
|
||||
expect(paramsValidationIssues(unionSchema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeTruthy(); // missing discriminator param `kind`
|
||||
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person' }, { noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
|
||||
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42, color: 'black' }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'object', color: 'black' }, { noExtra: true })).toBeTruthy(); // missing param `weight`
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42, name: 'John' }, { noExtra: true })).toBeTruthy(); // extra param `name`
|
||||
|
||||
expect(paramsValidationIssues(unionSchema, { kind: 'spanish_inquisition' }, { noExtra: true })).toBeTruthy(); // unexpected value for discriminator param `kind`
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
import { PathReporter } from 'io-ts/PathReporter';
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
|
||||
|
||||
/** All types that can be used in tree node params.
|
||||
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
|
||||
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {};
|
||||
|
||||
/** Type definition for a string */
|
||||
export const str = iots.string;
|
||||
/** Type definition for an integer */
|
||||
export const int = iots.Integer;
|
||||
/** Type definition for a float or integer number */
|
||||
export const float = iots.number;
|
||||
/** Type definition for a boolean */
|
||||
export const bool = iots.boolean;
|
||||
/** Type definition for a tuple, e.g. `tuple([str, int, int])` */
|
||||
export const tuple = iots.tuple;
|
||||
/** Type definition for a list/array, e.g. `list(str)` */
|
||||
export const list = iots.array;
|
||||
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
|
||||
export const union = iots.union;
|
||||
/** Type definition used to create objects */
|
||||
export const obj = iots.type;
|
||||
/** Type definition used to create partial objects */
|
||||
export const partial = iots.partial;
|
||||
|
||||
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
|
||||
export function nullable<T extends iots.Type<any>>(type: T) {
|
||||
return union([type, iots.null]);
|
||||
}
|
||||
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
|
||||
export function literal<V extends string | number | boolean>(...values: V[]) {
|
||||
if (values.length === 0) {
|
||||
throw new Error(`literal type must have at least one value`);
|
||||
}
|
||||
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
|
||||
return new iots.Type<V>(
|
||||
typeName,
|
||||
((value: any) => values.includes(value)) as any,
|
||||
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
|
||||
value => value
|
||||
);
|
||||
}
|
||||
/** Type definition for mapping between two types, e.g. `mapping(str, float)` means type `{ [key in string]: number }` */
|
||||
export function mapping<A extends iots.Type<any>, B extends iots.Type<any>>(from: A, to: B) {
|
||||
return iots.record(from, to);
|
||||
}
|
||||
|
||||
|
||||
interface FieldBase<V extends AllowedValueTypes = any, R extends boolean = boolean> {
|
||||
/** Definition of allowed types for the field */
|
||||
type: iots.Type<V>,
|
||||
/** If `required===true`, the value must always be defined in molviewspec format (can be `null` if `type` allows it).
|
||||
* If `required===false`, the value can be ommitted (meaning that a default should be used).
|
||||
* If `type` allows `null`, the default must be `null`. */
|
||||
required: R,
|
||||
/** Description of what the field value means */
|
||||
description: string,
|
||||
}
|
||||
|
||||
/** Schema for param field which must always be provided (has no default value) */
|
||||
export interface RequiredField<V extends AllowedValueTypes = any> extends FieldBase<V> {
|
||||
required: true,
|
||||
}
|
||||
export function RequiredField<V extends AllowedValueTypes>(type: iots.Type<V>, description: string): RequiredField<V> {
|
||||
return { type, required: true, description };
|
||||
}
|
||||
|
||||
/** Schema for param field which can be dropped (meaning that a default value will be used) */
|
||||
export interface OptionalField<V extends AllowedValueTypes = any> extends FieldBase<V> {
|
||||
required: false,
|
||||
/** Default value for optional field.
|
||||
* If field type allows `null`, default must be `null` (this is to avoid issues in languages that do not distinguish `null` and `undefined`). */
|
||||
default: DefaultValue<V>,
|
||||
}
|
||||
export function OptionalField<V extends AllowedValueTypes>(type: iots.Type<V>, defaultValue: DefaultValue<V>, description: string): OptionalField<V> {
|
||||
return { type, required: false, description, default: defaultValue };
|
||||
}
|
||||
|
||||
/** Schema for one field in params (i.e. a value in a top-level key-value pair) */
|
||||
export type Field<V extends AllowedValueTypes = any> = RequiredField<V> | OptionalField<V>;
|
||||
|
||||
/** Type of valid default value for value type `V` (if the type allows `null`, the default must be `null`) */
|
||||
type DefaultValue<V extends AllowedValueTypes> = null extends V ? null : V;
|
||||
|
||||
/** Type of valid value for field of type `F` (never includes `undefined`, even if field is optional) */
|
||||
export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V : F extends iots.Any ? iots.TypeOf<F> : never;
|
||||
|
||||
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
|
||||
* Return description of validation issues, if `value` has wrong type. */
|
||||
export function fieldValidationIssues<F extends Field>(field: F, value: any): string[] | undefined {
|
||||
const validation = field.type.decode(value);
|
||||
if (validation._tag === 'Right') {
|
||||
return undefined;
|
||||
} else {
|
||||
return PathReporter.report(validation);
|
||||
}
|
||||
}
|
||||
@@ -5,175 +5,153 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { isPlainObject, mapObjectMap, omitObjectKeys } from '../../../../mol-util/object';
|
||||
import { Field, fieldValidationIssues, OptionalField, RequiredField, ValueFor } from './field-schema';
|
||||
import * as iots from 'io-ts';
|
||||
import { PathReporter } from 'io-ts/PathReporter';
|
||||
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
|
||||
|
||||
type Fields = { [key in string]: Field };
|
||||
/** All types that can be used in tree node params.
|
||||
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
|
||||
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {}
|
||||
|
||||
/** Type of `ParamsSchema` where all fields are completely independent */
|
||||
export interface SimpleParamsSchema<TFields extends Fields = Fields> {
|
||||
type: 'simple',
|
||||
/** Parameter fields */
|
||||
fields: TFields,
|
||||
/** Type definition for a string */
|
||||
export const str = iots.string;
|
||||
/** Type definition for an integer */
|
||||
export const int = iots.Integer;
|
||||
/** Type definition for a float or integer number */
|
||||
export const float = iots.number;
|
||||
/** Type definition for a boolean */
|
||||
export const bool = iots.boolean;
|
||||
/** Type definition for a tuple, e.g. `tuple([str, int, int])` */
|
||||
export const tuple = iots.tuple;
|
||||
/** Type definition for a list/array, e.g. `list(str)` */
|
||||
export const list = iots.array;
|
||||
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
|
||||
export const union = iots.union;
|
||||
/** Type definition used to create objects */
|
||||
export const obj = iots.type;
|
||||
/** Type definition used to create partial objects */
|
||||
export const partial = iots.partial;
|
||||
|
||||
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
|
||||
export function nullable<T extends iots.Type<any>>(type: T) {
|
||||
return union([type, iots.null]);
|
||||
}
|
||||
export function SimpleParamsSchema<TFields extends Fields>(fields: TFields): SimpleParamsSchema<TFields> {
|
||||
return { type: 'simple', fields };
|
||||
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
|
||||
export function literal<V extends string | number | boolean>(...values: V[]) {
|
||||
if (values.length === 0) {
|
||||
throw new Error(`literal type must have at least one value`);
|
||||
}
|
||||
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
|
||||
return new iots.Type<V>(
|
||||
typeName,
|
||||
((value: any) => values.includes(value)) as any,
|
||||
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
|
||||
value => value
|
||||
);
|
||||
}
|
||||
/** Mapping between two types */
|
||||
export function mapping<A extends iots.Type<any>, B extends iots.Type<any>>(from: A, to: B) {
|
||||
return iots.record(from, to);
|
||||
}
|
||||
|
||||
type ValuesForFields<F extends Fields> =
|
||||
{ [key in keyof F as (F[key] extends RequiredField<any> ? key : never)]: ValueFor<F[key]> }
|
||||
& { [key in keyof F as (F[key] extends OptionalField<any> ? key : never)]?: ValueFor<F[key]> };
|
||||
|
||||
type ValuesForSimpleParamsSchema<TSchema extends SimpleParamsSchema> = ValuesForFields<TSchema['fields']>;
|
||||
|
||||
type AllRequiredFields<F extends Fields>
|
||||
= { [key in keyof F]: F[key] extends Field<infer V> ? RequiredField<V> : never };
|
||||
|
||||
type AllRequiredSimple<TSchema extends SimpleParamsSchema> = SimpleParamsSchema<AllRequiredFields<TSchema['fields']>>;
|
||||
|
||||
|
||||
type Cases = { [case_ in string]: SimpleParamsSchema };
|
||||
// Tried to have this recursive ({ [case_ in string]: ParamsSchema }) but ran into "ts(2589) Type instantiation is excessively deep..."
|
||||
|
||||
/** Type of `ParamsSchema` where one field (discriminator) determines what other fields are allowed (i.e. discriminated union type) */
|
||||
export interface UnionParamsSchema<TDiscriminator extends string = string, TCases extends Cases = Cases> {
|
||||
type: 'union',
|
||||
/** Name of parameter field that determines the rest (allowed values are defined by keys of `cases`) */
|
||||
discriminator: TDiscriminator,
|
||||
/** Description for the discriminator parameter field */
|
||||
discriminatorDescription: string,
|
||||
/** `ParamsSchema` for the rest, for each case of discriminator value */
|
||||
cases: TCases,
|
||||
/** Schema for one field in params (i.e. a value in a top-level key-value pair) */
|
||||
interface Field<V extends AllowedValueTypes = any, R extends boolean = boolean> {
|
||||
/** Definition of allowed types for the field */
|
||||
type: iots.Type<V>,
|
||||
/** If `required===true`, the value must always be defined in molviewspec format (can be `null` if `type` allows it).
|
||||
* If `required===false`, the value can be ommitted (meaning that a default should be used).
|
||||
* If `type` allows `null`, the default must be `null`. */
|
||||
required: R,
|
||||
/** Description of what the field value means */
|
||||
description?: string,
|
||||
}
|
||||
export function UnionParamsSchema<TDiscriminator extends string, TCases extends Cases>(discriminator: TDiscriminator, discriminatorDescription: string, cases: TCases): UnionParamsSchema<TDiscriminator, TCases> {
|
||||
return { type: 'union', discriminator, discriminatorDescription, cases };
|
||||
/** Schema for param field which must always be provided (has no default value) */
|
||||
export interface RequiredField<V extends AllowedValueTypes = any> extends Field<V> {
|
||||
required: true,
|
||||
}
|
||||
export function RequiredField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): RequiredField<V> {
|
||||
return { type, required: true, description };
|
||||
}
|
||||
|
||||
type ValuesForUnionParamsSchema<TSchema extends UnionParamsSchema, TCase extends keyof TSchema['cases'] = keyof TSchema['cases']>
|
||||
= TCase extends keyof TSchema['cases'] ? { [discriminator in TSchema['discriminator']]: TCase } & ValuesFor<TSchema['cases'][TCase]> : never;
|
||||
// `extends` clause seems superfluous here, but is needed to properly create discriminated union type
|
||||
|
||||
type AllRequiredUnion<TSchema extends UnionParamsSchema>
|
||||
= UnionParamsSchema<TSchema['discriminator'], { [case_ in keyof TSchema['cases']]: AllRequired<TSchema['cases'][case_]> }>;
|
||||
|
||||
|
||||
/** Schema for "params", i.e. a flat collection of key-value pairs */
|
||||
export type ParamsSchema = SimpleParamsSchema | UnionParamsSchema;
|
||||
|
||||
/** Type of values for a params schema (optional fields can be missing) */
|
||||
export type ValuesFor<P extends ParamsSchema>
|
||||
= P extends SimpleParamsSchema ? ValuesForSimpleParamsSchema<P> : P extends UnionParamsSchema ? ValuesForUnionParamsSchema<P> : never;
|
||||
|
||||
/** Variation of a params schema where all fields are required */
|
||||
export type AllRequired<P extends ParamsSchema>
|
||||
= P extends SimpleParamsSchema ? AllRequiredSimple<P> : P extends UnionParamsSchema ? AllRequiredUnion<P> : never;
|
||||
|
||||
function AllRequiredSimple<TSchema extends SimpleParamsSchema>(schema: TSchema): AllRequired<TSchema> {
|
||||
const newFields = mapObjectMap(schema.fields, field => RequiredField(field.type, field.description));
|
||||
return SimpleParamsSchema(newFields) as AllRequired<TSchema>;
|
||||
/** Schema for param field which can be dropped (meaning that a default value will be used) */
|
||||
export interface OptionalField<V extends AllowedValueTypes = any> extends Field<V> {
|
||||
required: false,
|
||||
}
|
||||
function AllRequiredUnion<TSchema extends UnionParamsSchema>(schema: TSchema): AllRequired<TSchema> {
|
||||
const newCases = mapObjectMap(schema.cases, AllRequired);
|
||||
return UnionParamsSchema(schema.discriminator, schema.discriminatorDescription, newCases) as AllRequired<TSchema>;
|
||||
export function OptionalField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): OptionalField<V> {
|
||||
return { type, required: false, description };
|
||||
}
|
||||
export function AllRequired<TSchema extends ParamsSchema>(schema: TSchema): AllRequired<TSchema> {
|
||||
if (schema.type === 'simple') {
|
||||
return AllRequiredSimple(schema) as AllRequired<TSchema>;
|
||||
|
||||
/** Type of valid value for field of type `F` (never includes `undefined`, even if field is optional) */
|
||||
export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V : F extends iots.Any ? iots.TypeOf<F> : never
|
||||
|
||||
/** Type of valid default value for field of type `F` (if the field's type allows `null`, the default must be `null`) */
|
||||
export type DefaultFor<F extends Field> = F extends Field<infer V> ? (null extends V ? null : V) : never
|
||||
|
||||
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
|
||||
* Return description of validation issues, if `value` has wrong type. */
|
||||
export function fieldValidationIssues<F extends Field, V>(field: F, value: V): V extends ValueFor<F> ? undefined : string[] {
|
||||
const validation = field.type.decode(value);
|
||||
if (validation._tag === 'Right') {
|
||||
return undefined as any;
|
||||
} else {
|
||||
return AllRequiredUnion(schema) as AllRequired<TSchema>;
|
||||
return PathReporter.report(validation) as any;
|
||||
}
|
||||
}
|
||||
|
||||
/** Type of full values for a params schema, i.e. including all optional fields */
|
||||
export type FullValuesFor<P extends ParamsSchema> = ValuesFor<AllRequired<P>>;
|
||||
|
||||
/** Schema for "params", i.e. a flat collection of key-value pairs */
|
||||
export type ParamsSchema<TKey extends string = string> = { [key in TKey]: Field }
|
||||
|
||||
interface ValidationOptions {
|
||||
/** Check that all parameters (including optional) have a value provided. */
|
||||
requireAll?: boolean,
|
||||
/** Check there are extra parameters other that those defined in the schema. */
|
||||
noExtra?: boolean,
|
||||
/** Variation of a params schema where all fields are required */
|
||||
export type AllRequired<TParamsSchema extends ParamsSchema> = { [key in keyof TParamsSchema]: TParamsSchema[key] extends Field<infer V> ? RequiredField<V> : never }
|
||||
export function AllRequired<TParamsSchema extends ParamsSchema>(paramsSchema: TParamsSchema): AllRequired<TParamsSchema> {
|
||||
return mapObjectMap(paramsSchema, field => RequiredField(field.type, field.description)) as AllRequired<TParamsSchema>;
|
||||
}
|
||||
|
||||
/** Type of values for a params schema (optional fields can be missing) */
|
||||
export type ValuesFor<P extends ParamsSchema> =
|
||||
{ [key in keyof P as (P[key] extends RequiredField<any> ? key : never)]: ValueFor<P[key]> }
|
||||
& { [key in keyof P as (P[key] extends OptionalField<any> ? key : never)]?: ValueFor<P[key]> }
|
||||
|
||||
/** Type of full values for a params schema, i.e. including all optional fields */
|
||||
export type FullValuesFor<P extends ParamsSchema> = { [key in keyof P]: ValueFor<P[key]> }
|
||||
|
||||
/** Type of default values for a params schema, i.e. including only optional fields */
|
||||
export type DefaultsFor<P extends ParamsSchema> = { [key in keyof P as (P[key] extends Field<any, false> ? key : never)]: ValueFor<P[key]> }
|
||||
|
||||
|
||||
/** Return `undefined` if `values` contains correct value types for `schema`,
|
||||
* return description of validation issues, if `values` have wrong type.
|
||||
* If `options.requireAll`, all parameters (including optional) must have a value provided.
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue. */
|
||||
export function paramsValidationIssues<P extends ParamsSchema>(schema: P, values: { [k: string]: any }, options: ValidationOptions = {}): string[] | undefined {
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
|
||||
*/
|
||||
export function paramsValidationIssues<P extends ParamsSchema, V extends { [k: string]: any }>(schema: P, values: V, options: { requireAll?: boolean, noExtra?: boolean } = {}): string[] | undefined {
|
||||
if (!isPlainObject(values)) return [`Parameters must be an object, not ${values}`];
|
||||
for (const key in schema) {
|
||||
const paramDef = schema[key];
|
||||
|
||||
if (schema.type === 'simple') {
|
||||
return simpleParamsValidationIssue(schema, values, options);
|
||||
} else {
|
||||
return unionParamsValidationIssues(schema, values, options);
|
||||
}
|
||||
}
|
||||
|
||||
function simpleParamsValidationIssue(schema: SimpleParamsSchema, values: { [k: string]: any }, options: ValidationOptions): string[] | undefined {
|
||||
for (const key in schema.fields) {
|
||||
const fieldSchema = schema.fields[key];
|
||||
// Special handling of "union" param type
|
||||
// TODO: figure out how to do this properly, ignoring the validation for now
|
||||
if (key === '_union_') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Object.hasOwn(values, key)) {
|
||||
const value = values[key];
|
||||
const issues = fieldValidationIssues(fieldSchema, value);
|
||||
if (issues) return [`Invalid value for parameter "${key}":`, ...issues.map(s => ' ' + s)];
|
||||
const issues = fieldValidationIssues(paramDef, value);
|
||||
if (issues) return [`Invalid type for parameter "${key}":`, ...issues.map(s => ' ' + s)];
|
||||
} else {
|
||||
if (fieldSchema.required) return [`Missing required parameter "${key}".`];
|
||||
if (paramDef.required) return [`Missing required parameter "${key}".`];
|
||||
if (options.requireAll) return [`Missing optional parameter "${key}".`];
|
||||
}
|
||||
}
|
||||
if (options.noExtra) {
|
||||
for (const key in values) {
|
||||
if (!Object.hasOwn(schema.fields, key)) return [`Unknown parameter "${key}".`];
|
||||
if (!Object.hasOwn(schema, key)) return [`Unknown parameter "${key}".`];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function unionParamsValidationIssues(schema: UnionParamsSchema, values: { [k: string]: any }, options: ValidationOptions): string[] | undefined {
|
||||
if (!Object.hasOwn(values, schema.discriminator)) {
|
||||
return [`Missing required parameter "${schema.discriminator}".`];
|
||||
}
|
||||
const case_ = values[schema.discriminator];
|
||||
const subschema = schema.cases[case_];
|
||||
if (subschema === undefined) {
|
||||
const allowedCases = Object.keys(schema.cases).map(x => `"${x}"`).join(' | ');
|
||||
return [
|
||||
`Invalid value for parameter "${schema.discriminator}":`,
|
||||
`"${case_}" is not a valid value for literal type (${allowedCases})`,
|
||||
];
|
||||
}
|
||||
const issues = paramsValidationIssues(subschema, omitObjectKeys(values, [schema.discriminator]), options);
|
||||
if (issues) {
|
||||
issues.unshift(`(case "${schema.discriminator}": "${case_}")`);
|
||||
return issues.map(s => ' ' + s);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Add default parameter values to `values` based on a parameter schema (only for optional parameters) */
|
||||
export function addParamDefaults<P extends ParamsSchema>(schema: P, values: ValuesFor<P>): FullValuesFor<P> {
|
||||
if (schema.type === 'simple') {
|
||||
return addSimpleParamsDefaults(schema, values);
|
||||
} else {
|
||||
return addUnionParamsDefaults(schema, values);
|
||||
}
|
||||
}
|
||||
|
||||
function addSimpleParamsDefaults(schema: SimpleParamsSchema, values: any): any {
|
||||
const out = { ...values };
|
||||
for (const key in schema.fields) {
|
||||
const field = schema.fields[key];
|
||||
if (!field.required && out[key] === undefined) {
|
||||
out[key] = field.default;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function addUnionParamsDefaults(schema: UnionParamsSchema, values: any): any {
|
||||
const case_ = values[schema.discriminator];
|
||||
const subschema = schema.cases[case_];
|
||||
return addParamDefaults(subschema, values);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
|
||||
import { Field } from './field-schema';
|
||||
import { AllRequired, ParamsSchema, SimpleParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
|
||||
import { AllRequired, DefaultsFor, ParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
|
||||
import { treeToString } from './tree-utils';
|
||||
|
||||
|
||||
@@ -86,7 +85,7 @@ export interface TreeSchema<TParamsSchemas extends ParamsSchemas = ParamsSchemas
|
||||
},
|
||||
}
|
||||
export function TreeSchema<P extends ParamsSchemas = ParamsSchemas, R extends keyof P = string>(schema: TreeSchema<P, R>): TreeSchema<P, R> {
|
||||
return schema;
|
||||
return schema as any;
|
||||
}
|
||||
|
||||
/** ParamsSchemas per node kind */
|
||||
@@ -115,6 +114,9 @@ export type NodeFor<TTreeSchema extends TreeSchema, TKind extends keyof ParamsSc
|
||||
/** Type of tree which conforms to tree schema `TTreeSchema` */
|
||||
export type TreeFor<TTreeSchema extends TreeSchema> = Tree<NodeFor<TTreeSchema>, RootFor<TTreeSchema> & NodeFor<TTreeSchema>>
|
||||
|
||||
/** Type of default parameter values for each node kind in a tree schema `TTreeSchema` */
|
||||
export type DefaultsForTree<TTreeSchema extends TreeSchema> = { [kind in keyof TTreeSchema['nodes']]: DefaultsFor<TTreeSchema['nodes'][kind]['params']> }
|
||||
|
||||
|
||||
/** Return `undefined` if a tree conforms to the given schema,
|
||||
* return validation issues (as a list of lines) if it does not conform.
|
||||
@@ -158,14 +160,14 @@ export function validateTree(schema: TreeSchema, tree: Tree, label: string): voi
|
||||
}
|
||||
|
||||
/** Return documentation for a tree schema as plain text */
|
||||
export function treeSchemaToString<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, false);
|
||||
export function treeSchemaToString<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
|
||||
return treeSchemaToString_(schema, defaults, false);
|
||||
}
|
||||
/** Return documentation for a tree schema as markdown text */
|
||||
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, true);
|
||||
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
|
||||
return treeSchemaToString_(schema, defaults, true);
|
||||
}
|
||||
function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean = false): string {
|
||||
function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>, markdown: boolean = false): string {
|
||||
const out: string[] = [];
|
||||
const bold = (str: string) => markdown ? `**${str}**` : str;
|
||||
const code = (str: string) => markdown ? `\`${str}\`` : str;
|
||||
@@ -173,8 +175,6 @@ function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean
|
||||
const p1 = markdown ? '' : ' ';
|
||||
const h2 = markdown ? '- ' : ' - ';
|
||||
const p2 = markdown ? ' ' : ' ';
|
||||
const h3 = markdown ? ' - ' : ' - ';
|
||||
const p3 = markdown ? ' ' : ' ';
|
||||
const newline = markdown ? '\n\n' : '\n';
|
||||
out.push(`Tree schema:`);
|
||||
for (const kind in schema.nodes) {
|
||||
@@ -188,46 +188,21 @@ function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean
|
||||
}
|
||||
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
|
||||
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
|
||||
if (params.type === 'simple') {
|
||||
formatSimpleParams(out, params, { h: h2, p: p2, code, bold });
|
||||
} else {
|
||||
const key = params.discriminator;
|
||||
const casesStr = Object.keys(params.cases).join(' | ');
|
||||
out.push(`${h2}${bold(code(key + ': '))}${code(casesStr)}`);
|
||||
if (params.discriminatorDescription) {
|
||||
out.push(`${p2}${params.discriminatorDescription}`);
|
||||
for (const key in params) {
|
||||
const field = params[key];
|
||||
let typeString = field.type.name;
|
||||
if (typeString.startsWith('(') && typeString.endsWith(')')) {
|
||||
typeString = typeString.slice(1, -1);
|
||||
}
|
||||
out.push(`${p2}[This parameter determines the rest of parameters]`);
|
||||
for (const case_ in params.cases) {
|
||||
const caseStr = `${params.discriminator}: "${case_}"`;
|
||||
out.push(`${p2}${bold(`Case ${code(caseStr)}:`)}`);
|
||||
formatSimpleParams(out, params.cases[case_], { h: h3, p: p3, code, bold });
|
||||
out.push(`${h2}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(typeString)}`);
|
||||
const defaultValue = (defaults?.[kind] as any)?.[key];
|
||||
if (field.description) {
|
||||
out.push(`${p2}${field.description}`);
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
out.push(`${p2}Default: ${code(onelinerJsonString(defaultValue))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.join(newline);
|
||||
}
|
||||
|
||||
function formatSimpleParams(out: string[], params: SimpleParamsSchema, formatting: { h: string, p: string, code: (str: string) => string, bold: (str: string) => string }): void {
|
||||
const { h, p, code, bold } = formatting;
|
||||
for (const key in params.fields) {
|
||||
const field = params.fields[key];
|
||||
out.push(`${h}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(formatFieldType(field))}`);
|
||||
const defaultValue = field.required ? undefined : field.default;
|
||||
if (field.description) {
|
||||
out.push(`${p}${field.description}`);
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
out.push(`${p}Default: ${code(onelinerJsonString(defaultValue))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatFieldType(field: Field): string {
|
||||
const typeString = field.type.name;
|
||||
if (typeString.startsWith('(') && typeString.endsWith(')')) {
|
||||
return typeString.slice(1, -1);
|
||||
} else {
|
||||
return typeString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import { canonicalJsonString } from '../../../../mol-util/json';
|
||||
import { addParamDefaults } from './params-schema';
|
||||
import { CustomProps, Kind, Node, Subtree, SubtreeOfKind, Tree, TreeFor, TreeSchema, TreeSchemaWithAllRequired, getParams } from './tree-schema';
|
||||
import { CustomProps, DefaultsForTree, Kind, Node, Subtree, SubtreeOfKind, Tree, TreeFor, TreeSchema, TreeSchemaWithAllRequired, getParams } from './tree-schema';
|
||||
|
||||
|
||||
/** Run DFS (depth-first search) algorithm on a rooted tree.
|
||||
@@ -149,13 +148,12 @@ export function condenseTree<T extends Tree>(root: T, condenseNodes?: Set<Kind<T
|
||||
}
|
||||
|
||||
/** Create a copy of the tree where missing optional params for each node are added based on `defaults`. */
|
||||
export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, treeSchema: S): TreeFor<TreeSchemaWithAllRequired<S>> {
|
||||
type TTree = TreeFor<S>;
|
||||
const rules: ConversionRules<TTree, TTree> = {};
|
||||
for (const kind in treeSchema.nodes) {
|
||||
rules[kind as Kind<Subtree<TTree>>] = node => [{
|
||||
export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, defaults: DefaultsForTree<S>): TreeFor<TreeSchemaWithAllRequired<S>> {
|
||||
const rules: ConversionRules<TreeFor<S>, TreeFor<S>> = {};
|
||||
for (const kind in defaults) {
|
||||
rules[kind] = node => [{
|
||||
kind: node.kind,
|
||||
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
|
||||
params: { ...defaults[kind], ...node.params },
|
||||
custom: node.custom,
|
||||
ref: node.ref,
|
||||
} as Node as any];
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { ConversionRules, addDefaults, condenseTree, convertTree, dfs, resolveUris } from '../generic/tree-utils';
|
||||
import { FullMVSTree, MVSTree, MVSTreeSchema } from '../mvs/mvs-tree';
|
||||
import { MolstarParseFormatT, ParseFormatT } from '../mvs/param-types';
|
||||
import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
|
||||
import { FullMVSTree, MVSTree, MVSTreeSchema } from '../mvs/mvs-tree';
|
||||
import { MVSDefaults } from '../mvs/mvs-defaults';
|
||||
import { MolstarParseFormatT, ParseFormatT } from '../mvs/param-types';
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
|
||||
|
||||
/** Convert `format` parameter of `parse` node in `MolstarTree`
|
||||
@@ -52,7 +53,7 @@ const molstarNodesToCondense = new Set<MolstarKind>(['download', 'parse', 'traje
|
||||
|
||||
/** Convert MolViewSpec tree into MolStar tree */
|
||||
export function convertMvsToMolstar(mvsTree: MVSTree, sourceUrl: string | undefined): MolstarTree {
|
||||
const full = addDefaults<typeof MVSTreeSchema>(mvsTree, MVSTreeSchema) as FullMVSTree;
|
||||
const full = addDefaults<typeof MVSTreeSchema>(mvsTree, MVSDefaults) as FullMVSTree;
|
||||
if (sourceUrl) resolveUris(full, sourceUrl, ['uri', 'url']);
|
||||
const converted = convertTree<FullMVSTree, MolstarTree>(full, mvsToMolstarConversionRules);
|
||||
if (converted.kind !== 'root') throw new Error("Root's type is not 'root' after conversion from MVS tree to Molstar tree.");
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { RequiredField, bool } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema } from '../generic/params-schema';
|
||||
import { RequiredField, bool } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
|
||||
import { FullMVSTreeSchema } from '../mvs/mvs-tree';
|
||||
import { MolstarParseFormatT } from '../mvs/param-types';
|
||||
@@ -19,41 +18,37 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
...FullMVSTreeSchema.nodes,
|
||||
download: {
|
||||
...FullMVSTreeSchema.nodes.download,
|
||||
params: SimpleParamsSchema({
|
||||
...FullMVSTreeSchema.nodes.download.params.fields,
|
||||
is_binary: RequiredField(bool, 'Specifies whether file is downloaded as bytes array or string'),
|
||||
}),
|
||||
params: {
|
||||
...FullMVSTreeSchema.nodes.download.params,
|
||||
is_binary: RequiredField(bool),
|
||||
},
|
||||
},
|
||||
parse: {
|
||||
...FullMVSTreeSchema.nodes.parse,
|
||||
params: SimpleParamsSchema({
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
}),
|
||||
params: {
|
||||
format: RequiredField(MolstarParseFormatT),
|
||||
},
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
|
||||
trajectory: {
|
||||
description: "Auxiliary node corresponding to Molstar's TrajectoryFrom*.",
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index'] as const),
|
||||
}),
|
||||
params: {
|
||||
format: RequiredField(MolstarParseFormatT),
|
||||
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index'] as const),
|
||||
},
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's ModelFromTrajectory. */
|
||||
model: {
|
||||
description: "Auxiliary node corresponding to Molstar's ModelFromTrajectory.",
|
||||
parent: ['trajectory'],
|
||||
params: SimpleParamsSchema(
|
||||
pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['model_index'] as const)
|
||||
),
|
||||
params: pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['model_index'] as const),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's StructureFromModel. */
|
||||
structure: {
|
||||
...FullMVSTreeSchema.nodes.structure,
|
||||
parent: ['model'],
|
||||
params: SimpleParamsSchema(
|
||||
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index'] as const)
|
||||
),
|
||||
params: omitObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index', 'model_index'] as const),
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { MVSData } from '../../../mvs-data';
|
||||
import { treeValidationIssues } from '../../generic/tree-schema';
|
||||
import { builderDemo } from '../mvs-builder';
|
||||
import { MVSTreeSchema } from '../mvs-tree';
|
||||
|
||||
|
||||
describe('mvs-builder', () => {
|
||||
@@ -13,6 +14,6 @@ describe('mvs-builder', () => {
|
||||
const mvsData = builderDemo();
|
||||
expect(typeof mvsData.metadata.version).toEqual('string');
|
||||
expect(typeof mvsData.metadata.timestamp).toEqual('string');
|
||||
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
|
||||
expect(treeValidationIssues(MVSTreeSchema, mvsData.root)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
*/
|
||||
|
||||
import { deepClone, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { GlobalMetadata, MVSData_State, Snapshot, SnapshotMetadata } from '../../mvs-data';
|
||||
import { MVSData } from '../../mvs-data';
|
||||
import { CustomProps } from '../generic/tree-schema';
|
||||
import { MVSKind, MVSNode, MVSNodeParams, MVSSubtree } from './mvs-tree';
|
||||
import { MVSDefaults } from './mvs-defaults';
|
||||
import { MVSKind, MVSNode, MVSSubtree, MVSNodeParams, MVSTreeSchema } from './mvs-tree';
|
||||
|
||||
|
||||
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
|
||||
@@ -27,16 +28,17 @@ export function createMVSBuilder(params: CustomAndRef = {}) {
|
||||
|
||||
/** Base class for MVS builder pointing to anything */
|
||||
class _Base<TKind extends MVSKind> {
|
||||
constructor(
|
||||
protected constructor(
|
||||
protected readonly _root: Root,
|
||||
protected readonly _node: MVSSubtree<TKind>,
|
||||
) { }
|
||||
/** Create a new node, append as child to current _node, and return the new node */
|
||||
protected addChild<TChildKind extends MVSKind>(kind: TChildKind, params_: MVSNodeParams<TChildKind> & CustomAndRef) {
|
||||
const { params, custom, ref } = splitParams<MVSNodeParams<TChildKind>>(params_);
|
||||
const allowedParamNames = Object.keys(MVSTreeSchema.nodes[kind].params) as (keyof MVSNodeParams<TChildKind>)[];
|
||||
const node = {
|
||||
kind,
|
||||
params,
|
||||
params: pickObjectKeys(params, allowedParamNames) as unknown,
|
||||
custom,
|
||||
ref,
|
||||
} as MVSSubtree<TChildKind>;
|
||||
@@ -48,7 +50,7 @@ class _Base<TKind extends MVSKind> {
|
||||
|
||||
|
||||
/** MVS builder pointing to the 'root' node */
|
||||
export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
export class Root extends _Base<'root'> {
|
||||
constructor(params_: CustomAndRef) {
|
||||
const { custom, ref } = params_;
|
||||
const node: MVSNode<'root'> = { kind: 'root', custom, ref };
|
||||
@@ -56,20 +58,17 @@ export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
(this._root as Root) = this;
|
||||
}
|
||||
/** Return the current state of the builder as object in MVS format. */
|
||||
getState(metadata?: Pick<GlobalMetadata, 'title' | 'description' | 'description_format'>): MVSData_State {
|
||||
getState(metadata?: Partial<Pick<MVSData['metadata'], 'title' | 'description' | 'description_format'>>): MVSData {
|
||||
return {
|
||||
root: deepClone(this._node),
|
||||
metadata: GlobalMetadata.create(metadata),
|
||||
metadata: {
|
||||
...metadata,
|
||||
version: `${MVSData.SupportedVersion}`,
|
||||
timestamp: utcNowISO(),
|
||||
},
|
||||
};
|
||||
}
|
||||
// omitting `saveState`, filesystem operations are responsibility of the caller code (platform-dependent)
|
||||
/** Return the current state of the builder as a snapshot object to be used in multi-state . */
|
||||
getSnapshot(metadata: SnapshotMetadata): Snapshot {
|
||||
return {
|
||||
root: deepClone(this._node),
|
||||
metadata: { ...metadata },
|
||||
};
|
||||
}
|
||||
|
||||
/** Add a 'camera' node and return builder pointing to the root. 'camera' node instructs to set the camera position and orientation. */
|
||||
camera(params: MVSNodeParams<'camera'> & CustomAndRef): Root {
|
||||
@@ -85,9 +84,6 @@ export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
download(params: MVSNodeParams<'download'> & CustomAndRef): Download {
|
||||
return new Download(this._root, this.addChild('download', params));
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
|
||||
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
|
||||
}
|
||||
|
||||
|
||||
@@ -155,10 +151,10 @@ export class Parse extends _Base<'parse'> {
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'structure' node */
|
||||
export class Structure extends _Base<'structure'> implements PrimitivesMixin {
|
||||
export class Structure extends _Base<'structure'> {
|
||||
/** Add a 'component' node and return builder pointing to it. 'component' node instructs to create a component (i.e. a subset of the parent structure). */
|
||||
component(params: Partial<MVSNodeParams<'component'>> & CustomAndRef = {}): Component {
|
||||
const fullParams = { ...params, selector: params.selector ?? 'all' };
|
||||
const fullParams = { ...params, selector: params.selector ?? MVSDefaults.component.selector };
|
||||
return new Component(this._root, this.addChild('component', fullParams));
|
||||
}
|
||||
/** Add a 'component_from_uri' node and return builder pointing to it. 'component_from_uri' node instructs to create a component defined by an external annotation resource. */
|
||||
@@ -197,13 +193,11 @@ export class Structure extends _Base<'structure'> implements PrimitivesMixin {
|
||||
this.addChild('transform', params);
|
||||
return this;
|
||||
}
|
||||
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
|
||||
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
|
||||
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> implements FocusMixin {
|
||||
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> {
|
||||
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
|
||||
representation(params: Partial<MVSNodeParams<'representation'>> & CustomAndRef = {}): Representation {
|
||||
const fullParams: MVSNodeParams<'representation'> = { ...params, type: params.type ?? 'cartoon' };
|
||||
@@ -219,7 +213,11 @@ export class Component extends _Base<'component' | 'component_from_uri' | 'compo
|
||||
this.addChild('tooltip', params);
|
||||
return this;
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
/** Add a 'focus' node and return builder pointing back to the component node. 'focus' node instructs to set the camera focus to a component (zoom in). */
|
||||
focus(params: MVSNodeParams<'focus'> & CustomAndRef = {}): Component {
|
||||
this.addChild('focus', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -240,95 +238,9 @@ export class Representation extends _Base<'representation'> {
|
||||
this.addChild('color_from_source', params);
|
||||
return this;
|
||||
}
|
||||
/** Add an 'opacity' node and return builder pointing back to the representation node. 'opacity' node instructs to customize opacity/transparency of a visual representation. */
|
||||
opacity(params: MVSNodeParams<'opacity'> & CustomAndRef): Representation {
|
||||
this.addChild('opacity', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type MVSPrimitiveSubparams<TKind extends MVSNodeParams<'primitive'>['kind']> = Omit<Extract<MVSNodeParams<'primitive'>, { kind: TKind }>, 'kind'>;
|
||||
|
||||
/** MVS builder pointing to a 'primitives' node */
|
||||
class Primitives extends _Base<'primitives'> implements FocusMixin {
|
||||
/** Construct custom meshes/shapes in a low-level fashion by providing vertices and indices. */
|
||||
mesh(params: MVSPrimitiveSubparams<'mesh'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'mesh', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Construct custom set of lines in a low-level fashion by providing vertices and indices. */
|
||||
lines(params: MVSPrimitiveSubparams<'lines'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'lines', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a tube (3D cylinder), connecting a start and an end point. */
|
||||
tube(params: MVSPrimitiveSubparams<'tube'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'tube', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a tube, connecting a start and an end point, with label containing distance between start and end. */
|
||||
distance(params: MVSPrimitiveSubparams<'distance_measurement'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'distance_measurement', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a label. */
|
||||
label(params: MVSPrimitiveSubparams<'label'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'label', ...params });
|
||||
return this;
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'primitives_from_uri' node */
|
||||
class PrimitivesFromUri extends _Base<'primitives_from_uri'> implements FocusMixin {
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
}
|
||||
|
||||
|
||||
// MIXINS
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
/** Fake interface for typing tweaks */
|
||||
interface Self { '@type': 'self' }
|
||||
|
||||
type ReplaceSelf<TFunction, TSelf> = TFunction extends (...args: infer TArgs) => Self ? (...args: TArgs) => TSelf : TFunction;
|
||||
|
||||
function bindMethod<O extends _Base<any>, C extends Constructor<_Base<any>>, M extends keyof InstanceType<C>>(thisObj: O, mixin: C, methodName: M): ReplaceSelf<InstanceType<C>[M], O> {
|
||||
return mixin.prototype[methodName].bind(thisObj);
|
||||
}
|
||||
|
||||
// This mixin implementation is really ugly but couldn't be bothered (running into TS2502: 'Root' is referenced directly or indirectly in its own type annotation)
|
||||
|
||||
interface FocusMixin {
|
||||
/** Add a 'focus' node and return builder pointing back to the original node. 'focus' node instructs to set the camera focus to a component (zoom in). */
|
||||
focus(params: MVSNodeParams<'focus'> & CustomAndRef): any,
|
||||
}
|
||||
class FocusMixinImpl extends _Base<MVSKind> implements FocusMixin {
|
||||
focus(params: MVSNodeParams<'focus'> & CustomAndRef = {}): Self {
|
||||
this.addChild('focus', params);
|
||||
return this as unknown as Self;
|
||||
}
|
||||
};
|
||||
|
||||
interface PrimitivesMixin {
|
||||
/** Allows the definition of a (group of) geometric primitives. You can add any number of primitives and then assign shared options (color, opacity etc.). */
|
||||
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef): Primitives,
|
||||
/** Allows the definition of a (group of) geometric primitives provided dynamically. */
|
||||
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri,
|
||||
};
|
||||
class PrimitivesMixinImpl extends _Base<MVSKind> implements PrimitivesMixin {
|
||||
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef = {}): Primitives {
|
||||
return new Primitives(this._root, this.addChild('primitives', params));
|
||||
}
|
||||
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri {
|
||||
return new PrimitivesFromUri(this._root, this.addChild('primitives_from_uri', params));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** Demonstration of usage of MVS builder */
|
||||
export function builderDemo() {
|
||||
const builder = createMVSBuilder();
|
||||
@@ -355,6 +267,11 @@ export function builderDemo() {
|
||||
return builder.getState();
|
||||
}
|
||||
|
||||
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
|
||||
function utcNowISO(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
export interface CustomAndRef {
|
||||
custom?: CustomProps,
|
||||
ref?: string,
|
||||
|
||||
120
src/extensions/mvs/tree/mvs/mvs-defaults.ts
Normal file
120
src/extensions/mvs/tree/mvs/mvs-defaults.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { DefaultsForTree } from '../generic/tree-schema';
|
||||
import { MVSTreeSchema } from './mvs-tree';
|
||||
|
||||
|
||||
/** Default values for params in `MVSTree` */
|
||||
export const MVSDefaults = {
|
||||
root: {},
|
||||
download: {
|
||||
},
|
||||
parse: {
|
||||
},
|
||||
structure: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
model_index: 0,
|
||||
assembly_id: null,
|
||||
radius: 5,
|
||||
ijk_min: [-1, -1, -1],
|
||||
ijk_max: [1, 1, 1],
|
||||
},
|
||||
component: {
|
||||
selector: 'all' as const,
|
||||
},
|
||||
component_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'component',
|
||||
field_values: null,
|
||||
},
|
||||
component_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'component',
|
||||
field_values: null,
|
||||
},
|
||||
representation: {
|
||||
},
|
||||
color: {
|
||||
selector: 'all' as const,
|
||||
},
|
||||
color_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'color',
|
||||
},
|
||||
color_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'color',
|
||||
},
|
||||
transparency: {
|
||||
},
|
||||
label: {
|
||||
},
|
||||
label_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'label',
|
||||
},
|
||||
label_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'label',
|
||||
},
|
||||
tooltip: {
|
||||
},
|
||||
tooltip_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'tooltip',
|
||||
},
|
||||
tooltip_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'tooltip',
|
||||
},
|
||||
focus: {
|
||||
direction: [0, 0, -1],
|
||||
up: [0, 1, 0],
|
||||
},
|
||||
transform: {
|
||||
rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1], // 3x3 identitity matrix
|
||||
translation: [0, 0, 0],
|
||||
},
|
||||
canvas: {
|
||||
},
|
||||
camera: {
|
||||
up: [0, 1, 0],
|
||||
},
|
||||
primitives: {
|
||||
color: null,
|
||||
label_color: null,
|
||||
tooltip: null,
|
||||
transparency: null,
|
||||
label_transparency: null,
|
||||
instances: null,
|
||||
},
|
||||
primitives_from_uri: {
|
||||
references: null,
|
||||
},
|
||||
primitive: { },
|
||||
} satisfies DefaultsForTree<typeof MVSTreeSchema>;
|
||||
|
||||
/** Color to be used e.g. for representations without 'color' node */
|
||||
export const DefaultColor = 'white';
|
||||
81
src/extensions/mvs/tree/mvs/mvs-primitives.ts
Normal file
81
src/extensions/mvs/tree/mvs/mvs-primitives.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, int, literal, mapping, nullable, obj, str, union, ValueFor } from '../generic/params-schema';
|
||||
import type { MVSNode } from './mvs-tree';
|
||||
import { ColorT, FloatList, IntList, PrimitivePositionT, StrList } from './param-types';
|
||||
|
||||
// TODO: Figure out validation and default values for these
|
||||
|
||||
const _LineBase = {
|
||||
start: PrimitivePositionT,
|
||||
end: PrimitivePositionT,
|
||||
thickness: nullable(float),
|
||||
color: nullable(ColorT),
|
||||
dash_length: nullable(float),
|
||||
};
|
||||
|
||||
const MeshParams = obj({
|
||||
kind: literal('mesh'),
|
||||
vertices: FloatList,
|
||||
indices: IntList,
|
||||
triangle_colors: nullable(StrList),
|
||||
triangle_groups: nullable(IntList),
|
||||
group_colors: nullable(mapping(int, ColorT)),
|
||||
group_tooltips: nullable(mapping(int, str)),
|
||||
tooltip: nullable(str),
|
||||
show_triangles: nullable(bool),
|
||||
show_wireframe: nullable(bool),
|
||||
color: nullable(ColorT),
|
||||
wireframe_radius: nullable(float),
|
||||
wireframe_color: nullable(ColorT),
|
||||
});
|
||||
|
||||
const LinesParams = obj({
|
||||
kind: literal('lines'),
|
||||
vertices: FloatList,
|
||||
indices: IntList,
|
||||
line_colors: nullable(StrList),
|
||||
line_groups: nullable(IntList),
|
||||
group_colors: nullable(mapping(int, ColorT)),
|
||||
group_tooltips: nullable(mapping(int, str)),
|
||||
group_radius: nullable(mapping(int, float)),
|
||||
tooltip: nullable(str),
|
||||
color: nullable(ColorT),
|
||||
line_radius: nullable(float),
|
||||
});
|
||||
|
||||
const LineParams = obj({
|
||||
kind: literal('line'),
|
||||
..._LineBase,
|
||||
tooltip: nullable(str),
|
||||
});
|
||||
|
||||
const DistanceMeasurementParams = obj({
|
||||
kind: literal('distance_measurement'),
|
||||
..._LineBase,
|
||||
label_template: nullable(str),
|
||||
label_size: nullable(union([float, literal('auto')])),
|
||||
label_auto_size_scale: nullable(float),
|
||||
label_auto_size_min: nullable(float),
|
||||
label_color: nullable(ColorT),
|
||||
});
|
||||
|
||||
const PrimitiveLabelParams = obj({
|
||||
kind: literal('label'),
|
||||
position: PrimitivePositionT,
|
||||
text: str,
|
||||
label_size: nullable(float),
|
||||
label_color: nullable(ColorT),
|
||||
label_offset: nullable(float),
|
||||
});
|
||||
|
||||
export const MVSPrimitiveParams = union([MeshParams, LinesParams, LineParams, DistanceMeasurementParams, PrimitiveLabelParams]);
|
||||
|
||||
export type MVSPrimitive = ValueFor<typeof MVSPrimitiveParams>
|
||||
export type MVSPrimitiveKind = MVSPrimitive['kind']
|
||||
export type MVSPrimitiveOptions = MVSNode<'primitives'>['params']
|
||||
export type MVSPrimitiveParams<T extends MVSPrimitiveKind = MVSPrimitiveKind> = Extract<MVSPrimitive, { kind: T }>
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
import { ColorT, FloatList, IntList, PrimitivePositionT } from './param-types';
|
||||
|
||||
|
||||
const _TubeBase = {
|
||||
/** Start point of the tube. */
|
||||
start: RequiredField(PrimitivePositionT, 'Start point of the tube.'),
|
||||
/** End point of the tube. */
|
||||
end: RequiredField(PrimitivePositionT, 'End point of the tube.'),
|
||||
/** Tube radius (in Angstroms). */
|
||||
radius: OptionalField(float, 0.05, 'Tube radius (in Angstroms).'),
|
||||
/** Length of each dash and gap between dashes. If not specified (null), draw full line. */
|
||||
dash_length: OptionalField(nullable(float), null, 'Length of each dash and gap between dashes. If not specified (null), draw full line.'),
|
||||
/** Color of the tube. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the tube. If not specified, uses the parent primitives group `color`.'),
|
||||
};
|
||||
|
||||
const MeshParams = {
|
||||
/** 3*n_vertices length array of floats with vertex position (x1, y1, z1, ...). */
|
||||
vertices: RequiredField(FloatList, '3*n_vertices length array of floats with vertex position (x1, y1, z1, ...).'),
|
||||
/** 3*n_triangles length array of indices into vertices that form triangles (t1_1, t1_2, t1_3, ...). */
|
||||
indices: RequiredField(IntList, '3*n_triangles length array of indices into vertices that form triangles (t1_1, t1_2, t1_3, ...).'),
|
||||
/** Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i). */
|
||||
triangle_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i).'),
|
||||
/** Assign a color to each group. Where not assigned, uses `color`. */
|
||||
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
|
||||
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
|
||||
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
|
||||
/** Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
|
||||
/** Tooltip shown when hovering over the mesh. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip shown when hovering over the mesh. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
/** Determine whether to render triangles of the mesh. */
|
||||
show_triangles: OptionalField(bool, true, 'Determine whether to render triangles of the mesh.'),
|
||||
/** Determine whether to render wireframe of the mesh. */
|
||||
show_wireframe: OptionalField(bool, false, 'Determine whether to render wireframe of the mesh.'),
|
||||
/** Wireframe line width (in screen-space units). */
|
||||
wireframe_width: OptionalField(float, 1, 'Wireframe line width (in screen-space units).'),
|
||||
/** Wireframe color. If not specified, uses `group_colors`. */
|
||||
wireframe_color: OptionalField(nullable(ColorT), null, 'Wireframe color. If not specified, uses `group_colors`.'),
|
||||
};
|
||||
|
||||
const LinesParams = {
|
||||
/** 3*n_vertices length array of floats with vertex position (x1, y1, z1, ...). */
|
||||
vertices: RequiredField(FloatList, '3*n_vertices length array of floats with vertex position (x1, y1, z1, ...).'),
|
||||
/** 2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...). */
|
||||
indices: RequiredField(IntList, '2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...).'),
|
||||
/** Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i). */
|
||||
line_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i).'),
|
||||
/** Assign a color to each group. Where not assigned, uses `color`. */
|
||||
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
|
||||
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
|
||||
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
|
||||
/** Assign a line width to each group. Where not assigned, uses `width`. */
|
||||
group_widths: OptionalField(mapping(int, float), {}, 'Assign a line width to each group. Where not assigned, uses `width`.'),
|
||||
/** Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
|
||||
/** Tooltip shown when hovering over the lines. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip shown when hovering over the lines. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
/** Line width (in screen-space units). Can be overwritten by `group_widths`. */
|
||||
width: OptionalField(float, 1, 'Line width (in screen-space units). Can be overwritten by `group_widths`.'),
|
||||
};
|
||||
|
||||
const TubeParams = {
|
||||
..._TubeBase,
|
||||
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
};
|
||||
|
||||
const DistanceMeasurementParams = {
|
||||
..._TubeBase,
|
||||
/** Template used to construct the label. Use {{distance}} as placeholder for the distance. */
|
||||
label_template: OptionalField(str, '{{distance}}', 'Template used to construct the label. Use {{distance}} as placeholder for the distance.'),
|
||||
/** Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min). */
|
||||
label_size: OptionalField(nullable(float), null, 'Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min).'),
|
||||
/** Scaling factor for relative size. */
|
||||
label_auto_size_scale: OptionalField(float, 0.1, 'Scaling factor for relative size.'),
|
||||
/** Minimum size for relative size. */
|
||||
label_auto_size_min: OptionalField(float, 0, 'Minimum size for relative size.'),
|
||||
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
|
||||
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
|
||||
};
|
||||
|
||||
const PrimitiveLabelParams = {
|
||||
/** Position of this label. */
|
||||
position: RequiredField(PrimitivePositionT, 'Position of this label.'),
|
||||
/** The label. */
|
||||
text: RequiredField(str, 'The label.'),
|
||||
/** Size of the label (text height in Angstroms). */
|
||||
label_size: OptionalField(float, 1, 'Size of the label (text height in Angstroms).'),
|
||||
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
|
||||
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
|
||||
/** Camera-facing offset to prevent overlap with geometry. */
|
||||
label_offset: OptionalField(float, 0, 'Camera-facing offset to prevent overlap with geometry.'),
|
||||
};
|
||||
|
||||
export const MVSPrimitiveParams = UnionParamsSchema(
|
||||
'kind',
|
||||
'Kind of geometrical primitive',
|
||||
{
|
||||
'mesh': SimpleParamsSchema(MeshParams),
|
||||
'lines': SimpleParamsSchema(LinesParams),
|
||||
'tube': SimpleParamsSchema(TubeParams),
|
||||
'distance_measurement': SimpleParamsSchema(DistanceMeasurementParams),
|
||||
'label': SimpleParamsSchema(PrimitiveLabelParams),
|
||||
},
|
||||
);
|
||||
@@ -5,10 +5,9 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema } from '../generic/params-schema';
|
||||
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
|
||||
import { MVSPrimitiveParams } from './mvs-tree-primitives';
|
||||
import { MVSPrimitiveParams } from './mvs-primitives';
|
||||
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, RepresentationTypeT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
|
||||
|
||||
|
||||
@@ -20,26 +19,26 @@ const _DataFromUriParams = {
|
||||
/** Annotation schema defines what fields in the annotation will be taken into account. */
|
||||
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
|
||||
/** Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`. */
|
||||
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.'),
|
||||
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.'),
|
||||
/** 0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`). */
|
||||
block_index: OptionalField(int, 0, '0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).'),
|
||||
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).'),
|
||||
/** Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used. */
|
||||
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
|
||||
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
|
||||
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
};
|
||||
|
||||
const _DataFromSourceParams = {
|
||||
/** Annotation schema defines what fields in the annotation will be taken into account. */
|
||||
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
|
||||
/** Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`. */
|
||||
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.'),
|
||||
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.'),
|
||||
/** 0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`). */
|
||||
block_index: OptionalField(int, 0, '0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).'),
|
||||
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).'),
|
||||
/** Name of the CIF category to read annotation from. If `null`, the first category in the block is used. */
|
||||
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
|
||||
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
|
||||
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
};
|
||||
|
||||
/** Schema for `MVSTree` (MolViewSpec tree) */
|
||||
@@ -50,274 +49,246 @@ export const MVSTreeSchema = TreeSchema({
|
||||
root: {
|
||||
description: 'Auxiliary node kind that only appears as the tree root.',
|
||||
parent: [],
|
||||
params: SimpleParamsSchema({
|
||||
}),
|
||||
params: {
|
||||
},
|
||||
},
|
||||
/** This node instructs to retrieve a data resource. */
|
||||
download: {
|
||||
description: 'This node instructs to retrieve a data resource.',
|
||||
parent: ['root'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** URL of the data resource. */
|
||||
url: RequiredField(str, 'URL of the data resource.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to parse a data resource. */
|
||||
parse: {
|
||||
description: 'This node instructs to parse a data resource.',
|
||||
parent: ['download'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Format of the input data resource. */
|
||||
format: RequiredField(ParseFormatT, 'Format of the input data resource.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation. */
|
||||
structure: {
|
||||
description: 'This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.',
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model). */
|
||||
type: RequiredField(StructureTypeT, 'Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model).'),
|
||||
/** Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`. */
|
||||
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.'),
|
||||
block_header: OptionalField(nullable(str), 'Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.'),
|
||||
/** 0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`). */
|
||||
block_index: OptionalField(int, 0, '0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).'),
|
||||
block_index: OptionalField(int, '0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).'),
|
||||
/** 0-based index of model in case the input data contain multiple models. */
|
||||
model_index: OptionalField(int, 0, '0-based index of model in case the input data contain multiple models.'),
|
||||
model_index: OptionalField(int, '0-based index of model in case the input data contain multiple models.'),
|
||||
/** Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected. */
|
||||
assembly_id: OptionalField(nullable(str), null, 'Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.'),
|
||||
assembly_id: OptionalField(nullable(str), 'Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.'),
|
||||
/** Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`). */
|
||||
radius: OptionalField(float, 5, 'Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).'),
|
||||
radius: OptionalField(float, 'Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).'),
|
||||
/** Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`). */
|
||||
ijk_min: OptionalField(tuple([int, int, int]), [-1, -1, -1], 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
ijk_min: OptionalField(tuple([int, int, int]), 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
/** Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`). */
|
||||
ijk_max: OptionalField(tuple([int, int, int]), [1, 1, 1], 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
}),
|
||||
ijk_max: OptionalField(tuple([int, int, int]), 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to rotate and/or translate structure coordinates. */
|
||||
transform: {
|
||||
description: 'This node instructs to rotate and/or translate structure coordinates.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation). */
|
||||
rotation: OptionalField(Matrix, [1, 0, 0, 0, 1, 0, 0, 0, 1], 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
|
||||
rotation: OptionalField(Matrix, 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
|
||||
/** Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation). */
|
||||
translation: OptionalField(Vector3, [0, 0, 0], 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
|
||||
}),
|
||||
translation: OptionalField(Vector3, 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a component (i.e. a subset of the parent structure). */
|
||||
component: {
|
||||
description: 'This node instructs to create a component (i.e. a subset of the parent structure).',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Defines what part of the parent structure should be included in this component. */
|
||||
selector: RequiredField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines what part of the parent structure should be included in this component.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a component defined by an external annotation resource. */
|
||||
component_from_uri: {
|
||||
description: 'This node instructs to create a component defined by an external annotation resource.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the component identifier. */
|
||||
field_name: OptionalField(str, 'component', 'Name of the column in CIF or field name (key) in JSON that contains the component identifier.'),
|
||||
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
|
||||
field_values: OptionalField(nullable(list(str)), null, 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
|
||||
}),
|
||||
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
component_from_source: {
|
||||
description: 'This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the component identifier. */
|
||||
field_name: OptionalField(str, 'component', 'Name of the column in CIF or field name (key) in JSON that contains the component identifier.'),
|
||||
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
|
||||
field_values: OptionalField(nullable(list(str)), null, 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
|
||||
}),
|
||||
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a visual representation of a component. */
|
||||
representation: {
|
||||
description: 'This node instructs to create a visual representation of a component.',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Method of visual representation of the component. */
|
||||
type: RequiredField(RepresentationTypeT, 'Method of visual representation of the component.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to apply color to a visual representation. */
|
||||
color: {
|
||||
description: 'This node instructs to apply color to a visual representation.',
|
||||
parent: ['representation'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
color: RequiredField(ColorT, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
/** Defines to what part of the representation this color should be applied. */
|
||||
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'all', 'Defines to what part of the representation this color should be applied.'),
|
||||
}),
|
||||
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines to what part of the representation this color should be applied.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
|
||||
color_from_uri: {
|
||||
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource.',
|
||||
parent: ['representation'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
|
||||
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
color_from_source: {
|
||||
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['representation'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
|
||||
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to apply opacity/transparency to a visual representation. */
|
||||
opacity: {
|
||||
description: 'This node instructs to apply opacity/transparency to a visual representation.',
|
||||
/** This node instructs to apply transparency to a visual representation. */
|
||||
transparency: {
|
||||
description: 'This node instructs to apply transparency to a visual representation.',
|
||||
parent: ['representation'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque. */
|
||||
opacity: RequiredField(float, 'Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque.'),
|
||||
}),
|
||||
params: {
|
||||
/** Transparency of the representation. 0.0: fully opaque, 1.0: fully transparent. */
|
||||
transparency: RequiredField(float, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add a label (textual visual representation) to a component. */
|
||||
label: {
|
||||
description: 'This node instructs to add a label (textual visual representation) to a component.',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Content of the shown label. */
|
||||
text: RequiredField(str, 'Content of the shown label.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource. */
|
||||
label_from_uri: {
|
||||
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
|
||||
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
label_from_source: {
|
||||
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
|
||||
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component). */
|
||||
tooltip: {
|
||||
description: 'This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component).',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Content of the shown tooltip. */
|
||||
text: RequiredField(str, 'Content of the shown tooltip.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource. */
|
||||
tooltip_from_uri: {
|
||||
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
|
||||
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
tooltip_from_source: {
|
||||
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
|
||||
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
/** This node instructs to set the camera focus to a component (zoom in). */
|
||||
focus: {
|
||||
description: 'This node instructs to set the camera focus to a component (zoom in).',
|
||||
parent: ['root', 'component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri'],
|
||||
params: SimpleParamsSchema({
|
||||
parent: ['component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri'],
|
||||
params: {
|
||||
/** Vector describing the direction of the view (camera position -> focused target). */
|
||||
direction: OptionalField(Vector3, [0, 0, -1], 'Vector describing the direction of the view (camera position -> focused target).'),
|
||||
direction: OptionalField(Vector3, 'Vector describing the direction of the view (camera position -> focused target).'),
|
||||
/** Vector which will be aligned with the screen Y axis. */
|
||||
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
|
||||
/** Radius of the focused sphere (overrides `radius_factor` and `radius_extra`. */
|
||||
radius: OptionalField(nullable(float), null, 'Radius of the focused sphere (overrides `radius_factor` and `radius_extra`).'),
|
||||
/** Radius of the focused sphere relative to the radius of parent component (default: 1). Focused radius = component_radius * radius_factor + radius_extent. */
|
||||
radius_factor: OptionalField(float, 1, 'Radius of the focused sphere relative to the radius of parent component (default: 1). Focused radius = component_radius * radius_factor + radius_extent.'),
|
||||
/** Addition to the radius of the focused sphere, if computed from the radius of parent component (default: 0). Focused radius = component_radius * radius_factor + radius_extent. */
|
||||
radius_extent: OptionalField(float, 0, 'Addition to the radius of the focused sphere, if computed from the radius of parent component (default: 0). Focused radius = component_radius * radius_factor + radius_extent.'),
|
||||
}),
|
||||
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to set the camera position and orientation. */
|
||||
camera: {
|
||||
description: 'This node instructs to set the camera position and orientation.',
|
||||
parent: ['root'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Coordinates of the point in space at which the camera is pointing. */
|
||||
target: RequiredField(Vector3, 'Coordinates of the point in space at which the camera is pointing.'),
|
||||
/** Coordinates of the camera. */
|
||||
position: RequiredField(Vector3, 'Coordinates of the camera.'),
|
||||
/** Vector which will be aligned with the screen Y axis. */
|
||||
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
|
||||
}),
|
||||
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
|
||||
},
|
||||
},
|
||||
/** This node sets canvas properties. */
|
||||
canvas: {
|
||||
description: 'This node sets canvas properties.',
|
||||
parent: ['root'],
|
||||
params: SimpleParamsSchema({
|
||||
params: {
|
||||
/** Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
background_color: RequiredField(ColorT, 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
primitives: {
|
||||
description: 'This node groups a list of geometrical primitives',
|
||||
parent: ['structure', 'root'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Default color for primitives in this group. */
|
||||
color: OptionalField(ColorT, 'white', 'Default color for primitives in this group.'),
|
||||
/** Default label color for primitives in this group. */
|
||||
label_color: OptionalField(ColorT, 'white', 'Default label color for primitives in this group.'),
|
||||
/** Default tooltip for primitives in this group. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Default tooltip for primitives in this group.'),
|
||||
/** Opacity of primitive geometry in this group. */
|
||||
opacity: OptionalField(float, 1, 'Opacity of primitive geometry in this group.'),
|
||||
/** Opacity of primitive labels in this group. */
|
||||
label_opacity: OptionalField(float, 1, 'Opacity of primitive labels in this group.'),
|
||||
/** Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices. */
|
||||
instances: OptionalField(nullable(list(Matrix)), null, 'Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices.'),
|
||||
}),
|
||||
params: {
|
||||
color: OptionalField(nullable(ColorT)),
|
||||
label_color: OptionalField(nullable(ColorT)),
|
||||
tooltip: OptionalField(nullable(str)),
|
||||
transparency: OptionalField(nullable(float)),
|
||||
label_transparency: OptionalField(nullable(float)),
|
||||
instances: OptionalField(nullable(list(Matrix))),
|
||||
},
|
||||
},
|
||||
primitives_from_uri: {
|
||||
description: 'This node loads a list of primitives from URI',
|
||||
parent: ['structure', 'root'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Location of the resource. */
|
||||
uri: RequiredField(str, 'Location of the resource.'),
|
||||
/** Format of the data. */
|
||||
format: RequiredField(literal('mvs-node-json'), 'Format of the data.'),
|
||||
/** List of nodes the data are referencing. */
|
||||
references: OptionalField(StrList, [], 'List of nodes the data are referencing.'),
|
||||
}),
|
||||
params: {
|
||||
uri: RequiredField(str),
|
||||
format: RequiredField(literal('mvs-node-json')),
|
||||
references: OptionalField(nullable(StrList)),
|
||||
},
|
||||
},
|
||||
primitive: {
|
||||
description: 'This node represents a geometrical primitive',
|
||||
parent: ['primitives'],
|
||||
params: MVSPrimitiveParams,
|
||||
params: {
|
||||
// TODO: validation
|
||||
_union_: RequiredField(MVSPrimitiveParams),
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
import { HexColor, ColorName } from '../../helpers/utils';
|
||||
import { ValueFor, float, int, list, literal, str, tuple, union } from '../generic/field-schema';
|
||||
import { HexColor } from '../../helpers/utils';
|
||||
import { ValueFor, float, int, list, literal, str, tuple, union } from '../generic/params-schema';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
|
||||
|
||||
@@ -80,19 +80,11 @@ export const HexColorT = new iots.Type<HexColor>(
|
||||
value => value
|
||||
);
|
||||
|
||||
/** `color` parameter values for `color` node in MVS tree */
|
||||
export const ColorNameT = new iots.Type<ColorName>(
|
||||
'ColorName',
|
||||
((value: any) => typeof value === 'string') as any,
|
||||
(value, ctx) => ColorName.is(value) ? { _tag: 'Right', right: value } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid hex color string` }] },
|
||||
value => value
|
||||
);
|
||||
|
||||
/** `color` parameter values for `color` node in MVS tree */
|
||||
export const ColorNamesT = literal(...Object.keys(ColorNames) as (keyof ColorNames)[]);
|
||||
|
||||
/** `color` parameter values for `color` node in MVS tree */
|
||||
export const ColorT = union([ColorNameT, HexColorT]);
|
||||
export const ColorT = union([HexColorT, ColorNamesT]);
|
||||
|
||||
/** Type helpers */
|
||||
export function isVector3(x: any): x is Vector3 {
|
||||
@@ -105,4 +97,4 @@ export function isPrimitiveComponentExpressions(x: any): x is PrimitiveComponent
|
||||
|
||||
export function isComponentExpression(x: any): x is ComponentExpressionT {
|
||||
return !!x && typeof x === 'object' && !x.expressions;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { CustomProperty } from '../../../mol-model-props/common/custom-property'
|
||||
const Colors = {
|
||||
Bond: Color(0xffffff),
|
||||
Error: Color(0x00ff00),
|
||||
MissingCharge: Color(0x66ff00),
|
||||
MissingCharge: Color(0xffffff),
|
||||
|
||||
Negative: Color(0xff0000),
|
||||
Zero: Color(0xffffff),
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
import { Column } from '../../../mol-data/db';
|
||||
|
||||
type TypeId = number;
|
||||
type IdToCharge = Map<number, number>;
|
||||
@@ -107,8 +106,6 @@ function getTypeIdToAtomIdToCharge(model: Model): SBNcbrPartialChargeData['typeI
|
||||
for (let i = 0; i < rowCount; ++i) {
|
||||
const typeId = typeIds.int(i);
|
||||
const atomId = atomIds.int(i);
|
||||
const isPresent = charges.valueKind(i) === Column.ValueKind.Present;
|
||||
if (!isPresent) continue;
|
||||
const charge = charges.float(i);
|
||||
if (!atomIdToCharge.has(typeId)) atomIdToCharge.set(typeId, new Map());
|
||||
atomIdToCharge.get(typeId)?.set(atomId, charge);
|
||||
|
||||
@@ -53,12 +53,7 @@ export class VolumeApiV2 {
|
||||
|
||||
public async getEntryList(maxEntries: number, keyword?: string): Promise<{ [source: string]: string[] }> {
|
||||
const response = await fetch(this.entryListUrl(maxEntries, keyword));
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
console.error('Failed to fetch "Volume & Segmentation" entry list');
|
||||
return {};
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async getMetadata(source: string, entryId: string): Promise<Metadata> {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2024 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>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
* @author Herman Bergwerf <post@hbergwerf.nl>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, Subject, Subscription, debounceTime, merge } from 'rxjs';
|
||||
@@ -141,7 +140,7 @@ namespace Canvas3DContext {
|
||||
preserveDrawingBuffer: true,
|
||||
preferWebGl1: false,
|
||||
|
||||
handleResize: () => { },
|
||||
handleResize: () => {},
|
||||
};
|
||||
export type Attribs = typeof DefaultAttribs
|
||||
|
||||
@@ -392,8 +391,6 @@ namespace Canvas3D {
|
||||
let y = 0;
|
||||
let width = 128;
|
||||
let height = 128;
|
||||
let canvasScaleRatioX = 1;
|
||||
let canvasScaleRatioY = 1;
|
||||
|
||||
let forceNextRender = false;
|
||||
let currentTime = 0;
|
||||
@@ -647,7 +644,7 @@ namespace Canvas3D {
|
||||
|
||||
function identify(x: number, y: number): PickData | undefined {
|
||||
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
|
||||
return webgl.isContextLost ? undefined : pickHelper.identify(x / canvasScaleRatioX, y / canvasScaleRatioY, cam);
|
||||
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
|
||||
}
|
||||
|
||||
function commit(isSynchronous: boolean = false) {
|
||||
@@ -976,10 +973,7 @@ namespace Canvas3D {
|
||||
reprRenderObjects.clear();
|
||||
scene.clear();
|
||||
helper.debug.clear();
|
||||
if (fenceSync !== null) {
|
||||
webgl.deleteSync(fenceSync);
|
||||
fenceSync = null;
|
||||
}
|
||||
if (fenceSync !== null) webgl.deleteSync(fenceSync);
|
||||
requestDraw();
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
},
|
||||
@@ -1148,10 +1142,7 @@ namespace Canvas3D {
|
||||
renderer.dispose();
|
||||
interactionHelper.dispose();
|
||||
hiZ.dispose();
|
||||
if (fenceSync !== null) {
|
||||
webgl.deleteSync(fenceSync);
|
||||
fenceSync = null;
|
||||
}
|
||||
if (fenceSync !== null) webgl.deleteSync(fenceSync);
|
||||
|
||||
removeConsoleStatsProvider(consoleStats);
|
||||
}
|
||||
@@ -1160,12 +1151,6 @@ namespace Canvas3D {
|
||||
function updateViewport() {
|
||||
const oldX = x, oldY = y, oldWidth = width, oldHeight = height;
|
||||
|
||||
const canvasRect = canvas?.getBoundingClientRect();
|
||||
canvasScaleRatioX = (canvasRect?.width ?? gl.drawingBufferWidth) / gl.drawingBufferWidth;
|
||||
if (!canvasScaleRatioX) canvasScaleRatioX = 1;
|
||||
canvasScaleRatioY = (canvasRect?.height ?? gl.drawingBufferHeight) / gl.drawingBufferHeight;
|
||||
if (!canvasScaleRatioY) canvasScaleRatioY = 1;
|
||||
|
||||
if (p.viewport.name === 'canvas') {
|
||||
x = 0;
|
||||
y = 0;
|
||||
@@ -1192,7 +1177,7 @@ namespace Canvas3D {
|
||||
pickHelper.setViewport(x, y, width, height);
|
||||
renderer.setViewport(x, y, width, height);
|
||||
Viewport.set(camera.viewport, x, y, width, height);
|
||||
Viewport.set(controls.viewport, x, y, width * canvasScaleRatioX, height * canvasScaleRatioY);
|
||||
Viewport.set(controls.viewport, x, y, width, height);
|
||||
hiZ.setViewport(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export class DpoitPass {
|
||||
resources.texture('image-float32', 'rgba', 'float', 'nearest')
|
||||
];
|
||||
} else {
|
||||
// webgl1 requires consistent bit plane counts
|
||||
// in webgl1 drawbuffers must be in the same format for some reason
|
||||
|
||||
this.depthTextures = [
|
||||
resources.texture('image-float32', 'rgba', 'float', 'nearest'),
|
||||
|
||||
@@ -47,7 +47,6 @@ export class DrawPass {
|
||||
private readonly drawTarget: RenderTarget;
|
||||
|
||||
readonly colorTarget: RenderTarget;
|
||||
readonly transparentColorTarget: RenderTarget;
|
||||
readonly depthTextureTransparent: Texture;
|
||||
readonly depthTextureOpaque: Texture;
|
||||
|
||||
@@ -92,8 +91,6 @@ export class DrawPass {
|
||||
const { extensions, resources, isWebGL2 } = webgl;
|
||||
this.drawTarget = createNullRenderTarget(webgl.gl);
|
||||
this.colorTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'linear');
|
||||
this.transparentColorTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
|
||||
|
||||
this.packedDepth = !extensions.depthTexture;
|
||||
|
||||
this.depthTargetTransparent = webgl.createRenderTarget(width, height);
|
||||
@@ -132,7 +129,6 @@ export class DrawPass {
|
||||
if (width !== w || height !== h) {
|
||||
this.colorTarget.setSize(width, height);
|
||||
this.depthTargetTransparent.setSize(width, height);
|
||||
this.transparentColorTarget.setSize(width, height);
|
||||
|
||||
if (this.depthTargetOpaque) {
|
||||
this.depthTargetOpaque.setSize(width, height);
|
||||
@@ -170,9 +166,10 @@ export class DrawPass {
|
||||
renderer.renderOpaque(scene.primitives, camera);
|
||||
}
|
||||
|
||||
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
|
||||
const dofEnabled = DofPass.isEnabled(postprocessingProps);
|
||||
|
||||
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
|
||||
if (outlineEnabled || dofEnabled) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
@@ -180,14 +177,16 @@ export class DrawPass {
|
||||
}
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
|
||||
}
|
||||
|
||||
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
|
||||
// render transparent primitives
|
||||
const isPostprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
|
||||
if (scene.opacityAverage < 1) {
|
||||
const target = isPostprocessingEnabled ? this.transparentColorTarget : this.colorTarget;
|
||||
if (isPostprocessingEnabled) {
|
||||
target.bind();
|
||||
renderer.clear(false, false, true);
|
||||
}
|
||||
const target = PostprocessingPass.isEnabled(postprocessingProps)
|
||||
? this.postprocessing.target : this.colorTarget;
|
||||
|
||||
const dpoitTextures = this.dpoit.bind();
|
||||
renderer.renderDpoitTransparent(scene.primitives, camera, this.depthTextureOpaque, dpoitTextures);
|
||||
@@ -207,10 +206,6 @@ export class DrawPass {
|
||||
this.dpoit.render();
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.render(camera, scene, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
|
||||
}
|
||||
|
||||
// render transparent volumes
|
||||
if (scene.volumes.renderables.length > 0) {
|
||||
renderer.renderVolume(scene.volumes, camera, this.depthTextureOpaque);
|
||||
@@ -228,7 +223,10 @@ export class DrawPass {
|
||||
renderer.renderOpaque(scene.primitives, camera);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
|
||||
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
|
||||
const dofEnabled = DofPass.isEnabled(postprocessingProps);
|
||||
|
||||
if (outlineEnabled || dofEnabled) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
@@ -236,38 +234,28 @@ export class DrawPass {
|
||||
}
|
||||
}
|
||||
|
||||
// render transparent primitives
|
||||
const isPostprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
|
||||
if (scene.opacityAverage < 1) {
|
||||
const target = isPostprocessingEnabled ? this.transparentColorTarget : this.colorTarget;
|
||||
if (isPostprocessingEnabled) {
|
||||
target.bind();
|
||||
renderer.clear(false, false, true);
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
|
||||
}
|
||||
|
||||
// render transparent primitives and volumes
|
||||
if (scene.opacityAverage < 1 || scene.volumes.renderables.length > 0) {
|
||||
this.wboit.bind();
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderWboitTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
if (scene.volumes.renderables.length > 0) {
|
||||
renderer.renderWboitTransparent(scene.volumes, camera, this.depthTextureOpaque);
|
||||
}
|
||||
|
||||
this.wboit.bind();
|
||||
renderer.renderWboitTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
|
||||
// evaluate wboit
|
||||
target.bind();
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.target.bind();
|
||||
} else {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
this.wboit.render();
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.render(camera, scene, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
|
||||
}
|
||||
|
||||
// render volumes
|
||||
if (scene.volumes.renderables.length > 0) {
|
||||
this.wboit.bind();
|
||||
renderer.renderWboitTransparent(scene.volumes, camera, this.depthTextureOpaque);
|
||||
|
||||
// evaluate wboit
|
||||
const target = isPostprocessingEnabled ? this.postprocessing.target : this.colorTarget;
|
||||
target.bind();
|
||||
this.wboit.render();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
|
||||
@@ -296,7 +284,10 @@ export class DrawPass {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
|
||||
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
|
||||
const dofEnabled = DofPass.isEnabled(postprocessingProps);
|
||||
|
||||
if (outlineEnabled || dofEnabled) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
@@ -304,39 +295,14 @@ export class DrawPass {
|
||||
}
|
||||
}
|
||||
|
||||
// render transparent primitives
|
||||
const isPostprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
|
||||
if (scene.opacityAverage < 1) {
|
||||
if (isPostprocessingEnabled) {
|
||||
this.transparentColorTarget.bind();
|
||||
renderer.clear(false, false, true);
|
||||
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.attachFramebuffer(this.transparentColorTarget.framebuffer, 'depth');
|
||||
} else {
|
||||
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.transparentColorTarget.framebuffer);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.renderBlendedTransparent(scene.primitives, camera);
|
||||
|
||||
if (isPostprocessingEnabled) {
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.detachFramebuffer(this.transparentColorTarget.framebuffer, 'depth');
|
||||
} else {
|
||||
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.transparentColorTarget.framebuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPostprocessingEnabled) {
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.detachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
|
||||
} else {
|
||||
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
|
||||
}
|
||||
|
||||
this.postprocessing.render(camera, scene, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
|
||||
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
|
||||
@@ -365,7 +331,9 @@ export class DrawPass {
|
||||
}
|
||||
target.bind();
|
||||
}
|
||||
} else if (scene.opacityAverage < 1) {
|
||||
}
|
||||
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderBlendedTransparent(scene.primitives, camera);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import { TracingParams, TracingPass } from './tracing';
|
||||
import { JitterVectors, MultiSampleProps } from './multi-sample';
|
||||
import { compose_frag as multiSample_compose_frag } from '../../mol-gl/shader/compose.frag';
|
||||
import { clamp, lerp } from '../../mol-math/interpolate';
|
||||
import { SsaoProps } from './ssao';
|
||||
|
||||
type Props = {
|
||||
transparentBackground: boolean;
|
||||
@@ -116,14 +115,14 @@ export class IlluminationPass {
|
||||
const width = colorTarget.getWidth();
|
||||
const height = colorTarget.getHeight();
|
||||
|
||||
this.tracing = new TracingPass(webgl, this.drawPass);
|
||||
this.tracing = new TracingPass(webgl, drawPass);
|
||||
|
||||
this.transparentTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest');
|
||||
this.outputTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
|
||||
|
||||
this.copyRenderable = createCopyRenderable(webgl, this.transparentTarget.texture);
|
||||
|
||||
this.composeRenderable = getComposeRenderable(webgl, this.tracing.accumulateTarget.texture, this.tracing.normalTextureOpaque, this.tracing.colorTextureOpaque, this.drawPass.depthTextureOpaque, this.drawPass.depthTargetTransparent.texture, this.drawPass.postprocessing.outline.target.texture, this.transparentTarget.texture, this.drawPass.postprocessing.ssao.ssaoDepthTexture, this.drawPass.postprocessing.ssao.ssaoDepthTransparentTexture, false);
|
||||
this.composeRenderable = getComposeRenderable(webgl, this.tracing.accumulateTarget.texture, this.tracing.normalTextureOpaque, this.tracing.colorTextureOpaque, this.drawPass.depthTextureOpaque, this.drawPass.depthTargetTransparent.texture, this.drawPass.postprocessing.outline.target.texture, false);
|
||||
|
||||
this.multiSampleComposeTarget = webgl.createRenderTarget(width, height, false, 'float32');
|
||||
this.multiSampleHoldTarget = webgl.createRenderTarget(width, height, false);
|
||||
@@ -145,15 +144,6 @@ export class IlluminationPass {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const outlineEnabled = PostprocessingPass.isTransparentOutlineEnabled(props.postprocessing) && !props.illumination.ignoreOutline;
|
||||
const dofEnabled = DofPass.isEnabled(props.postprocessing);
|
||||
const ssaoEnabled = PostprocessingPass.isTransparentSsaoEnabled(scene, props.postprocessing);
|
||||
|
||||
if (outlineEnabled || dofEnabled || ssaoEnabled) {
|
||||
this.drawPass.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
}
|
||||
|
||||
if (hasTransparent) {
|
||||
if (this.drawPass.transparency === 'wboit') {
|
||||
this.drawPass.wboit.bind();
|
||||
@@ -197,17 +187,16 @@ export class IlluminationPass {
|
||||
}
|
||||
}
|
||||
|
||||
if (outlineEnabled || dofEnabled || ssaoEnabled) {
|
||||
const outlineEnabled = PostprocessingPass.isEnabled(props.postprocessing) && PostprocessingPass.isTransparentOutlineEnabled(props.postprocessing) && !props.illumination.ignoreOutline;
|
||||
const dofEnabled = DofPass.isEnabled(props.postprocessing);
|
||||
|
||||
if (outlineEnabled || dofEnabled) {
|
||||
this.drawPass.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
if (ssaoEnabled) {
|
||||
this.drawPass.postprocessing.ssao.update(camera, scene, props.postprocessing.occlusion.params as SsaoProps, true);
|
||||
this.drawPass.postprocessing.ssao.render(camera);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -306,11 +295,6 @@ export class IlluminationPass {
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
|
||||
const outlinesEnabled = props.postprocessing.outline.name === 'on' && !props.illumination.ignoreOutline;
|
||||
const occlusionEnabled = PostprocessingPass.isTransparentSsaoEnabled(scene, props.postprocessing);
|
||||
|
||||
const markingEnabled = MarkingPass.isEnabled(props.marking);
|
||||
const hasTransparent = scene.opacityAverage < 1;
|
||||
const hasMarking = markingEnabled && scene.markerAverage > 0;
|
||||
|
||||
let needsUpdateCompose = false;
|
||||
|
||||
@@ -335,21 +319,6 @@ export class IlluminationPass {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.composeRenderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) {
|
||||
needsUpdateCompose = true;
|
||||
ValueCell.update(this.composeRenderable.values.dOcclusionEnable, occlusionEnabled);
|
||||
}
|
||||
|
||||
if (props.postprocessing.occlusion.name === 'on') {
|
||||
ValueCell.update(this.composeRenderable.values.uOcclusionColor, Color.toVec3Normalized(this.composeRenderable.values.uOcclusionColor.ref.value, props.postprocessing.occlusion.params.color));
|
||||
}
|
||||
|
||||
const blendTransparency = hasTransparent || hasMarking;
|
||||
if (this.composeRenderable.values.dBlendTransparency.ref.value !== blendTransparency) {
|
||||
needsUpdateCompose = true;
|
||||
ValueCell.update(this.composeRenderable.values.dBlendTransparency, blendTransparency);
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uFogFar, camera.fogFar);
|
||||
@@ -390,6 +359,16 @@ export class IlluminationPass {
|
||||
|
||||
//
|
||||
|
||||
state.enable(gl.BLEND);
|
||||
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
if (this.copyRenderable.values.tColor.ref.value !== this.transparentTarget.texture) {
|
||||
ValueCell.update(this.copyRenderable.values.tColor, this.transparentTarget.texture);
|
||||
this.copyRenderable.update();
|
||||
}
|
||||
this.copyRenderable.render();
|
||||
|
||||
//
|
||||
|
||||
renderer.setDrawingBufferSize(this.tracing.composeTarget.getWidth(), this.tracing.composeTarget.getHeight());
|
||||
renderer.setPixelRatio(this.webgl.pixelRatio);
|
||||
renderer.setViewport(x, y, width, height);
|
||||
@@ -427,7 +406,7 @@ export class IlluminationPass {
|
||||
|
||||
if (props.postprocessing.bloom.name === 'on') {
|
||||
const _toDrawingBuffer = (toDrawingBuffer && props.postprocessing.dof.name === 'off') || targetIsDrawingbuffer;
|
||||
this.drawPass.bloom.update(this.tracing.colorTextureOpaque, this.tracing.normalTextureOpaque, this.drawPass.depthTextureOpaque, props.postprocessing.bloom.params);
|
||||
this.drawPass.bloom.update(this.tracing.shadedTextureOpaque, this.tracing.normalTextureOpaque, this.drawPass.depthTextureOpaque, props.postprocessing.bloom.params);
|
||||
this.drawPass.bloom.render(camera.viewport, _toDrawingBuffer ? undefined : this._colorTarget);
|
||||
}
|
||||
|
||||
@@ -579,10 +558,6 @@ const ComposeSchema = {
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tNormal: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tShaded: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tTransparentColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
dBlendTransparency: DefineSpec('boolean'),
|
||||
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tSsaoDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
@@ -598,10 +573,8 @@ const ComposeSchema = {
|
||||
uFogFar: UniformSpec('f'),
|
||||
uFogColor: UniformSpec('v3'),
|
||||
uOutlineColor: UniformSpec('v3'),
|
||||
uOcclusionColor: UniformSpec('v3'),
|
||||
uTransparentBackground: UniformSpec('b'),
|
||||
|
||||
dOcclusionEnable: DefineSpec('boolean'),
|
||||
dOutlineEnable: DefineSpec('boolean'),
|
||||
dOutlineScale: DefineSpec('number'),
|
||||
dTransparentOutline: DefineSpec('boolean'),
|
||||
@@ -609,16 +582,12 @@ const ComposeSchema = {
|
||||
const ComposeShaderCode = ShaderCode('compose', quad_vert, compose_frag);
|
||||
type ComposeRenderable = ComputeRenderable<Values<typeof ComposeSchema>>
|
||||
|
||||
function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture, normalTexture: Texture, shadedTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, transparentColorTexture: Texture, ssaoDepthOpaqueTexture: Texture, ssaoDepthTransparentTexture: Texture, transparentOutline: boolean): ComposeRenderable {
|
||||
function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture, normalTexture: Texture, shadedTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, transparentOutline: boolean): ComposeRenderable {
|
||||
const values: Values<typeof ComposeSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
tNormal: ValueCell.create(normalTexture),
|
||||
tShaded: ValueCell.create(shadedTexture),
|
||||
tTransparentColor: ValueCell.create(transparentColorTexture),
|
||||
dBlendTransparency: ValueCell.create(true),
|
||||
tSsaoDepth: ValueCell.create(ssaoDepthOpaqueTexture),
|
||||
tSsaoDepthTransparent: ValueCell.create(ssaoDepthTransparentTexture),
|
||||
tDepthOpaque: ValueCell.create(depthTextureOpaque),
|
||||
tDepthTransparent: ValueCell.create(depthTextureTransparent),
|
||||
tOutlines: ValueCell.create(outlinesTexture),
|
||||
@@ -634,10 +603,8 @@ function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture, normalTe
|
||||
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),
|
||||
|
||||
dOcclusionEnable: ValueCell.create(false),
|
||||
dOutlineEnable: ValueCell.create(false),
|
||||
dOutlineScale: ValueCell.create(1),
|
||||
dTransparentOutline: ValueCell.create(transparentOutline),
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { DrawPass } from './draw';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { postprocessing_frag } from '../../mol-gl/shader/postprocessing.frag';
|
||||
import { Color } from '../../mol-util/color';
|
||||
@@ -31,7 +30,7 @@ import { BackgroundParams, BackgroundPass } from './background';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { Light } from '../../mol-gl/renderer';
|
||||
import { CasParams, CasPass } from './cas';
|
||||
import { DofPass, DofParams } from './dof';
|
||||
import { DofParams } from './dof';
|
||||
import { BloomParams } from './bloom';
|
||||
import { OutlinePass, OutlineProps, OutlineParams } from './outline';
|
||||
import { ShadowPass, ShadowProps, ShadowParams } from './shadow';
|
||||
@@ -41,12 +40,8 @@ import { SsaoPass, SsaoProps, SsaoParams } from './ssao';
|
||||
const PostprocessingSchema = {
|
||||
...QuadSchema,
|
||||
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tSsaoDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tTransparentColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
dBlendTransparency: DefineSpec('boolean'),
|
||||
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'),
|
||||
@@ -62,9 +57,6 @@ const PostprocessingSchema = {
|
||||
uTransparentBackground: UniformSpec('b'),
|
||||
|
||||
dOcclusionEnable: DefineSpec('boolean'),
|
||||
dOcclusionSingleDepth: DefineSpec('boolean'),
|
||||
dOcclusionIncludeOpacity: DefineSpec('boolean'),
|
||||
dOcclusionIncludeTransparency: DefineSpec('boolean'),
|
||||
uOcclusionOffset: UniformSpec('v2'),
|
||||
|
||||
dShadowEnable: DefineSpec('boolean'),
|
||||
@@ -75,16 +67,12 @@ const PostprocessingSchema = {
|
||||
};
|
||||
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
|
||||
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, transparentColorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, ssaoDepthTransparentTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
|
||||
const values: Values<typeof PostprocessingSchema> = {
|
||||
...QuadValues,
|
||||
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
|
||||
tSsaoDepthTransparent: ValueCell.create(ssaoDepthTransparentTexture),
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
tTransparentColor: ValueCell.create(transparentColorTexture),
|
||||
dBlendTransparency: ValueCell.create(true),
|
||||
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())),
|
||||
@@ -100,9 +88,6 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, t
|
||||
uTransparentBackground: ValueCell.create(false),
|
||||
|
||||
dOcclusionEnable: ValueCell.create(true),
|
||||
dOcclusionSingleDepth: ValueCell.create(false),
|
||||
dOcclusionIncludeOpacity: ValueCell.create(true),
|
||||
dOcclusionIncludeTransparency: ValueCell.create(false),
|
||||
uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
|
||||
|
||||
dShadowEnable: ValueCell.create(false),
|
||||
@@ -159,20 +144,8 @@ export class PostprocessingPass {
|
||||
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
|
||||
}
|
||||
|
||||
static isTransparentDepthRequired(scene: Scene, props: PostprocessingProps) {
|
||||
return DofPass.isEnabled(props) || OutlinePass.isEnabled(props) && PostprocessingPass.isTransparentOutlineEnabled(props) || SsaoPass.isEnabled(props) && PostprocessingPass.isTransparentSsaoEnabled(scene, props);
|
||||
}
|
||||
|
||||
static isTransparentOutlineEnabled(props: PostprocessingProps) {
|
||||
return OutlinePass.isEnabled(props) && ((props.outline.params as OutlineProps).includeTransparent ?? true);
|
||||
}
|
||||
|
||||
static isTransparentSsaoEnabled(scene: Scene, props: PostprocessingProps) {
|
||||
return SsaoPass.isEnabled(props) && SsaoPass.isTransparentEnabled(scene, props.occlusion.params as SsaoProps);
|
||||
}
|
||||
|
||||
static isSsaoEnabled(props: PostprocessingProps) {
|
||||
return SsaoPass.isEnabled(props);
|
||||
return OutlinePass.isEnabled(props) && (props.outline.params as OutlineProps).includeTransparent;
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
@@ -185,18 +158,18 @@ export class PostprocessingPass {
|
||||
readonly background: BackgroundPass;
|
||||
|
||||
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, readonly drawPass: DrawPass) {
|
||||
const { colorTarget, transparentColorTarget, depthTextureOpaque, depthTextureTransparent, packedDepth } = drawPass;
|
||||
const { colorTarget, depthTextureOpaque, depthTextureTransparent, packedDepth } = drawPass;
|
||||
const width = colorTarget.getWidth();
|
||||
const height = colorTarget.getHeight();
|
||||
|
||||
// needs to be linear for anti-aliasing pass
|
||||
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
|
||||
|
||||
this.ssao = new SsaoPass(webgl, width, height, packedDepth, depthTextureOpaque, depthTextureTransparent);
|
||||
this.ssao = new SsaoPass(webgl, width, height, packedDepth, depthTextureOpaque);
|
||||
this.shadow = new ShadowPass(webgl, width, height, depthTextureOpaque);
|
||||
this.outline = new OutlinePass(webgl, width, height, depthTextureTransparent, depthTextureOpaque);
|
||||
|
||||
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, transparentColorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadow.target.texture, this.outline.target.texture, this.ssao.ssaoDepthTexture, this.ssao.ssaoDepthTransparentTexture, true);
|
||||
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, this.shadow.target.texture, this.outline.target.texture, this.ssao.ssaoDepthTexture, true);
|
||||
|
||||
this.background = new BackgroundPass(webgl, assetManager, width, height);
|
||||
}
|
||||
@@ -215,7 +188,7 @@ export class PostprocessingPass {
|
||||
this.background.setSize(width, height);
|
||||
}
|
||||
|
||||
updateState(camera: ICamera, scene: Scene, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
|
||||
updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
|
||||
let needsUpdateMain = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
@@ -224,14 +197,7 @@ export class PostprocessingPass {
|
||||
const occlusionEnabled = SsaoPass.isEnabled(props);
|
||||
|
||||
if (occlusionEnabled) {
|
||||
const params = props.occlusion.params as SsaoProps;
|
||||
this.ssao.update(camera, scene, params);
|
||||
const includeTransparency = SsaoPass.isTransparentEnabled(scene, params);
|
||||
if (this.renderable.values.dOcclusionIncludeTransparency.ref.value !== includeTransparency) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dOcclusionIncludeTransparency, includeTransparency);
|
||||
}
|
||||
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, params.color));
|
||||
this.ssao.update(camera, props.occlusion.params as SsaoProps, this.occlusionOffset);
|
||||
}
|
||||
|
||||
if (shadowsEnabled) {
|
||||
@@ -279,12 +245,6 @@ export class PostprocessingPass {
|
||||
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
|
||||
}
|
||||
|
||||
const blendTransparency = scene.opacityAverage < 1;
|
||||
if (this.renderable.values.dBlendTransparency.ref.value !== blendTransparency) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dBlendTransparency, blendTransparency);
|
||||
}
|
||||
|
||||
if (needsUpdateMain) {
|
||||
this.renderable.update();
|
||||
}
|
||||
@@ -309,9 +269,9 @@ export class PostprocessingPass {
|
||||
this.transparentBackground = value;
|
||||
}
|
||||
|
||||
render(camera: ICamera, scene: Scene, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
|
||||
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
|
||||
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
|
||||
this.updateState(camera, scene, transparentBackground, backgroundColor, props, light, ambientColor);
|
||||
this.updateState(camera, transparentBackground, backgroundColor, props, light, ambientColor);
|
||||
|
||||
const { state } = this.webgl;
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
@@ -319,6 +279,7 @@ export class PostprocessingPass {
|
||||
// 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 (props.occlusion.name === 'on') {
|
||||
this.ssao.render(camera);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
|
||||
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
|
||||
@@ -29,11 +28,11 @@ import { isTimingMode } from '../../mol-util/debug';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const SsaoParams = {
|
||||
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
|
||||
samples: PD.Numeric(24, { 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' }),
|
||||
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 },
|
||||
@@ -46,13 +45,14 @@ export const SsaoParams = {
|
||||
}),
|
||||
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: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x.', hideIf: p => p?.multiScale.name === 'on' }),
|
||||
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
|
||||
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
|
||||
blurKernelSize: PD.Numeric(15, { min: 1, max: 35, step: 2 }),
|
||||
blurStepSize: PD.Numeric(2, { min: 1, max: 3, step: 1 }, { description: 'Step size for the blur. Values greater than one work best with multi-sample enabled to mitigate artefacts.' }),
|
||||
blurDepthBias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
|
||||
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
|
||||
blurNormalBias: PD.Numeric(0.0, { min: 0, max: 0.95, step: 0.01 }, { description: 'Bias for normal comparison in blur. Mainly improves creases between overlapping spheres and the like. Quite expensive, use with care. Disabled when set to zero.' }),
|
||||
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation.' }),
|
||||
color: PD.Color(Color(0x000000)),
|
||||
transparentThreshold: PD.Numeric(0.4, { min: 0, max: 1, step: 0.05 }),
|
||||
};
|
||||
|
||||
export type SsaoProps = PD.Values<typeof SsaoParams>
|
||||
@@ -83,75 +83,48 @@ export class SsaoPass {
|
||||
return props.occlusion.name !== 'off';
|
||||
}
|
||||
|
||||
static isTransparentEnabled(scene: Scene, props: SsaoProps) {
|
||||
return scene.opacityAverage < 1 && scene.transparencyMin < props.transparentThreshold;
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
|
||||
private readonly framebuffer: Framebuffer;
|
||||
private readonly blurFirstPassFramebuffer: Framebuffer;
|
||||
private readonly blurSecondPassFramebuffer: Framebuffer;
|
||||
|
||||
private readonly downsampledDepthTargetOpaque: RenderTarget;
|
||||
private readonly downsampleDepthRenderableOpaque: CopyRenderable;
|
||||
private readonly downsampledDepthTarget: RenderTarget;
|
||||
private readonly downsampleDepthRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthHalfTargetOpaque: RenderTarget;
|
||||
private readonly depthHalfRenderableOpaque: CopyRenderable;
|
||||
private readonly depthHalfTarget: RenderTarget;
|
||||
private readonly depthHalfRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthQuarterTargetOpaque: RenderTarget;
|
||||
private readonly depthQuarterRenderableOpaque: CopyRenderable;
|
||||
|
||||
private readonly downsampledDepthTargetTransparent: RenderTarget;
|
||||
private readonly downsampleDepthRenderableTransparent: CopyRenderable;
|
||||
|
||||
private readonly depthHalfTargetTransparent: RenderTarget;
|
||||
private readonly depthHalfRenderableTransparent: CopyRenderable;
|
||||
|
||||
private readonly depthQuarterTargetTransparent: RenderTarget;
|
||||
private readonly depthQuarterRenderableTransparent: CopyRenderable;
|
||||
private readonly depthQuarterTarget: RenderTarget;
|
||||
private readonly depthQuarterRenderable: CopyRenderable;
|
||||
|
||||
readonly ssaoDepthTexture: Texture;
|
||||
readonly ssaoDepthTransparentTexture: Texture;
|
||||
|
||||
private readonly depthBlurProxyTexture: Texture;
|
||||
|
||||
private depthTextureOpaque: Texture;
|
||||
private depthTextureTransparent: Texture;
|
||||
|
||||
private readonly renderable: SsaoRenderable;
|
||||
private readonly blurFirstPassRenderable: SsaoBlurRenderable;
|
||||
private readonly blurSecondPassRenderable: SsaoBlurRenderable;
|
||||
|
||||
private nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
private depthTexture: Texture;
|
||||
private texSize: [number, number];
|
||||
|
||||
private nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
private ssaoScale: number;
|
||||
private calcSsaoScale(resolutionScale: number) {
|
||||
// downscale ssao for high pixel-ratios
|
||||
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
|
||||
}
|
||||
|
||||
private levels: { radius: number, bias: number }[];
|
||||
|
||||
private getDepthTexture() {
|
||||
return this.ssaoScale === 1 ? this.depthTextureOpaque : this.downsampledDepthTargetOpaque.texture;
|
||||
return this.ssaoScale === 1 ? this.depthTexture : this.downsampledDepthTarget.texture;
|
||||
}
|
||||
|
||||
private getTransparentDepthTexture() {
|
||||
return this.ssaoScale === 1 ? this.depthTextureTransparent : this.downsampledDepthTargetTransparent.texture;
|
||||
}
|
||||
|
||||
constructor(private readonly webgl: WebGLContext, width: number, height: number, packedDepth: boolean, depthTextureOpaque: Texture, depthTextureTransparent: Texture) {
|
||||
constructor(private readonly webgl: WebGLContext, width: number, height: number, packedDepth: boolean, depthTexture: Texture) {
|
||||
const { textureFloatLinear } = webgl.extensions;
|
||||
|
||||
this.depthTextureOpaque = depthTextureOpaque;
|
||||
this.depthTextureTransparent = depthTextureTransparent;
|
||||
this.depthTexture = depthTexture;
|
||||
|
||||
this.nSamples = 1;
|
||||
this.blurKernelSize = 1;
|
||||
this.ssaoScale = this.calcSsaoScale(1);
|
||||
this.ssaoScale = 1;
|
||||
this.texSize = [width, height];
|
||||
this.levels = [];
|
||||
|
||||
@@ -170,101 +143,73 @@ export class SsaoPass {
|
||||
|
||||
const filter = textureFloatLinear ? 'linear' : 'nearest';
|
||||
|
||||
this.downsampledDepthTargetOpaque = packedDepth
|
||||
this.downsampledDepthTarget = packedDepth
|
||||
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(sw, sh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.downsampleDepthRenderableOpaque = createCopyRenderable(webgl, depthTextureOpaque);
|
||||
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTexture);
|
||||
|
||||
const depthTexture = this.getDepthTexture();
|
||||
this.depthHalfTargetOpaque = packedDepth
|
||||
this.depthHalfTarget = packedDepth
|
||||
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(hw, hh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.depthHalfRenderableOpaque = createCopyRenderable(webgl, depthTexture);
|
||||
this.depthHalfRenderable = createCopyRenderable(webgl, this.getDepthTexture());
|
||||
|
||||
this.depthQuarterTargetOpaque = packedDepth
|
||||
this.depthQuarterTarget = packedDepth
|
||||
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(qw, qh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.depthQuarterRenderableOpaque = createCopyRenderable(webgl, this.depthHalfTargetOpaque.texture);
|
||||
|
||||
this.downsampledDepthTargetTransparent = webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba');
|
||||
this.downsampleDepthRenderableTransparent = createCopyRenderable(webgl, depthTextureTransparent);
|
||||
|
||||
const transparentDepthTexture = this.getTransparentDepthTexture();
|
||||
this.depthHalfTargetTransparent = webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba');
|
||||
this.depthHalfRenderableTransparent = createCopyRenderable(webgl, transparentDepthTexture);
|
||||
|
||||
this.depthQuarterTargetTransparent = webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba');
|
||||
this.depthQuarterRenderableTransparent = createCopyRenderable(webgl, this.depthHalfTargetTransparent.texture);
|
||||
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.framebuffer, 'color0');
|
||||
|
||||
this.ssaoDepthTransparentTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
|
||||
this.ssaoDepthTransparentTexture.define(sw, sh);
|
||||
|
||||
this.depthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
|
||||
this.depthBlurProxyTexture.define(sw, sh);
|
||||
this.depthBlurProxyTexture.attachFramebuffer(this.blurFirstPassFramebuffer, 'color0');
|
||||
|
||||
this.renderable = getSsaoRenderable(webgl, depthTexture, this.depthHalfTargetOpaque.texture, this.depthQuarterTargetOpaque.texture, transparentDepthTexture, this.depthHalfTargetTransparent.texture, this.depthQuarterTargetTransparent.texture);
|
||||
this.blurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTransparentTexture, 'horizontal');
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
|
||||
|
||||
this.renderable = getSsaoRenderable(webgl, this.getDepthTexture(), this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
|
||||
this.blurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
|
||||
this.blurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.depthBlurProxyTexture, 'vertical');
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.texSize;
|
||||
const ssaoScale = this.calcSsaoScale(1);
|
||||
const ssaoScale = 1;
|
||||
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
|
||||
this.texSize.splice(0, 2, width, height);
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
this.downsampledDepthTarget.setSize(sw, sh);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthTransparentTexture.define(sw, sh);
|
||||
this.depthBlurProxyTexture.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);
|
||||
|
||||
this.downsampledDepthTargetOpaque.setSize(sw, sh);
|
||||
this.depthHalfTargetOpaque.setSize(hw, hh);
|
||||
this.depthQuarterTargetOpaque.setSize(qw, qh);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderableOpaque.values.uTexSize, Vec2.set(this.downsampleDepthRenderableOpaque.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderableOpaque.values.uTexSize, Vec2.set(this.depthHalfRenderableOpaque.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderableOpaque.values.uTexSize, Vec2.set(this.depthQuarterRenderableOpaque.values.uTexSize.ref.value, qw, qh));
|
||||
|
||||
this.downsampledDepthTargetTransparent.setSize(sw, sh);
|
||||
this.depthHalfTargetTransparent.setSize(hw, hh);
|
||||
this.depthQuarterTargetTransparent.setSize(qw, qh);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderableTransparent.values.uTexSize, Vec2.set(this.downsampleDepthRenderableTransparent.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderableTransparent.values.uTexSize, Vec2.set(this.depthHalfRenderableTransparent.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderableTransparent.values.uTexSize, Vec2.set(this.depthQuarterRenderableTransparent.values.uTexSize.ref.value, qw, qh));
|
||||
|
||||
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.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
|
||||
const depthTexture = this.getDepthTexture();
|
||||
const transparentDepthTexture = this.getTransparentDepthTexture();
|
||||
|
||||
ValueCell.update(this.depthHalfRenderableOpaque.values.tColor, depthTexture);
|
||||
ValueCell.update(this.depthHalfRenderableTransparent.values.tColor, transparentDepthTexture);
|
||||
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.renderable.values.tDepth, depthTexture);
|
||||
ValueCell.update(this.renderable.values.tDepthTransparent, transparentDepthTexture);
|
||||
|
||||
this.depthHalfRenderableOpaque.update();
|
||||
this.depthHalfRenderableTransparent.update();
|
||||
this.depthHalfRenderable.update();
|
||||
this.renderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
update(camera: ICamera, scene: Scene, props: SsaoProps, illuminationMode = false) {
|
||||
update(camera: ICamera, props: SsaoProps, offset: [x: number, y: number]) {
|
||||
let needsUpdateSsao = false;
|
||||
let needsUpdateSsaoBlur = false;
|
||||
let needsUpdateDepthHalf = false;
|
||||
@@ -304,25 +249,24 @@ export class SsaoPass {
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
|
||||
|
||||
const dBlurNormalBias = props.blurNormalBias !== 0;
|
||||
if (this.blurFirstPassRenderable.values.dBlurNormalBias.ref.value !== dBlurNormalBias) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.dBlurNormalBias, dBlurNormalBias);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.dBlurNormalBias, dBlurNormalBias);
|
||||
}
|
||||
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uBlurNormalBias, props.blurNormalBias);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uBlurNormalBias, props.blurNormalBias);
|
||||
|
||||
|
||||
if (this.blurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.dOrthographic, orthographic);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.dOrthographic, orthographic);
|
||||
}
|
||||
|
||||
const includeTransparent = SsaoPass.isTransparentEnabled(scene, props);
|
||||
if (this.renderable.values.dIncludeTransparent.ref.value !== includeTransparent) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
ValueCell.update(this.renderable.values.dIncludeTransparent, includeTransparent);
|
||||
}
|
||||
|
||||
if (this.renderable.values.dIllumination.ref.value !== illuminationMode) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
ValueCell.update(this.renderable.values.dIllumination, illuminationMode);
|
||||
}
|
||||
|
||||
if (this.nSamples !== props.samples) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
@@ -334,7 +278,6 @@ export class SsaoPass {
|
||||
const multiScale = props.multiScale.name === 'on';
|
||||
if (this.renderable.values.dMultiScale.ref.value !== multiScale) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
ValueCell.update(this.renderable.values.dMultiScale, multiScale);
|
||||
}
|
||||
|
||||
@@ -357,66 +300,55 @@ export class SsaoPass {
|
||||
}
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBias, props.bias);
|
||||
|
||||
if (this.blurKernelSize !== props.blurKernelSize) {
|
||||
const blurKernelSize = Math.max(1, Math.floor(props.blurKernelSize / props.blurStepSize));
|
||||
if (this.blurKernelSize !== blurKernelSize) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
|
||||
this.blurKernelSize = props.blurKernelSize;
|
||||
const kernel = getBlurKernel(this.blurKernelSize);
|
||||
this.blurKernelSize = blurKernelSize;
|
||||
const kernel = getBlurKernel(blurKernelSize);
|
||||
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.dOcclusionKernelSize, blurKernelSize);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.dOcclusionKernelSize, blurKernelSize);
|
||||
}
|
||||
|
||||
const ssaoScale = this.calcSsaoScale(props.resolutionScale);
|
||||
if (this.ssaoScale !== ssaoScale) {
|
||||
ValueCell.updateIfChanged(this.blurFirstPassRenderable.values.uBlurStepSize, props.blurStepSize);
|
||||
ValueCell.updateIfChanged(this.blurSecondPassRenderable.values.uBlurStepSize, props.blurStepSize);
|
||||
|
||||
ValueCell.updateIfChanged(this.blurFirstPassRenderable.values.uBlurStepOffset, Vec2.set(this.blurFirstPassRenderable.values.uBlurStepOffset.ref.value, offset[0], offset[1]));
|
||||
ValueCell.updateIfChanged(this.blurSecondPassRenderable.values.uBlurStepOffset, Vec2.set(this.blurSecondPassRenderable.values.uBlurStepOffset.ref.value, offset[0], offset[1]));
|
||||
|
||||
if (this.ssaoScale !== props.resolutionScale) {
|
||||
needsUpdateSsao = true;
|
||||
needsUpdateDepthHalf = true;
|
||||
|
||||
this.ssaoScale = ssaoScale;
|
||||
this.ssaoScale = props.resolutionScale;
|
||||
|
||||
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.ssaoDepthTransparentTexture.define(sw, sh);
|
||||
this.depthBlurProxyTexture.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.downsampledDepthTargetOpaque.setSize(sw, sh);
|
||||
this.depthHalfTargetOpaque.setSize(hw, hh);
|
||||
this.depthQuarterTargetOpaque.setSize(qw, qh);
|
||||
this.depthQuarterTarget.setSize(qw, qh);
|
||||
|
||||
const depthTexture = this.getDepthTexture();
|
||||
ValueCell.update(this.depthHalfRenderableOpaque.values.tColor, depthTexture);
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.renderable.values.tDepth, depthTexture);
|
||||
|
||||
ValueCell.update(this.renderable.values.tDepthHalf, this.depthHalfTargetOpaque.texture);
|
||||
ValueCell.update(this.renderable.values.tDepthQuarter, this.depthQuarterTargetOpaque.texture);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderableOpaque.values.uTexSize, Vec2.set(this.downsampleDepthRenderableOpaque.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderableOpaque.values.uTexSize, Vec2.set(this.depthHalfRenderableOpaque.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderableOpaque.values.uTexSize, Vec2.set(this.depthQuarterRenderableOpaque.values.uTexSize.ref.value, qw, qh));
|
||||
|
||||
this.downsampledDepthTargetTransparent.setSize(sw, sh);
|
||||
this.depthHalfTargetTransparent.setSize(hw, hh);
|
||||
this.depthQuarterTargetTransparent.setSize(qw, qh);
|
||||
|
||||
const transparentDepthTexture = this.getTransparentDepthTexture();
|
||||
ValueCell.update(this.depthHalfRenderableTransparent.values.tColor, transparentDepthTexture);
|
||||
ValueCell.update(this.renderable.values.tDepthTransparent, transparentDepthTexture);
|
||||
|
||||
ValueCell.update(this.renderable.values.tDepthHalfTransparent, this.depthHalfTargetTransparent.texture);
|
||||
ValueCell.update(this.renderable.values.tDepthQuarterTransparent, this.depthQuarterTargetTransparent.texture);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderableTransparent.values.uTexSize, Vec2.set(this.downsampleDepthRenderableTransparent.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderableTransparent.values.uTexSize, Vec2.set(this.depthHalfRenderableTransparent.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderableTransparent.values.uTexSize, Vec2.set(this.depthQuarterRenderableTransparent.values.uTexSize.ref.value, qw, qh));
|
||||
ValueCell.update(this.renderable.values.tDepthHalf, this.depthHalfTarget.texture);
|
||||
ValueCell.update(this.renderable.values.tDepthQuarter, this.depthQuarterTarget.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.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
@@ -432,98 +364,71 @@ export class SsaoPass {
|
||||
}
|
||||
|
||||
if (needsUpdateDepthHalf) {
|
||||
this.depthHalfRenderableOpaque.update();
|
||||
this.depthHalfRenderableTransparent.update();
|
||||
this.depthHalfRenderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
render(camera: ICamera) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.render');
|
||||
|
||||
const { state } = this.webgl;
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
|
||||
const includeTransparent = this.renderable.values.dIncludeTransparent.ref.value;
|
||||
const multiScale = this.renderable.values.dMultiScale.ref.value;
|
||||
|
||||
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);
|
||||
|
||||
if (this.ssaoScale < 1) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.downsample');
|
||||
state.viewport(sx, sy, sw, sh);
|
||||
state.scissor(sx, sy, sw, sh);
|
||||
this.downsampledDepthTarget.bind();
|
||||
this.downsampleDepthRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.downsample');
|
||||
}
|
||||
|
||||
if (this.renderable.values.dMultiScale.ref.value) {
|
||||
const hx = Math.floor(sx * 0.5);
|
||||
const hy = Math.floor(sy * 0.5);
|
||||
const hw = Math.ceil(sw * 0.5);
|
||||
const hh = Math.ceil(sh * 0.5);
|
||||
|
||||
const qx = Math.floor(sx * 0.25);
|
||||
const qy = Math.floor(sy * 0.25);
|
||||
const qw = Math.ceil(sw * 0.25);
|
||||
const qh = Math.ceil(sh * 0.25);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.half');
|
||||
state.viewport(hx, hy, hw, hh);
|
||||
state.scissor(hx, hy, hw, hh);
|
||||
this.depthHalfTarget.bind();
|
||||
this.depthHalfRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.half');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.quarter');
|
||||
state.viewport(qx, qy, qw, qh);
|
||||
state.scissor(sx, qy, sw, qh);
|
||||
this.depthQuarterTarget.bind();
|
||||
this.depthQuarterRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.quarter');
|
||||
}
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.sample');
|
||||
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.downsampledDepthTargetOpaque.bind();
|
||||
this.downsampleDepthRenderableOpaque.render();
|
||||
if (includeTransparent) {
|
||||
this.downsampledDepthTargetTransparent.bind();
|
||||
this.downsampleDepthRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
|
||||
}
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
if (multiScale) {
|
||||
this.depthHalfTargetOpaque.bind();
|
||||
this.depthHalfRenderableOpaque.render();
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthHalfTargetTransparent.bind();
|
||||
this.depthHalfRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
if (multiScale) {
|
||||
this.depthQuarterTargetOpaque.bind();
|
||||
this.depthQuarterRenderableOpaque.render();
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthQuarterTargetTransparent.bind();
|
||||
this.depthQuarterRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.opaque');
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
|
||||
ValueCell.update(this.renderable.values.uTransparencyFlag, 0);
|
||||
this.framebuffer.bind();
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.opaque');
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.sample');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.blurOpaque');
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.tSsaoDepth, this.ssaoDepthTexture);
|
||||
this.blurFirstPassRenderable.update();
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.blur');
|
||||
this.blurFirstPassFramebuffer.bind();
|
||||
this.blurFirstPassRenderable.render();
|
||||
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
|
||||
this.blurSecondPassFramebuffer.bind();
|
||||
this.blurSecondPassRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.blurOpaque');
|
||||
if (includeTransparent) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.transparent ');
|
||||
this.ssaoDepthTransparentTexture.attachFramebuffer(this.framebuffer, 'color0');
|
||||
ValueCell.update(this.renderable.values.uTransparencyFlag, 1);
|
||||
this.framebuffer.bind();
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.transparent ');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.blurTransparent ');
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.tSsaoDepth, this.ssaoDepthTransparentTexture);
|
||||
this.blurFirstPassRenderable.update();
|
||||
this.blurFirstPassFramebuffer.bind();
|
||||
this.blurFirstPassRenderable.render();
|
||||
|
||||
this.ssaoDepthTransparentTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
|
||||
this.blurSecondPassFramebuffer.bind();
|
||||
this.blurSecondPassRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.blurTransparent ');
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.blur');
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.render');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,13 +438,6 @@ const SsaoSchema = {
|
||||
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
|
||||
dIllumination: DefineSpec('boolean'),
|
||||
uTransparencyFlag: UniformSpec('i'),
|
||||
dIncludeTransparent: DefineSpec('boolean'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
tDepthHalfTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
tDepthQuarterTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
|
||||
uSamples: UniformSpec('v3[]'),
|
||||
dNSamples: DefineSpec('number'),
|
||||
|
||||
@@ -562,20 +460,13 @@ const SsaoSchema = {
|
||||
|
||||
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
|
||||
|
||||
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture, transparentDepthTexture: Texture, transparentDepthHalfTexture: Texture, transparentDepthQuarterTexture: Texture): SsaoRenderable {
|
||||
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
|
||||
const values: Values<typeof SsaoSchema> = {
|
||||
...QuadValues,
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
tDepthHalf: ValueCell.create(depthHalfTexture),
|
||||
tDepthQuarter: ValueCell.create(depthQuarterTexture),
|
||||
|
||||
dIllumination: ValueCell.create(false),
|
||||
dIncludeTransparent: ValueCell.create(true),
|
||||
uTransparencyFlag: ValueCell.create(0),
|
||||
tDepthTransparent: ValueCell.create(transparentDepthTexture),
|
||||
tDepthHalfTransparent: ValueCell.create(transparentDepthHalfTexture),
|
||||
tDepthQuarterTransparent: ValueCell.create(transparentDepthQuarterTexture),
|
||||
|
||||
uSamples: ValueCell.create(getSamples(32)),
|
||||
dNSamples: ValueCell.create(32),
|
||||
|
||||
@@ -611,6 +502,10 @@ const SsaoBlurSchema = {
|
||||
uKernel: UniformSpec('f[]'),
|
||||
dOcclusionKernelSize: DefineSpec('number'),
|
||||
uBlurDepthBias: UniformSpec('f'),
|
||||
dBlurNormalBias: DefineSpec('boolean'),
|
||||
uBlurNormalBias: UniformSpec('f'),
|
||||
uBlurStepSize: UniformSpec('f'),
|
||||
uBlurStepOffset: UniformSpec('v2'),
|
||||
|
||||
uBlurDirectionX: UniformSpec('f'),
|
||||
uBlurDirectionY: UniformSpec('f'),
|
||||
@@ -633,6 +528,10 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
|
||||
uKernel: ValueCell.create(getBlurKernel(15)),
|
||||
dOcclusionKernelSize: ValueCell.create(15),
|
||||
uBlurDepthBias: ValueCell.create(0.5),
|
||||
dBlurNormalBias: ValueCell.create(false),
|
||||
uBlurNormalBias: ValueCell.create(0.0),
|
||||
uBlurStepSize: ValueCell.create(1),
|
||||
uBlurStepOffset: ValueCell.create(Vec2()),
|
||||
|
||||
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
|
||||
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
|
||||
|
||||
@@ -49,7 +49,6 @@ export interface DirectVolume {
|
||||
readonly cartnToUnit: ValueCell<Mat4>
|
||||
readonly packedGroup: ValueCell<boolean>
|
||||
readonly axisOrder: ValueCell<Vec3>
|
||||
readonly dataType: ValueCell<'byte' | 'float' | 'halfFloat'>
|
||||
|
||||
/** Bounding sphere of the volume */
|
||||
readonly boundingSphere: Sphere3D
|
||||
@@ -58,10 +57,10 @@ export interface DirectVolume {
|
||||
}
|
||||
|
||||
export namespace DirectVolume {
|
||||
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume?: DirectVolume): DirectVolume {
|
||||
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume?: DirectVolume): DirectVolume {
|
||||
return directVolume ?
|
||||
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume) :
|
||||
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType);
|
||||
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume) :
|
||||
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder);
|
||||
}
|
||||
|
||||
function hashCode(directVolume: DirectVolume) {
|
||||
@@ -72,7 +71,7 @@ export namespace DirectVolume {
|
||||
]);
|
||||
}
|
||||
|
||||
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat'): DirectVolume {
|
||||
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3): DirectVolume {
|
||||
const boundingSphere = Sphere3D();
|
||||
let currentHash = -1;
|
||||
|
||||
@@ -104,7 +103,6 @@ export namespace DirectVolume {
|
||||
},
|
||||
packedGroup: ValueCell.create(packedGroup),
|
||||
axisOrder: ValueCell.create(axisOrder),
|
||||
dataType: ValueCell.create(dataType),
|
||||
setBoundingSphere(sphere: Sphere3D) {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(directVolume);
|
||||
@@ -113,7 +111,7 @@ export namespace DirectVolume {
|
||||
return directVolume;
|
||||
}
|
||||
|
||||
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume: DirectVolume): DirectVolume {
|
||||
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume: DirectVolume): DirectVolume {
|
||||
const width = texture.getWidth();
|
||||
const height = texture.getHeight();
|
||||
const depth = texture.getDepth();
|
||||
@@ -131,7 +129,6 @@ export namespace DirectVolume {
|
||||
ValueCell.update(directVolume.cartnToUnit, Mat4.invert(Mat4(), unitToCartn));
|
||||
ValueCell.updateIfChanged(directVolume.packedGroup, packedGroup);
|
||||
ValueCell.updateIfChanged(directVolume.axisOrder, Vec3.fromArray(directVolume.axisOrder.ref.value, axisOrder, 0));
|
||||
ValueCell.updateIfChanged(directVolume.dataType, dataType);
|
||||
return directVolume;
|
||||
}
|
||||
|
||||
@@ -145,8 +142,7 @@ export namespace DirectVolume {
|
||||
const stats = Grid.One.stats;
|
||||
const packedGroup = false;
|
||||
const axisOrder = Vec3.create(0, 1, 2);
|
||||
const dataType = 'byte';
|
||||
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume);
|
||||
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume);
|
||||
}
|
||||
|
||||
export const Params = {
|
||||
|
||||
@@ -66,10 +66,7 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
const q1 = Math.round(radialSegments / 4);
|
||||
const q3 = q1 * 3;
|
||||
|
||||
const roundCapFlag = roundCap && linearSegments && (startCap || endCap);
|
||||
let halfLinearSegments;
|
||||
const doubleRoundCap = roundCapFlag && startCap && endCap;
|
||||
if (doubleRoundCap) halfLinearSegments = linearSegments / 2;
|
||||
const roundCapFlag = roundCap && linearSegments && !(startCap && endCap) && (startCap || endCap); // disabled if both caps are active
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
const i3 = i * 3;
|
||||
v3fromArray(u, normalVectors, i3);
|
||||
@@ -80,15 +77,10 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
let height = heightValues[i];
|
||||
let capSmoothingFactor: number;
|
||||
if (roundCapFlag) {
|
||||
const sc = doubleRoundCap ? i <= halfLinearSegments! : startCap;
|
||||
if (doubleRoundCap) {
|
||||
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((sc ? halfLinearSegments! - i : i - halfLinearSegments!) / halfLinearSegments!, 2)));
|
||||
} else {
|
||||
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((sc ? linearSegments - i : i) / linearSegments, 2)));
|
||||
}
|
||||
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((startCap ? linearSegments - i : i) / (linearSegments), 2)));
|
||||
width *= capSmoothingFactor;
|
||||
height *= capSmoothingFactor;
|
||||
v3cross(capNormalSmoothingVector, sc ? v : u, sc ? u : v);
|
||||
v3cross(capNormalSmoothingVector, startCap ? v : u, startCap ? u : v);
|
||||
v3normalize(capNormalSmoothingVector, capNormalSmoothingVector);
|
||||
}
|
||||
const rounded = crossSection === 'rounded' && height > width;
|
||||
|
||||
@@ -285,7 +285,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
|
||||
webgl.namedTextures[ColorCountName] = resources.texture('image-float32', 'alpha', 'float', 'nearest');
|
||||
}
|
||||
} else {
|
||||
// webgl1 requires consistent bit plane counts
|
||||
// in webgl1 drawbuffers must be in the same format for some reason
|
||||
// this is quite wasteful but good enough for medium size meshes
|
||||
|
||||
if (!webgl.namedTextures[ColorAccumulateName]) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 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>
|
||||
*/
|
||||
@@ -16,7 +16,6 @@ export type TransparencyData = {
|
||||
uTransparencyTexDim: ValueCell<Vec2>
|
||||
dTransparency: ValueCell<boolean>,
|
||||
transparencyAverage: ValueCell<number>,
|
||||
transparencyMin: ValueCell<number>,
|
||||
|
||||
tTransparencyGrid: ValueCell<Texture>,
|
||||
uTransparencyGridDim: ValueCell<Vec3>,
|
||||
@@ -41,16 +40,6 @@ export function getTransparencyAverage(array: Uint8Array, count: number): number
|
||||
return sum / (255 * count);
|
||||
}
|
||||
|
||||
/** exclude fully opaque parts */
|
||||
export function getTransparencyMin(array: Uint8Array, count: number): number {
|
||||
if (count === 0 || array.length < count) return 1;
|
||||
let min = 255;
|
||||
for (let i = 0; i < count; ++i) {
|
||||
if (array[i] > 0 && array[i] < min) min = array[i];
|
||||
}
|
||||
return min / 255;
|
||||
}
|
||||
|
||||
export function clearTransparency(array: Uint8Array, start: number, end: number) {
|
||||
array.fill(0, start, end);
|
||||
}
|
||||
@@ -62,7 +51,6 @@ export function createTransparency(count: number, type: TransparencyType, transp
|
||||
ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height));
|
||||
ValueCell.updateIfChanged(transparencyData.dTransparency, count > 0);
|
||||
ValueCell.updateIfChanged(transparencyData.transparencyAverage, getTransparencyAverage(transparency.array, count));
|
||||
ValueCell.updateIfChanged(transparencyData.transparencyMin, getTransparencyMin(transparency.array, count));
|
||||
ValueCell.updateIfChanged(transparencyData.dTransparencyType, type);
|
||||
return transparencyData;
|
||||
} else {
|
||||
@@ -71,7 +59,6 @@ export function createTransparency(count: number, type: TransparencyType, transp
|
||||
uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)),
|
||||
dTransparency: ValueCell.create(count > 0),
|
||||
transparencyAverage: ValueCell.create(0),
|
||||
transparencyMin: ValueCell.create(1),
|
||||
|
||||
tTransparencyGrid: ValueCell.create(createNullTexture()),
|
||||
uTransparencyGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
@@ -94,7 +81,6 @@ export function createEmptyTransparency(transparencyData?: TransparencyData): Tr
|
||||
uTransparencyTexDim: ValueCell.create(Vec2.create(1, 1)),
|
||||
dTransparency: ValueCell.create(false),
|
||||
transparencyAverage: ValueCell.create(0),
|
||||
transparencyMin: ValueCell.create(1),
|
||||
|
||||
tTransparencyGrid: ValueCell.create(createNullTexture()),
|
||||
uTransparencyGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 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>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { ComputeRenderable, createComputeRenderable } from '../../renderable';
|
||||
import { WebGLContext } from '../../webgl/context';
|
||||
import { createComputeRenderItem } from '../../webgl/render-item';
|
||||
import { Values, TextureSpec, UniformSpec, DefineSpec } from '../../renderable/schema';
|
||||
import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
|
||||
import { Texture } from '../../../mol-gl/webgl/texture';
|
||||
import { ShaderCode } from '../../../mol-gl/shader-code';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
@@ -17,14 +17,12 @@ import { getTriCount } from './tables';
|
||||
import { quad_vert } from '../../../mol-gl/shader/quad.vert';
|
||||
import { activeVoxels_frag } from '../../../mol-gl/shader/marching-cubes/active-voxels.frag';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { isWebGL2 } from '../../webgl/compat';
|
||||
|
||||
const ActiveVoxelsSchema = {
|
||||
...QuadSchema,
|
||||
|
||||
tTriCount: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
|
||||
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
dValueChannel: DefineSpec('string', ['red', 'alpha']),
|
||||
uIsoValue: UniformSpec('f'),
|
||||
|
||||
uGridDim: UniformSpec('v3'),
|
||||
@@ -36,17 +34,12 @@ type ActiveVoxelsValues = Values<typeof ActiveVoxelsSchema>
|
||||
|
||||
const ActiveVoxelsName = 'active-voxels';
|
||||
|
||||
function valueChannel(ctx: WebGLContext, volumeData: Texture) {
|
||||
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
|
||||
}
|
||||
|
||||
function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2): ComputeRenderable<ActiveVoxelsValues> {
|
||||
if (ctx.namedComputeRenderables[ActiveVoxelsName]) {
|
||||
const v = ctx.namedComputeRenderables[ActiveVoxelsName].values as ActiveVoxelsValues;
|
||||
|
||||
ValueCell.update(v.uQuadScale, scale);
|
||||
ValueCell.update(v.tVolumeData, volumeData);
|
||||
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
|
||||
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
|
||||
ValueCell.update(v.uGridDim, gridDim);
|
||||
ValueCell.update(v.uGridTexDim, gridTexDim);
|
||||
@@ -66,7 +59,6 @@ function createActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gr
|
||||
|
||||
uQuadScale: ValueCell.create(scale),
|
||||
tVolumeData: ValueCell.create(volumeData),
|
||||
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
|
||||
uIsoValue: ValueCell.create(isoValue),
|
||||
uGridDim: ValueCell.create(gridDim),
|
||||
uGridTexDim: ValueCell.create(gridTexDim),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 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>
|
||||
*/
|
||||
@@ -28,7 +28,6 @@ const IsosurfaceSchema = {
|
||||
tActiveVoxelsPyramid: TextureSpec('texture', 'rgba', 'float', 'nearest'),
|
||||
tActiveVoxelsBase: TextureSpec('texture', 'rgba', 'float', 'nearest'),
|
||||
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
dValueChannel: DefineSpec('string', ['red', 'alpha']),
|
||||
uIsoValue: UniformSpec('f'),
|
||||
|
||||
uSize: UniformSpec('f'),
|
||||
@@ -49,10 +48,6 @@ type IsosurfaceValues = Values<typeof IsosurfaceSchema>
|
||||
|
||||
const IsosurfaceName = 'isosurface';
|
||||
|
||||
function valueChannel(ctx: WebGLContext, volumeData: Texture) {
|
||||
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
|
||||
}
|
||||
|
||||
function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean): ComputeRenderable<IsosurfaceValues> {
|
||||
if (ctx.namedComputeRenderables[IsosurfaceName]) {
|
||||
const v = ctx.namedComputeRenderables[IsosurfaceName].values as IsosurfaceValues;
|
||||
@@ -60,7 +55,6 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
|
||||
ValueCell.update(v.tActiveVoxelsPyramid, activeVoxelsPyramid);
|
||||
ValueCell.update(v.tActiveVoxelsBase, activeVoxelsBase);
|
||||
ValueCell.update(v.tVolumeData, volumeData);
|
||||
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
|
||||
|
||||
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
|
||||
ValueCell.updateIfChanged(v.uSize, Math.pow(2, levels));
|
||||
@@ -93,7 +87,6 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
|
||||
tActiveVoxelsPyramid: ValueCell.create(activeVoxelsPyramid),
|
||||
tActiveVoxelsBase: ValueCell.create(activeVoxelsBase),
|
||||
tVolumeData: ValueCell.create(volumeData),
|
||||
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
|
||||
|
||||
uIsoValue: ValueCell.create(isoValue),
|
||||
uSize: ValueCell.create(Math.pow(2, levels)),
|
||||
@@ -162,7 +155,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
|
||||
: resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
}
|
||||
} else {
|
||||
// webgl1 requires consistent bit plane counts
|
||||
// in webgl1 drawbuffers must be in the same format for some reason
|
||||
// this is quite wasteful but good enough for medium size meshes
|
||||
|
||||
if (!vertexTexture) {
|
||||
|
||||
@@ -253,7 +253,6 @@ export const TransparencySchema = {
|
||||
tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
|
||||
dTransparency: DefineSpec('boolean'),
|
||||
transparencyAverage: ValueSpec('number'),
|
||||
transparencyMin: ValueSpec('number'),
|
||||
|
||||
uTransparencyGridDim: UniformSpec('v3'),
|
||||
uTransparencyGridTransform: UniformSpec('v4'),
|
||||
|
||||
@@ -60,7 +60,7 @@ interface Renderer {
|
||||
readonly light: Readonly<Light>
|
||||
readonly ambientColor: Vec3
|
||||
|
||||
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean, forceToTransparency?: boolean) => void
|
||||
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean) => void
|
||||
clearDepth: (packed?: boolean) => void
|
||||
update: (camera: ICamera, scene: Scene) => void
|
||||
|
||||
@@ -727,13 +727,13 @@ namespace Renderer {
|
||||
};
|
||||
|
||||
return {
|
||||
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean, forceToTransparency?: boolean) => {
|
||||
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean) => {
|
||||
state.enable(gl.SCISSOR_TEST);
|
||||
state.enable(gl.DEPTH_TEST);
|
||||
state.colorMask(true, true, true, true);
|
||||
state.depthMask(true);
|
||||
|
||||
if (forceToTransparency || transparentBackground && !ignoreTransparentBackground) {
|
||||
if (transparentBackground && !ignoreTransparentBackground) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
} else if (toBackgroundColor) {
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1);
|
||||
|
||||
@@ -88,8 +88,6 @@ interface Scene extends Object3D {
|
||||
readonly emissiveAverage: number
|
||||
/** Opacity average of primitive renderables */
|
||||
readonly opacityAverage: number
|
||||
/** Transparency minimum, excluding fully opaque, of primitive renderables */
|
||||
readonly transparencyMin: number
|
||||
/** Is `true` if any primitive renderable (possibly) has any opaque part */
|
||||
readonly hasOpaque: boolean
|
||||
}
|
||||
@@ -114,13 +112,11 @@ namespace Scene {
|
||||
let markerAverageDirty = true;
|
||||
let emissiveAverageDirty = true;
|
||||
let opacityAverageDirty = true;
|
||||
let transparencyMinDirty = true;
|
||||
let hasOpaqueDirty = true;
|
||||
|
||||
let markerAverage = 0;
|
||||
let emissiveAverage = 0;
|
||||
let opacityAverage = 0;
|
||||
let transparencyMin = 0;
|
||||
let hasOpaque = false;
|
||||
|
||||
const object3d = Object3D.create();
|
||||
@@ -180,7 +176,6 @@ namespace Scene {
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
return true;
|
||||
}
|
||||
@@ -206,7 +201,6 @@ namespace Scene {
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
visibleHash = newVisibleHash;
|
||||
return true;
|
||||
@@ -258,28 +252,6 @@ namespace Scene {
|
||||
return count > 0 ? opacityAverage / count : 0;
|
||||
}
|
||||
|
||||
/** exclude fully opaque parts */
|
||||
function calculateTransparencyMin() {
|
||||
if (primitives.length === 0) return 1;
|
||||
let transparencyMin = 1;
|
||||
const transparenyValues: number[] = [];
|
||||
for (let i = 0, il = primitives.length; i < il; ++i) {
|
||||
const p = primitives[i];
|
||||
if (!p.state.visible) continue;
|
||||
transparenyValues.length = 0;
|
||||
const alpha = clamp(p.values.alpha.ref.value * p.state.alphaFactor, 0, 1);
|
||||
if (alpha < 1) transparenyValues.push(1 - alpha);
|
||||
if (p.values.dXrayShaded?.ref.value === 'on' ||
|
||||
p.values.dXrayShaded?.ref.value === 'inverted' ||
|
||||
p.values.dPointStyle?.ref.value === 'fuzzy' ||
|
||||
p.values.dGeometryType.ref.value === 'text'
|
||||
) transparenyValues.push(0.5);
|
||||
if (p.values.transparencyMin.ref.value > 0) transparenyValues.push(p.values.transparencyMin.ref.value);
|
||||
transparencyMin = Math.min(transparencyMin, ...transparenyValues);
|
||||
}
|
||||
return transparencyMin;
|
||||
}
|
||||
|
||||
function calculateHasOpaque() {
|
||||
if (primitives.length === 0) return false;
|
||||
for (let i = 0, il = primitives.length; i < il; ++i) {
|
||||
@@ -327,7 +299,6 @@ namespace Scene {
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
},
|
||||
add: (o: GraphicsRenderObject) => commitQueue.add(o),
|
||||
@@ -390,13 +361,6 @@ namespace Scene {
|
||||
}
|
||||
return opacityAverage;
|
||||
},
|
||||
get transparencyMin() {
|
||||
if (transparencyMinDirty) {
|
||||
transparencyMin = calculateTransparencyMin();
|
||||
transparencyMinDirty = false;
|
||||
}
|
||||
return transparencyMin;
|
||||
},
|
||||
get hasOpaque() {
|
||||
if (hasOpaqueDirty) {
|
||||
hasOpaque = calculateHasOpaque();
|
||||
|
||||
@@ -42,42 +42,31 @@ export const assign_material_color = `
|
||||
if (fragmentDepth > getDepth(gl_FragCoord.xy / uDrawingBufferSize)) {
|
||||
discard;
|
||||
}
|
||||
vec4 material;
|
||||
if (uRenderMask == MaskOpaque) {
|
||||
#if defined(dXrayShaded)
|
||||
|
||||
#ifndef dXrayShaded
|
||||
#if defined(dTransparency)
|
||||
float dta = 1.0 - vTransparency;
|
||||
if (vTransparency < 0.2) dta = 1.0; // hard cutoff looks better
|
||||
|
||||
if (uRenderMask == MaskTransparent && uAlpha * dta == 1.0) {
|
||||
discard;
|
||||
} else if (uRenderMask == MaskOpaque && uAlpha * dta < 1.0) {
|
||||
discard;
|
||||
}
|
||||
#else
|
||||
if (uRenderMask == MaskTransparent && uAlpha == 1.0) {
|
||||
discard;
|
||||
} else if (uRenderMask == MaskOpaque && uAlpha < 1.0) {
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
if (uRenderMask == MaskOpaque) {
|
||||
discard;
|
||||
#endif
|
||||
#if defined(dTransparency)
|
||||
float dta = 1.0 - vTransparency;
|
||||
#if __VERSION__ == 100 || defined(dVaryingGroup)
|
||||
if (vTransparency < 0.1) dta = 1.0; // hard cutoff to avoid artifacts
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
if (uAlpha * dta < 1.0) {
|
||||
discard;
|
||||
}
|
||||
#else
|
||||
if (uAlpha < 1.0) {
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
material = packDepthToRGBA(fragmentDepth);
|
||||
} else if (uRenderMask == MaskTransparent) {
|
||||
float alpha = uAlpha;
|
||||
#if defined(dTransparency)
|
||||
float dta = 1.0 - vTransparency;
|
||||
alpha *= dta;
|
||||
#endif
|
||||
|
||||
#ifdef dXrayShaded
|
||||
alpha = calcXrayShadedAlpha(alpha, normal);
|
||||
#else
|
||||
if (alpha == 1.0) {
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
material = packDepthWithAlphaToRGBA(fragmentDepth, alpha);
|
||||
}
|
||||
vec4 material = packDepthToRGBA(fragmentDepth);
|
||||
#elif defined(dRenderVariant_marking)
|
||||
vec4 material;
|
||||
if(uMarkingType == 1) {
|
||||
|
||||
@@ -14,10 +14,4 @@ export const check_transparency = `
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(dRenderVariant_depth)
|
||||
#if defined(dTransparentBackfaces_off)
|
||||
if (interior) discard;
|
||||
#endif
|
||||
#endif
|
||||
`;
|
||||
|
||||
@@ -157,7 +157,7 @@ float fbm(in vec3 p) {
|
||||
#elif defined(dXrayShaded_inverted)
|
||||
alpha *= pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
|
||||
#endif
|
||||
return clamp(alpha, 0.001, 0.999);
|
||||
return alpha;
|
||||
}
|
||||
#endif
|
||||
`;
|
||||
@@ -103,15 +103,6 @@ float unpackRGBAToDepth(const in vec4 v) {
|
||||
return dot(v, UnpackFactors);
|
||||
}
|
||||
|
||||
vec4 packDepthWithAlphaToRGBA(const in float depth, const in float alpha){
|
||||
vec3 r = vec3(fract(depth * PackFactors.yz), depth);
|
||||
r.yz -= r.xy * ShiftRight8; // tidy overflow
|
||||
return vec4(r * PackUpscale, alpha);
|
||||
}
|
||||
vec2 unpackRGBAToDepthWithAlpha(const in vec4 v) {
|
||||
return vec2(dot(v.xyz, UnpackFactors.yzw), v.w);
|
||||
}
|
||||
|
||||
vec4 sRGBToLinear(const in vec4 c) {
|
||||
return vec4(mix(pow(c.rgb * 0.9478672986 + vec3(0.0521327014), vec3(2.4)), c.rgb * 0.0773993808, vec3(lessThanEqual(c.rgb, vec3(0.04045)))), c.a);
|
||||
}
|
||||
@@ -141,15 +132,6 @@ float depthToViewZ(const in float isOrtho, const in float linearClipZ, const in
|
||||
return isOrtho == 1.0 ? orthographicDepthToViewZ(linearClipZ, near, far) : perspectiveDepthToViewZ(linearClipZ, near, far);
|
||||
}
|
||||
|
||||
// see https://github.com/graphitemaster/normals_revisited and https://www.shadertoy.com/view/3s33zj
|
||||
mat3 adjoint(const in mat4 m) {
|
||||
return mat3(
|
||||
cross(m[1].xyz, m[2].xyz),
|
||||
cross(m[2].xyz, m[0].xyz),
|
||||
cross(m[0].xyz, m[1].xyz)
|
||||
);
|
||||
}
|
||||
|
||||
#if __VERSION__ == 100
|
||||
// transpose
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ void main() {
|
||||
#include clip_pixel
|
||||
|
||||
#ifdef dNeedsNormal
|
||||
mat3 normalMatrix = adjoint(uView);
|
||||
mat3 normalMatrix = transpose3(inverse3(mat3(uView)));
|
||||
vec3 normal = normalize(normalMatrix * -normalize(cameraNormal));
|
||||
#endif
|
||||
|
||||
|
||||
24
src/mol-gl/shader/depth-merge.frag.ts
Normal file
24
src/mol-gl/shader/depth-merge.frag.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const depthMerge_frag = `
|
||||
precision highp float;
|
||||
precision highp sampler2D;
|
||||
|
||||
uniform sampler2D tDepthPrimitives;
|
||||
uniform sampler2D tDepthVolumes;
|
||||
uniform vec2 uTexSize;
|
||||
|
||||
#include common
|
||||
|
||||
float getDepth(const in vec2 coords, sampler2D tDepth) {
|
||||
#ifdef dPackedDepth
|
||||
return unpackRGBAToDepth(texture2D(tDepth, coords));
|
||||
#else
|
||||
return texture2D(tDepth, coords).r;
|
||||
#endif
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 coords = gl_FragCoord.xy / uTexSize;
|
||||
float depth = min(getDepth(coords, tDepthPrimitives), getDepth(coords, tDepthVolumes));
|
||||
gl_FragColor = packDepthToRGBA(depth);
|
||||
}
|
||||
`;
|
||||
@@ -167,7 +167,7 @@ vec3 v3m4(vec3 p, mat4 m) {
|
||||
float preFogAlphaBlended = 0.0;
|
||||
|
||||
vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
|
||||
mat3 normalMatrix = adjoint(uModelView * vTransform);
|
||||
mat3 normalMatrix = transpose3(inverse3(mat3(uModelView * vTransform)));
|
||||
mat4 cartnToUnit = uCartnToUnit * inverse4(vTransform);
|
||||
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
|
||||
mat4 modelTransform = uModel * vTransform * uTransform;
|
||||
|
||||
@@ -52,7 +52,7 @@ float getDepthOpaque(const in vec2 coords) {
|
||||
|
||||
// Retrieve depth from transparent depth texture
|
||||
float getDepthTransparent(const in vec2 coords) {
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
|
||||
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
|
||||
}
|
||||
|
||||
bool isBackground(const in float depth) {
|
||||
|
||||
@@ -5,9 +5,6 @@ precision highp sampler2D;
|
||||
uniform sampler2D tShaded;
|
||||
uniform sampler2D tColor;
|
||||
uniform sampler2D tNormal;
|
||||
uniform sampler2D tTransparentColor;
|
||||
uniform sampler2D tSsaoDepth;
|
||||
uniform sampler2D tSsaoDepthTransparent;
|
||||
uniform sampler2D tDepthOpaque;
|
||||
uniform sampler2D tDepthTransparent;
|
||||
uniform sampler2D tOutlines;
|
||||
@@ -19,7 +16,6 @@ uniform float uFogNear;
|
||||
uniform float uFogFar;
|
||||
uniform vec3 uFogColor;
|
||||
uniform vec3 uOutlineColor;
|
||||
uniform vec3 uOcclusionColor;
|
||||
uniform bool uTransparentBackground;
|
||||
|
||||
uniform float uDenoiseThreshold;
|
||||
@@ -43,8 +39,8 @@ float getDepthOpaque(const in vec2 coords) {
|
||||
}
|
||||
|
||||
float getDepthTransparent(const in vec2 coords) {
|
||||
#if defined(dTransparentOutline) || defined(dOcclusionEnable)
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
|
||||
#ifdef dTransparentOutline
|
||||
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
|
||||
#else
|
||||
return 1.0;
|
||||
#endif
|
||||
@@ -54,28 +50,6 @@ bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
float getSsao(vec2 coords) {
|
||||
float rawSsao = unpackRGToUnitInterval(texture2D(tSsaoDepth, coords).xy);
|
||||
if (rawSsao > 0.999) {
|
||||
return 1.0;
|
||||
} else if (rawSsao > 0.001) {
|
||||
return rawSsao;
|
||||
}
|
||||
// treat values close to 0.0 as errors and return no occlusion
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float getSsaoTransparent(vec2 coords) {
|
||||
float rawSsao = unpackRGToUnitInterval(texture2D(tSsaoDepthTransparent, coords).xy);
|
||||
if (rawSsao > 0.999) {
|
||||
return 1.0;
|
||||
} else if (rawSsao > 0.001) {
|
||||
return rawSsao;
|
||||
}
|
||||
// treat values close to 0.0 as errors and return no occlusion
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// TODO: investigate
|
||||
@@ -143,16 +117,20 @@ vec4 smartDeNoise(sampler2D tex, vec2 uv) {
|
||||
return aBuff / zBuff;
|
||||
}
|
||||
|
||||
int squaredOutlineScale = dOutlineScale * dOutlineScale;
|
||||
float getOutline(const in vec2 coords, const in float opaqueDepth, const in float transparentDepth, out float closestTexel, out float isTransparent) {
|
||||
float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
|
||||
float backgroundViewZ = 2.0 * uFar;
|
||||
vec2 invTexSize = 1.0 / uTexSize;
|
||||
|
||||
float transparentDepth = getDepthTransparent(coords);
|
||||
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
|
||||
float transparentSelfViewZ = isBackground(transparentDepth) ? backgroundViewZ : getViewZ(transparentDepth);
|
||||
float selfDepth = min(opaqueDepth, transparentDepth);
|
||||
|
||||
float outline = 1.0;
|
||||
closestTexel = 1.0;
|
||||
isTransparent = 0.0;
|
||||
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
|
||||
for (int x = -dOutlineScale; x <= dOutlineScale; x++) {
|
||||
if (x * x + y * y > squaredOutlineScale) {
|
||||
if (x * x + y * y > dOutlineScale * dOutlineScale) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -161,15 +139,16 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, const in floa
|
||||
vec4 sampleOutlineCombined = texture2D(tOutlines, sampleCoords);
|
||||
float sampleOutline = sampleOutlineCombined.r;
|
||||
float sampleOutlineDepth = unpackRGToUnitInterval(sampleOutlineCombined.gb);
|
||||
float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
|
||||
|
||||
float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
|
||||
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
|
||||
outline = 0.0;
|
||||
closestTexel = sampleOutlineDepth;
|
||||
isTransparent = sampleOutlineCombined.a;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isTransparent == 0.0 ? outline : (closestTexel > opaqueDepth && closestTexel < transparentDepth) ? 1.0 : outline;
|
||||
return closestTexel < opaqueDepth ? outline : 1.0;
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -182,19 +161,12 @@ void main() {
|
||||
#endif
|
||||
|
||||
float opaqueDepth = getDepthOpaque(coords);
|
||||
|
||||
float backgroundViewZ = 2.0 * uFar;
|
||||
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, abs(opaqueSelfViewZ));
|
||||
float fogAlpha = 1.0 - fogFactor;
|
||||
|
||||
float transparentDepth = 1.0;
|
||||
#ifdef dBlendTransparency
|
||||
bool blendTransparency = true;
|
||||
vec4 transparentColor = texture2D(tTransparentColor, coords);
|
||||
|
||||
transparentDepth = getDepthTransparent(coords);
|
||||
#endif
|
||||
|
||||
float alpha = 1.0;
|
||||
if (!uTransparentBackground) {
|
||||
// mix opaque objects with background color
|
||||
@@ -205,63 +177,17 @@ void main() {
|
||||
color.rgb *= fogAlpha;
|
||||
}
|
||||
|
||||
#if defined(dOcclusionEnable)
|
||||
if (!isBackground(opaqueDepth)) {
|
||||
float occlusionFactor = getSsao(coords);
|
||||
|
||||
if (!uTransparentBackground) {
|
||||
color.rgb = mix(mix(uOcclusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
|
||||
} else {
|
||||
color.rgb = mix(uOcclusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
|
||||
}
|
||||
}
|
||||
#ifdef dBlendTransparency
|
||||
if (!isBackground(transparentDepth)) {
|
||||
float viewDist = abs(getViewZ(transparentDepth));
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
float occlusionFactor = getSsaoTransparent(coords);
|
||||
transparentColor.rgb = mix(uOcclusionColor * (1.0 - fogFactor), transparentColor.rgb, occlusionFactor);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef dOutlineEnable
|
||||
float closestTexel;
|
||||
float isTransparentOutline;
|
||||
float outline = getOutline(coords, opaqueDepth, transparentDepth, closestTexel, isTransparentOutline);
|
||||
float outline = getOutline(coords, opaqueDepth, closestTexel);
|
||||
if (outline == 0.0) {
|
||||
float viewDist = abs(getViewZ(closestTexel));
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
float fogFactorOutline = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
if (!uTransparentBackground) {
|
||||
color.rgb = mix(uOutlineColor, uFogColor, fogFactor);
|
||||
color.rgb = mix(uOutlineColor, uFogColor, fogFactorOutline);
|
||||
} else {
|
||||
alpha = 1.0 - fogFactor;
|
||||
color.rgb = mix(uOutlineColor, vec3(0.0), fogFactor);
|
||||
}
|
||||
#ifdef dBlendTransparency
|
||||
if (isTransparentOutline == 1.0 || transparentDepth > closestTexel) {
|
||||
blendTransparency = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef dBlendTransparency
|
||||
if (blendTransparency) {
|
||||
if (transparentColor.a != 0.0) {
|
||||
if (isBackground(opaqueDepth)) {
|
||||
if (uTransparentBackground) {
|
||||
color = transparentColor;
|
||||
alpha = transparentColor.a;
|
||||
} else {
|
||||
color.rgb = transparentColor.rgb + uFogColor * (1.0 - transparentColor.a);
|
||||
alpha = 1.0;
|
||||
}
|
||||
} else {
|
||||
// blending
|
||||
color = transparentColor + color * (1.0 - transparentColor.a);
|
||||
alpha = transparentColor.a + alpha * (1.0 - transparentColor.a);
|
||||
}
|
||||
color.rgb = mix(uOutlineColor, color.rgb, fogFactorOutline);
|
||||
alpha = 1.0 - fogFactorOutline;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -121,7 +121,7 @@ void main() {
|
||||
gl_FragColor = vec4(packIntToRGB(float(uObjectId)), 1.0);
|
||||
gl_FragData[1] = vec4(packIntToRGB(vInstance), 1.0);
|
||||
gl_FragData[2] = vec4(texture2D(tGroupTex, vUv).rgb, 1.0);
|
||||
gl_FragData[3] = packDepthToRGBA(fragmentDepth);
|
||||
gl_FragData[3] = packDepthToRGBA(gl_FragCoord.z);
|
||||
#else
|
||||
gl_FragColor = vColor;
|
||||
if (uPickType == 1) {
|
||||
@@ -135,11 +135,7 @@ void main() {
|
||||
#elif defined(dRenderVariant_depth)
|
||||
if (imageData.a < 0.05)
|
||||
discard;
|
||||
if (uRenderMask == MaskOpaque) {
|
||||
gl_FragColor = packDepthToRGBA(fragmentDepth);
|
||||
} else if (uRenderMask == MaskTransparent) {
|
||||
gl_FragColor = packDepthWithAlphaToRGBA(fragmentDepth, imageData.a);
|
||||
}
|
||||
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
|
||||
#elif defined(dRenderVariant_marking)
|
||||
float marker = uMarker;
|
||||
if (uMarker == -1.0) {
|
||||
@@ -150,7 +146,7 @@ void main() {
|
||||
if (uMarkingType == 1) {
|
||||
if (marker > 0.0 || imageData.a < 0.05)
|
||||
discard;
|
||||
gl_FragColor = packDepthToRGBA(fragmentDepth);
|
||||
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
|
||||
} else {
|
||||
if (marker == 0.0 || imageData.a < 0.05)
|
||||
discard;
|
||||
|
||||
@@ -38,14 +38,9 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim)
|
||||
return texture2D(tex, coord);
|
||||
}
|
||||
|
||||
float voxelValue(vec3 pos) {
|
||||
vec4 voxel(vec3 pos) {
|
||||
pos = min(max(vec3(0.0), pos), uGridDim - vec3(1.0));
|
||||
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
|
||||
#ifdef dValueChannel_red
|
||||
return v.r;
|
||||
#else
|
||||
return v.a;
|
||||
#endif
|
||||
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
@@ -53,14 +48,14 @@ void main(void) {
|
||||
vec3 posXYZ = index3dFrom2d(uv);
|
||||
|
||||
// get MC case as the sum of corners that are below the given iso level
|
||||
float c = step(voxelValue(posXYZ), uIsoValue)
|
||||
+ 2. * step(voxelValue(posXYZ + c1), uIsoValue)
|
||||
+ 4. * step(voxelValue(posXYZ + c2), uIsoValue)
|
||||
+ 8. * step(voxelValue(posXYZ + c3), uIsoValue)
|
||||
+ 16. * step(voxelValue(posXYZ + c4), uIsoValue)
|
||||
+ 32. * step(voxelValue(posXYZ + c5), uIsoValue)
|
||||
+ 64. * step(voxelValue(posXYZ + c6), uIsoValue)
|
||||
+ 128. * step(voxelValue(posXYZ + c7), uIsoValue);
|
||||
float c = step(voxel(posXYZ).a, uIsoValue)
|
||||
+ 2. * step(voxel(posXYZ + c1).a, uIsoValue)
|
||||
+ 4. * step(voxel(posXYZ + c2).a, uIsoValue)
|
||||
+ 8. * step(voxel(posXYZ + c3).a, uIsoValue)
|
||||
+ 16. * step(voxel(posXYZ + c4).a, uIsoValue)
|
||||
+ 32. * step(voxel(posXYZ + c5).a, uIsoValue)
|
||||
+ 64. * step(voxel(posXYZ + c6).a, uIsoValue)
|
||||
+ 128. * step(voxel(posXYZ + c7).a, uIsoValue);
|
||||
c *= step(c, 254.);
|
||||
|
||||
// handle out of bounds positions
|
||||
|
||||
@@ -59,14 +59,9 @@ vec4 voxel(vec3 pos) {
|
||||
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
|
||||
}
|
||||
|
||||
float voxelValuePadded(vec3 pos) {
|
||||
vec4 voxelPadded(vec3 pos) {
|
||||
pos = min(max(vec3(0.0), pos), uGridDim - vec3(vec2(2.0), 1.0)); // remove xy padding
|
||||
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
|
||||
#ifdef dValueChannel_red
|
||||
return v.r;
|
||||
#else
|
||||
return v.a;
|
||||
#endif
|
||||
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
|
||||
}
|
||||
|
||||
int idot2(const in ivec2 a, const in ivec2 b) {
|
||||
@@ -266,13 +261,8 @@ void main(void) {
|
||||
vec4 d0 = voxel(b0);
|
||||
vec4 d1 = voxel(b1);
|
||||
|
||||
#ifdef dValueChannel_red
|
||||
float v0 = d0.r;
|
||||
float v1 = d1.r;
|
||||
#else
|
||||
float v0 = d0.a;
|
||||
float v1 = d1.a;
|
||||
#endif
|
||||
float v0 = d0.a;
|
||||
float v1 = d1.a;
|
||||
|
||||
float t = (uIsoValue - v0) / (v0 - v1);
|
||||
gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz;
|
||||
@@ -296,14 +286,14 @@ void main(void) {
|
||||
|
||||
// normals from gradients
|
||||
vec3 n0 = -normalize(vec3(
|
||||
voxelValuePadded(b0 - c1) - voxelValuePadded(b0 + c1),
|
||||
voxelValuePadded(b0 - c3) - voxelValuePadded(b0 + c3),
|
||||
voxelValuePadded(b0 - c4) - voxelValuePadded(b0 + c4)
|
||||
voxelPadded(b0 - c1).a - voxelPadded(b0 + c1).a,
|
||||
voxelPadded(b0 - c3).a - voxelPadded(b0 + c3).a,
|
||||
voxelPadded(b0 - c4).a - voxelPadded(b0 + c4).a
|
||||
));
|
||||
vec3 n1 = -normalize(vec3(
|
||||
voxelValuePadded(b1 - c1) - voxelValuePadded(b1 + c1),
|
||||
voxelValuePadded(b1 - c3) - voxelValuePadded(b1 + c3),
|
||||
voxelValuePadded(b1 - c4) - voxelValuePadded(b1 + c4)
|
||||
voxelPadded(b1 - c1).a - voxelPadded(b1 + c1).a,
|
||||
voxelPadded(b1 - c3).a - voxelPadded(b1 + c3).a,
|
||||
voxelPadded(b1 - c4).a - voxelPadded(b1 + c4).a
|
||||
));
|
||||
gl_FragData[2].xyz = -vec3(
|
||||
n0.x + t * (n0.x - n1.x),
|
||||
@@ -317,6 +307,6 @@ void main(void) {
|
||||
}
|
||||
|
||||
// apply normal matrix
|
||||
gl_FragData[2].xyz *= adjoint(uGridTransform);
|
||||
gl_FragData[2].xyz *= transpose3(inverse3(mat3(uGridTransform)));
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 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 Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -44,7 +44,7 @@ void main(){
|
||||
#else
|
||||
vec3 normal = aNormal;
|
||||
#endif
|
||||
mat3 normalMatrix = adjoint(modelView);
|
||||
mat3 normalMatrix = transpose3(inverse3(mat3(modelView)));
|
||||
vec3 transformedNormal = normalize(normalMatrix * normalize(normal));
|
||||
#if defined(dFlipSided)
|
||||
if (!uDoubleSided) { // TODO checking uDoubleSided should not be required, ASR
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -40,7 +40,7 @@ float getDepthOpaque(const in vec2 coords) {
|
||||
|
||||
float getDepthTransparent(const in vec2 coords) {
|
||||
#ifdef dTransparentOutline
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
|
||||
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
|
||||
#else
|
||||
return 1.0;
|
||||
#endif
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 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 Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
export const postprocessing_frag = `
|
||||
@@ -12,11 +11,8 @@ precision highp int;
|
||||
precision highp sampler2D;
|
||||
|
||||
uniform sampler2D tSsaoDepth;
|
||||
uniform sampler2D tSsaoDepthTransparent;
|
||||
uniform sampler2D tColor;
|
||||
uniform sampler2D tTransparentColor;
|
||||
uniform sampler2D tDepthOpaque;
|
||||
uniform sampler2D tDepthTransparent;
|
||||
uniform sampler2D tShadows;
|
||||
uniform sampler2D tOutlines;
|
||||
uniform vec2 uTexSize;
|
||||
@@ -49,24 +45,19 @@ float getDepthOpaque(const in vec2 coords) {
|
||||
#endif
|
||||
}
|
||||
|
||||
float getDepthTransparent(const in vec2 coords) {
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
|
||||
}
|
||||
|
||||
bool isBackground(const in float depth) {
|
||||
return depth > 0.999; // handle depth packing precision issues
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
int squaredOutlineScale = dOutlineScale * dOutlineScale;
|
||||
float getOutline(const in vec2 coords, const in float opaqueDepth, const in float transparentDepth, out float closestTexel, out float isTransparent) {
|
||||
float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
|
||||
float backgroundViewZ = 2.0 * uFar;
|
||||
vec2 invTexSize = 1.0 / uTexSize;
|
||||
|
||||
float outline = 1.0;
|
||||
closestTexel = 1.0;
|
||||
isTransparent = 0.0;
|
||||
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
|
||||
for (int x = -dOutlineScale; x <= dOutlineScale; x++) {
|
||||
if (x * x + y * y > squaredOutlineScale) {
|
||||
if (x * x + y * y > dOutlineScale * dOutlineScale) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -79,11 +70,10 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, const in floa
|
||||
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
|
||||
outline = 0.0;
|
||||
closestTexel = sampleOutlineDepth;
|
||||
isTransparent = sampleOutlineCombined.a;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isTransparent == 0.0 ? outline : (closestTexel > opaqueDepth && closestTexel < transparentDepth) ? 1.0 : outline;
|
||||
return closestTexel < opaqueDepth ? outline : 1.0;
|
||||
}
|
||||
|
||||
float getSsao(vec2 coords) {
|
||||
@@ -97,60 +87,31 @@ float getSsao(vec2 coords) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float getSsaoTransparent(vec2 coords) {
|
||||
float rawSsao = unpackRGToUnitInterval(texture2D(tSsaoDepthTransparent, coords).xy);
|
||||
if (rawSsao > 0.999) {
|
||||
return 1.0;
|
||||
} else if (rawSsao > 0.001) {
|
||||
return rawSsao;
|
||||
}
|
||||
// treat values close to 0.0 as errors and return no occlusion
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
vec2 coords = gl_FragCoord.xy / uTexSize;
|
||||
vec4 color = texture2D(tColor, coords);
|
||||
|
||||
float viewDist;
|
||||
float fogFactor;
|
||||
float opaqueDepth = getDepthOpaque(coords);
|
||||
float transparentDepth = 1.0;
|
||||
#ifdef dBlendTransparency
|
||||
bool blendTransparency = true;
|
||||
vec4 transparentColor = texture2D(tTransparentColor, coords);
|
||||
|
||||
#if defined(dOutlineEnable) || defined(dOcclusionEnable) && defined(dOcclusionIncludeTransparency)
|
||||
transparentDepth = getDepthTransparent(coords);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(dOcclusionEnable) || defined(dShadowEnable)
|
||||
bool isOpaqueBackground = isBackground(opaqueDepth);
|
||||
float viewDist = abs(getViewZ(opaqueDepth));
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
#endif
|
||||
|
||||
#if defined(dOcclusionEnable)
|
||||
if (!isOpaqueBackground) {
|
||||
#ifdef dOcclusionEnable
|
||||
if (!isBackground(opaqueDepth)) {
|
||||
viewDist = abs(getViewZ(opaqueDepth));
|
||||
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
float occlusionFactor = getSsao(coords + uOcclusionOffset);
|
||||
|
||||
if (!uTransparentBackground) {
|
||||
color.rgb = mix(mix(uOcclusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
|
||||
} else {
|
||||
color.rgb = mix(uOcclusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
|
||||
}
|
||||
}
|
||||
#if defined(dBlendTransparency) && defined(dOcclusionIncludeTransparency)
|
||||
if (!isBackground(transparentDepth)) {
|
||||
float viewDist = abs(getViewZ(transparentDepth));
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
float occlusionFactor = getSsaoTransparent(coords + uOcclusionOffset);
|
||||
transparentColor.rgb = mix(uOcclusionColor * (1.0 - fogFactor), transparentColor.rgb, occlusionFactor);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef dShadowEnable
|
||||
if (!isOpaqueBackground) {
|
||||
if (!isBackground(opaqueDepth)) {
|
||||
viewDist = abs(getViewZ(opaqueDepth));
|
||||
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
vec4 shadow = texture2D(tShadows, coords);
|
||||
if (!uTransparentBackground) {
|
||||
color.rgb = mix(mix(vec3(0), uFogColor, fogFactor), color.rgb, shadow.a);
|
||||
@@ -163,31 +124,15 @@ void main(void) {
|
||||
// outline needs to be handled after occlusion and shadow to keep them clean
|
||||
#ifdef dOutlineEnable
|
||||
float closestTexel;
|
||||
float isTransparentOutline;
|
||||
float outline = getOutline(coords, opaqueDepth, transparentDepth, closestTexel, isTransparentOutline);
|
||||
float outline = getOutline(coords, opaqueDepth, closestTexel);
|
||||
if (outline == 0.0) {
|
||||
float viewDist = abs(getViewZ(closestTexel));
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
viewDist = abs(getViewZ(closestTexel));
|
||||
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
|
||||
if (!uTransparentBackground) {
|
||||
color.rgb = mix(uOutlineColor, uFogColor, fogFactor);
|
||||
color.rgb = mix(uOutlineColor, uFogColor, fogFactor);
|
||||
} else {
|
||||
color.a = 1.0 - fogFactor;
|
||||
color.rgb = mix(uOutlineColor, vec3(0.0), fogFactor);
|
||||
}
|
||||
#ifdef dBlendTransparency
|
||||
if (isTransparentOutline == 1.0 || transparentDepth > closestTexel) {
|
||||
blendTransparency = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef dBlendTransparency
|
||||
if (blendTransparency) {
|
||||
float alpha = transparentColor.a;
|
||||
if (alpha != 0.0) {
|
||||
// blending
|
||||
color = transparentColor + color * (1.0 - alpha);
|
||||
color.rgb = mix(uOutlineColor, color.rgb, fogFactor);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -17,6 +17,9 @@ uniform vec4 uBounds;
|
||||
|
||||
uniform float uKernel[dOcclusionKernelSize];
|
||||
uniform float uBlurDepthBias;
|
||||
uniform float uBlurNormalBias;
|
||||
uniform float uBlurStepSize;
|
||||
uniform vec2 uBlurStepOffset;
|
||||
|
||||
uniform float uBlurDirectionX;
|
||||
uniform float uBlurDirectionY;
|
||||
@@ -36,8 +39,7 @@ float getViewZ(const in float depth) {
|
||||
}
|
||||
|
||||
bool isBackground(const in float depth) {
|
||||
// checking for 1.0 is not enough, because of precision issues
|
||||
return depth >= 0.999;
|
||||
return depth > 0.999;
|
||||
}
|
||||
|
||||
bool isNearClip(const in float depth) {
|
||||
@@ -54,6 +56,64 @@ float getPixelSize(const in vec2 coords, const in float depth) {
|
||||
return distance(viewPos0, viewPos1);
|
||||
}
|
||||
|
||||
#ifdef dBlurNormalBias
|
||||
float getDepth(const in vec2 coords) {
|
||||
return unpackRGToUnitInterval(texture2D(tSsaoDepth, coords).zw);
|
||||
}
|
||||
|
||||
// adapted from https://gist.github.com/bgolus/a07ed65602c009d5e2f753826e8078a0
|
||||
vec3 viewNormalAtPixelPositionAccurate(const in vec2 vpos) {
|
||||
// current pixel's depth
|
||||
float c = getDepth(vpos);
|
||||
|
||||
// get current pixel's view space position
|
||||
vec3 viewSpacePos_c = screenSpaceToViewSpace(vec3(vpos, c), uInvProjection);
|
||||
|
||||
// get view space position at 1 pixel offsets in each major direction
|
||||
vec3 viewSpacePos_l = screenSpaceToViewSpace(vec3(vpos + vec2(-1.0, 0.0) / uTexSize, getDepth(vpos + vec2(-1.0, 0.0) / uTexSize)), uInvProjection);
|
||||
vec3 viewSpacePos_r = screenSpaceToViewSpace(vec3(vpos + vec2( 1.0, 0.0) / uTexSize, getDepth(vpos + vec2( 1.0, 0.0) / uTexSize)), uInvProjection);
|
||||
vec3 viewSpacePos_d = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0,-1.0) / uTexSize, getDepth(vpos + vec2( 0.0,-1.0) / uTexSize)), uInvProjection);
|
||||
vec3 viewSpacePos_u = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0, 1.0) / uTexSize, getDepth(vpos + vec2( 0.0, 1.0) / uTexSize)), uInvProjection);
|
||||
|
||||
// get the difference between the current and each offset position
|
||||
vec3 l = viewSpacePos_c - viewSpacePos_l;
|
||||
vec3 r = viewSpacePos_r - viewSpacePos_c;
|
||||
vec3 d = viewSpacePos_c - viewSpacePos_d;
|
||||
vec3 u = viewSpacePos_u - viewSpacePos_c;
|
||||
|
||||
// get depth values at 1 & 2 pixels offsets from current along the horizontal axis
|
||||
vec4 H = vec4(
|
||||
getDepth(vpos + vec2(-1.0, 0.0) / uTexSize),
|
||||
getDepth(vpos + vec2( 1.0, 0.0) / uTexSize),
|
||||
getDepth(vpos + vec2(-2.0, 0.0) / uTexSize),
|
||||
getDepth(vpos + vec2( 2.0, 0.0) / uTexSize)
|
||||
);
|
||||
|
||||
// get depth values at 1 & 2 pixels offsets from current along the vertical axis
|
||||
vec4 V = vec4(
|
||||
getDepth(vpos + vec2(0.0,-1.0) / uTexSize),
|
||||
getDepth(vpos + vec2(0.0, 1.0) / uTexSize),
|
||||
getDepth(vpos + vec2(0.0,-2.0) / uTexSize),
|
||||
getDepth(vpos + vec2(0.0, 2.0) / uTexSize)
|
||||
);
|
||||
|
||||
// current pixel's depth difference from slope of offset depth samples
|
||||
// differs from original article because we're using non-linear depth values
|
||||
// see article's comments
|
||||
vec2 he = abs((2.0 * H.xy - H.zw) - c);
|
||||
vec2 ve = abs((2.0 * V.xy - V.zw) - c);
|
||||
|
||||
// pick horizontal and vertical diff with the smallest depth difference from slopes
|
||||
vec3 hDeriv = he.x < he.y ? l : r;
|
||||
vec3 vDeriv = ve.x < ve.y ? d : u;
|
||||
|
||||
// get view space normal from the cross product of the best derivatives
|
||||
vec3 viewNormal = normalize(cross(hDeriv, vDeriv));
|
||||
|
||||
return viewNormal;
|
||||
}
|
||||
#endif
|
||||
|
||||
void main(void) {
|
||||
vec2 coords = gl_FragCoord.xy / uTexSize;
|
||||
|
||||
@@ -65,8 +125,7 @@ void main(void) {
|
||||
}
|
||||
|
||||
float selfDepth = unpackRGToUnitInterval(packedDepth);
|
||||
// (if background and if second pass) or if near clip
|
||||
if ((isBackground(selfDepth) && uBlurDirectionY != 0.0) || isNearClip(selfDepth)) {
|
||||
if (isBackground(selfDepth) || isNearClip(selfDepth)) {
|
||||
gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
|
||||
return;
|
||||
}
|
||||
@@ -74,7 +133,13 @@ void main(void) {
|
||||
float selfViewZ = getViewZ(selfDepth);
|
||||
float pixelSize = getPixelSize(coords, selfDepth);
|
||||
|
||||
#ifdef dBlurNormalBias
|
||||
vec3 selfNormal = viewNormalAtPixelPositionAccurate(coords);
|
||||
#endif
|
||||
|
||||
vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
|
||||
coords += uBlurStepOffset * vec2(uBlurDirectionX, uBlurDirectionY);
|
||||
offset *= uBlurStepSize;
|
||||
|
||||
float sum = 0.0;
|
||||
float kernelSum = 0.0;
|
||||
@@ -94,6 +159,13 @@ void main(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef dBlurNormalBias
|
||||
vec3 sampleNormal = viewNormalAtPixelPositionAccurate(sampleCoords);
|
||||
if (saturate(dot(selfNormal, sampleNormal)) < uBlurNormalBias) {
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
float sampleViewZ = getViewZ(sampleDepth);
|
||||
if (abs(selfViewZ - sampleViewZ) >= uBlurDepthBias) {
|
||||
continue;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
export const ssao_frag = `
|
||||
@@ -14,19 +13,9 @@ precision highp sampler2D;
|
||||
|
||||
#include common
|
||||
|
||||
|
||||
uniform sampler2D tDepth;
|
||||
uniform sampler2D tDepthHalf;
|
||||
uniform sampler2D tDepthQuarter;
|
||||
|
||||
#if defined(dIncludeTransparent)
|
||||
uniform sampler2D tDepthTransparent;
|
||||
uniform sampler2D tDepthHalfTransparent;
|
||||
uniform sampler2D tDepthQuarterTransparent;
|
||||
#endif
|
||||
|
||||
uniform int uTransparencyFlag;
|
||||
|
||||
uniform vec2 uTexSize;
|
||||
uniform vec4 uBounds;
|
||||
|
||||
@@ -64,33 +53,18 @@ vec2 getNoiseVec2(const in vec2 coords) {
|
||||
}
|
||||
|
||||
bool isBackground(const in float depth) {
|
||||
return depth > 0.999; // handle precision issues with packed depth
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
float getDepth(const in vec2 coords, const in int transparentFlag) {
|
||||
float getDepth(const in vec2 coords) {
|
||||
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
|
||||
if (transparentFlag == 1){
|
||||
#if defined(dIncludeTransparent)
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, c)).x;
|
||||
#else
|
||||
return 1.0;
|
||||
#endif
|
||||
} else {
|
||||
#ifdef depthTextureSupport
|
||||
return texture2D(tDepth, c).r;
|
||||
#else
|
||||
return unpackRGBAToDepth(texture2D(tDepth, c));
|
||||
#endif
|
||||
}
|
||||
#ifdef depthTextureSupport
|
||||
return texture2D(tDepth, c).r;
|
||||
#else
|
||||
return unpackRGBAToDepth(texture2D(tDepth, c));
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(dIncludeTransparent)
|
||||
vec2 getDepthTransparentWithAlpha(const in vec2 coords){
|
||||
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, c));
|
||||
}
|
||||
#endif
|
||||
|
||||
#define dQuarterThreshold 0.1
|
||||
#define dHalfThreshold 0.05
|
||||
|
||||
@@ -116,72 +90,6 @@ float getMappedDepth(const in vec2 coords, const in vec2 selfCoords) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(dIncludeTransparent)
|
||||
vec2 getMappedDepthTransparentWithAlpha(const in vec2 coords, const in vec2 selfCoords) {
|
||||
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
|
||||
float d = distance(coords, selfCoords);
|
||||
if (d > dQuarterThreshold) {
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthQuarterTransparent, c));
|
||||
} else if (d > dHalfThreshold) {
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthHalfTransparent, c));
|
||||
} else {
|
||||
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, c));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// adapted from https://gist.github.com/bgolus/a07ed65602c009d5e2f753826e8078a0
|
||||
vec3 viewNormalAtPixelPositionAccurate(const in vec2 vpos, const in int transparentFlag) {
|
||||
// current pixel's depth
|
||||
float c = getDepth(vpos, transparentFlag);
|
||||
|
||||
// get current pixel's view space position
|
||||
vec3 viewSpacePos_c = screenSpaceToViewSpace(vec3(vpos, c), uInvProjection);
|
||||
|
||||
// get view space position at 1 pixel offsets in each major direction
|
||||
vec3 viewSpacePos_l = screenSpaceToViewSpace(vec3(vpos + vec2(-1.0, 0.0) / uTexSize, getDepth(vpos + vec2(-1.0, 0.0) / uTexSize, transparentFlag)), uInvProjection);
|
||||
vec3 viewSpacePos_r = screenSpaceToViewSpace(vec3(vpos + vec2( 1.0, 0.0) / uTexSize, getDepth(vpos + vec2( 1.0, 0.0) / uTexSize, transparentFlag)), uInvProjection);
|
||||
vec3 viewSpacePos_d = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0,-1.0) / uTexSize, getDepth(vpos + vec2( 0.0,-1.0) / uTexSize, transparentFlag)), uInvProjection);
|
||||
vec3 viewSpacePos_u = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0, 1.0) / uTexSize, getDepth(vpos + vec2( 0.0, 1.0) / uTexSize, transparentFlag)), uInvProjection);
|
||||
|
||||
// get the difference between the current and each offset position
|
||||
vec3 l = viewSpacePos_c - viewSpacePos_l;
|
||||
vec3 r = viewSpacePos_r - viewSpacePos_c;
|
||||
vec3 d = viewSpacePos_c - viewSpacePos_d;
|
||||
vec3 u = viewSpacePos_u - viewSpacePos_c;
|
||||
|
||||
// get depth values at 1 & 2 pixels offsets from current along the horizontal axis
|
||||
vec4 H = vec4(
|
||||
getDepth(vpos + vec2(-1.0, 0.0) / uTexSize, transparentFlag),
|
||||
getDepth(vpos + vec2( 1.0, 0.0) / uTexSize, transparentFlag),
|
||||
getDepth(vpos + vec2(-2.0, 0.0) / uTexSize, transparentFlag),
|
||||
getDepth(vpos + vec2( 2.0, 0.0) / uTexSize, transparentFlag)
|
||||
);
|
||||
|
||||
// get depth values at 1 & 2 pixels offsets from current along the vertical axis
|
||||
vec4 V = vec4(
|
||||
getDepth(vpos + vec2(0.0,-1.0) / uTexSize, transparentFlag),
|
||||
getDepth(vpos + vec2(0.0, 1.0) / uTexSize, transparentFlag),
|
||||
getDepth(vpos + vec2(0.0,-2.0) / uTexSize, transparentFlag),
|
||||
getDepth(vpos + vec2(0.0, 2.0) / uTexSize, transparentFlag)
|
||||
);
|
||||
|
||||
// current pixel's depth difference from slope of offset depth samples
|
||||
// differs from original article because we're using non-linear depth values
|
||||
// see article's comments
|
||||
vec2 he = abs((2.0 * H.xy - H.zw) - c);
|
||||
vec2 ve = abs((2.0 * V.xy - V.zw) - c);
|
||||
|
||||
// pick horizontal and vertical diff with the smallest depth difference from slopes
|
||||
vec3 hDeriv = he.x < he.y ? l : r;
|
||||
vec3 vDeriv = ve.x < ve.y ? d : u;
|
||||
|
||||
// get view space normal from the cross product of the best derivatives
|
||||
vec3 viewNormal = normalize(cross(hDeriv, vDeriv));
|
||||
|
||||
return viewNormal;
|
||||
}
|
||||
|
||||
float getPixelSize(const in vec2 coords, const in float depth) {
|
||||
vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
|
||||
vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
|
||||
@@ -192,7 +100,8 @@ float getPixelSize(const in vec2 coords, const in float depth) {
|
||||
void main(void) {
|
||||
vec2 invTexSize = 1.0 / uTexSize;
|
||||
vec2 selfCoords = gl_FragCoord.xy * invTexSize;
|
||||
float selfDepth = getDepth(selfCoords, uTransparencyFlag);
|
||||
|
||||
float selfDepth = getDepth(selfCoords);
|
||||
vec2 selfPackedDepth = packUnitIntervalToRG(selfDepth);
|
||||
|
||||
if (isBackground(selfDepth)) {
|
||||
@@ -200,8 +109,8 @@ void main(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
vec3 selfViewNormal = viewNormalAtPixelPositionAccurate(selfCoords, uTransparencyFlag);
|
||||
vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
|
||||
vec3 selfViewNormal = normalize(cross(dFdx(selfViewPos), dFdy(selfViewPos)));
|
||||
|
||||
vec3 randomVec = normalize(vec3(getNoiseVec2(selfCoords) * 2.0 - 1.0, 0.0));
|
||||
vec3 tangent = normalize(randomVec - selfViewNormal * dot(randomVec, selfViewNormal));
|
||||
@@ -229,26 +138,9 @@ void main(void) {
|
||||
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
|
||||
|
||||
// get sample depth:
|
||||
float sampleOcc = 0.0;
|
||||
#ifdef dIllumination
|
||||
if (uTransparencyFlag == 1) {
|
||||
#endif
|
||||
float sampleDepth = getMappedDepth(offset.xy, selfCoords);
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
|
||||
|
||||
sampleOcc = step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l];
|
||||
#ifdef dIllumination
|
||||
}
|
||||
#endif
|
||||
#if defined(dIncludeTransparent)
|
||||
vec2 sampleDepthWithAlpha = getMappedDepthTransparentWithAlpha(offset.xy, selfCoords);
|
||||
if (!isBackground(sampleDepthWithAlpha.x)) {
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepthWithAlpha.x), uInvProjection).z;
|
||||
sampleOcc = max(sampleOcc, step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l] * sampleDepthWithAlpha.y);
|
||||
}
|
||||
#endif
|
||||
|
||||
levelOcclusion += sampleOcc;
|
||||
float sampleDepth = getMappedDepth(offset.xy, selfCoords);
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
|
||||
levelOcclusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l];
|
||||
}
|
||||
occlusion = max(occlusion, levelOcclusion);
|
||||
}
|
||||
@@ -261,27 +153,11 @@ void main(void) {
|
||||
offset = uProjection * offset;
|
||||
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
|
||||
|
||||
float sampleOcc = 0.0;
|
||||
#ifdef dIllumination
|
||||
if (uTransparencyFlag == 1) {
|
||||
#endif
|
||||
// NOTE: using getMappedDepth here causes issues on some mobile devices
|
||||
float sampleDepth = getDepth(offset.xy, 0);
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
|
||||
// NOTE: using getMappedDepth here causes issues on some mobile devices
|
||||
float sampleDepth = getDepth(offset.xy);
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
|
||||
|
||||
sampleOcc = step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
|
||||
#ifdef dIllumination
|
||||
}
|
||||
#endif
|
||||
#if defined(dIncludeTransparent)
|
||||
vec2 sampleDepthWithAlpha = getDepthTransparentWithAlpha(offset.xy);
|
||||
if (!isBackground(sampleDepthWithAlpha.x)) {
|
||||
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepthWithAlpha.x), uInvProjection).z;
|
||||
sampleOcc = max(sampleOcc, step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ)) * sampleDepthWithAlpha.y);
|
||||
}
|
||||
#endif
|
||||
|
||||
occlusion += sampleOcc;
|
||||
occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
|
||||
}
|
||||
#endif
|
||||
occlusion = 1.0 - (uBias * occlusion / float(dNSamples));
|
||||
|
||||
@@ -22,6 +22,13 @@ uniform float uBackgroundOpacity;
|
||||
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
const float smoothness = 32.0;
|
||||
const float gamma = 2.2;
|
||||
|
||||
void main2(){
|
||||
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main(){
|
||||
#include fade_lod
|
||||
#include clip_pixel
|
||||
@@ -37,13 +44,20 @@ void main(){
|
||||
// retrieve signed distance
|
||||
float sdf = texture2D(tFont, vTexCoord).a + uBorderWidth;
|
||||
|
||||
if (sdf < 0.5) discard;
|
||||
// perform adaptive anti-aliasing of the edges
|
||||
float w = clamp(smoothness * (abs(dFdx(vTexCoord.x)) + abs(dFdy(vTexCoord.y))), 0.0, 0.5);
|
||||
float a = clamp(0.0, 1.0, smoothstep(0.5 - w, 0.5 + w, sdf));
|
||||
|
||||
// gamma correction for linear attenuation
|
||||
a = pow(a, 1.0 / gamma);
|
||||
|
||||
if (a < 0.5) discard;
|
||||
|
||||
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
|
||||
// add border
|
||||
float t = 0.5 + uBorderWidth;
|
||||
if (uBorderWidth > 0.0 && sdf < t) {
|
||||
material.xyz = uBorderColor;
|
||||
material.xyz = mix(uBorderColor, material.xyz, smoothstep(t - w, t, sdf));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -792,86 +792,6 @@ export function getClipControl(gl: GLRenderingContext): COMPAT_clip_control | nu
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/EXT_render_snorm/
|
||||
*/
|
||||
export interface COMPAT_render_snorm {
|
||||
}
|
||||
|
||||
export function getRenderSnorm(gl: GLRenderingContext): COMPAT_render_snorm | null {
|
||||
if (isWebGL2(gl)) {
|
||||
const ext = gl.getExtension('EXT_render_snorm');
|
||||
if (ext) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/WEBGL_render_shared_exponent/
|
||||
*/
|
||||
export interface COMPAT_render_shared_exponent {
|
||||
}
|
||||
|
||||
export function getRenderSharedExponent(gl: GLRenderingContext): COMPAT_render_shared_exponent | null {
|
||||
if (isWebGL2(gl)) {
|
||||
const ext = gl.getExtension('WEBGL_render_shared_exponent');
|
||||
if (ext) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/EXT_texture_norm16/
|
||||
*/
|
||||
export interface COMPAT_texture_norm16 {
|
||||
readonly R16: number;
|
||||
readonly RG16: number;
|
||||
readonly RGB16: number;
|
||||
readonly RGBA16: number;
|
||||
readonly R16_SNORM: number;
|
||||
readonly RG16_SNORM: number;
|
||||
readonly RGB16_SNORM: number;
|
||||
readonly RGBA16_SNORM: number;
|
||||
}
|
||||
|
||||
export function getTextureNorm16(gl: GLRenderingContext): COMPAT_texture_norm16 | null {
|
||||
const ext = gl.getExtension('EXT_texture_norm16');
|
||||
if (ext) {
|
||||
return {
|
||||
R16: ext.R16_EXT,
|
||||
RG16: ext.RG16_EXT,
|
||||
RGB16: ext.RGB16_EXT,
|
||||
RGBA16: ext.RGBA16_EXT,
|
||||
R16_SNORM: ext.R16_SNORM_EXT,
|
||||
RG16_SNORM: ext.RG16_SNORM_EXT,
|
||||
RGB16_SNORM: ext.RGB16_SNORM_EXT,
|
||||
RGBA16_SNORM: ext.RGBA16_SNORM_EXT
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://registry.khronos.org/webgl/extensions/EXT_depth_clamp/
|
||||
*/
|
||||
export interface COMPAT_depth_clamp {
|
||||
readonly DEPTH_CLAMP: number;
|
||||
}
|
||||
|
||||
export function getDepthClamp(gl: GLRenderingContext): COMPAT_depth_clamp | null {
|
||||
const ext = gl.getExtension('EXT_depth_clamp');
|
||||
if (ext) {
|
||||
return {
|
||||
DEPTH_CLAMP: ext.DEPTH_CLAMP_EXT
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getNoNonInstancedActiveAttribs(gl: GLRenderingContext): boolean {
|
||||
if (!isWebGL2(gl)) return false;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject, getDisjointTimerQuery, COMPAT_disjoint_timer_query, getNoNonInstancedActiveAttribs, COMPAT_multi_draw, getMultiDraw, getDrawInstancedBaseVertexBaseInstance, getMultiDrawInstancedBaseVertexBaseInstance, COMPAT_draw_instanced_base_vertex_base_instance, COMPAT_multi_draw_instanced_base_vertex_base_instance, getDrawBuffersIndexed, COMPAT_draw_buffers_indexed, getParallelShaderCompile, COMPAT_parallel_shader_compile, getFboRenderMipmap, COMPAT_fboRenderMipmap, COMPAT_provoking_vertex, getProvokingVertex, COMPAT_clip_cull_distance, getClipCullDistance, COMPAT_conservative_depth, getConservativeDepth, COMPAT_stencil_texturing, getStencilTexturing, COMPAT_clip_control, getClipControl, getRenderSnorm, COMPAT_render_snorm, getRenderSharedExponent, COMPAT_render_shared_exponent, getTextureNorm16, COMPAT_texture_norm16, getDepthClamp, COMPAT_depth_clamp } from './compat';
|
||||
import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject, getDisjointTimerQuery, COMPAT_disjoint_timer_query, getNoNonInstancedActiveAttribs, COMPAT_multi_draw, getMultiDraw, getDrawInstancedBaseVertexBaseInstance, getMultiDrawInstancedBaseVertexBaseInstance, COMPAT_draw_instanced_base_vertex_base_instance, COMPAT_multi_draw_instanced_base_vertex_base_instance, getDrawBuffersIndexed, COMPAT_draw_buffers_indexed, getParallelShaderCompile, COMPAT_parallel_shader_compile, getFboRenderMipmap, COMPAT_fboRenderMipmap, COMPAT_provoking_vertex, getProvokingVertex, COMPAT_clip_cull_distance, getClipCullDistance, COMPAT_conservative_depth, getConservativeDepth, COMPAT_stencil_texturing, getStencilTexturing, COMPAT_clip_control, getClipControl } from './compat';
|
||||
import { isDebugMode } from '../../mol-util/debug';
|
||||
|
||||
export type WebGLExtensions = {
|
||||
@@ -37,10 +37,6 @@ export type WebGLExtensions = {
|
||||
conservativeDepth: COMPAT_conservative_depth | null
|
||||
stencilTexturing: COMPAT_stencil_texturing | null
|
||||
clipControl: COMPAT_clip_control | null
|
||||
renderSnorm: COMPAT_render_snorm | null
|
||||
renderSharedExponent: COMPAT_render_shared_exponent | null
|
||||
textureNorm16: COMPAT_texture_norm16 | null
|
||||
depthClamp: COMPAT_depth_clamp | null
|
||||
|
||||
noNonInstancedActiveAttribs: boolean
|
||||
}
|
||||
@@ -165,22 +161,6 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
|
||||
if (isDebugMode && clipControl === null) {
|
||||
console.log('Could not find support for "clip_control"');
|
||||
}
|
||||
const renderSnorm = getRenderSnorm(gl);
|
||||
if (isDebugMode && renderSnorm === null) {
|
||||
console.log('Could not find support for "render_snorm"');
|
||||
}
|
||||
const renderSharedExponent = getRenderSharedExponent(gl);
|
||||
if (isDebugMode && renderSharedExponent === null) {
|
||||
console.log('Could not find support for "render_shared_exponent"');
|
||||
}
|
||||
const textureNorm16 = getTextureNorm16(gl);
|
||||
if (isDebugMode && textureNorm16 === null) {
|
||||
console.log('Could not find support for "texture_norm16"');
|
||||
}
|
||||
const depthClamp = getDepthClamp(gl);
|
||||
if (isDebugMode && depthClamp === null) {
|
||||
console.log('Could not find support for "depth_clamp"');
|
||||
}
|
||||
|
||||
const noNonInstancedActiveAttribs = getNoNonInstancedActiveAttribs(gl);
|
||||
|
||||
@@ -214,10 +194,6 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
|
||||
conservativeDepth,
|
||||
stencilTexturing,
|
||||
clipControl,
|
||||
renderSnorm,
|
||||
renderSharedExponent,
|
||||
textureNorm16,
|
||||
depthClamp,
|
||||
|
||||
noNonInstancedActiveAttribs,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -24,7 +24,6 @@ export type WebGLState = {
|
||||
* - `gl.SCISSOR_TEST`: scissor test that discards fragments that are outside of the scissor rectangle
|
||||
* - `gl.STENCIL_TEST`: stencil testing and updates to the stencil buffer
|
||||
* - `ext.CLIP_DISTANCE[0-7]`: clip distance 0 to 7 (with `ext` being `WEBGL_clip_cull_distance`)
|
||||
* - `ext.DEPTH_CLAMP`: depth clamping (with `ext` being `EXT_depth_clamp`)
|
||||
*/
|
||||
enable: (cap: number) => void
|
||||
/**
|
||||
@@ -39,7 +38,6 @@ export type WebGLState = {
|
||||
* - `gl.SCISSOR_TEST`: scissor test that discards fragments that are outside of the scissor rectangle
|
||||
* - `gl.STENCIL_TEST`: stencil testing and updates to the stencil buffer
|
||||
* - `ext.CLIP_DISTANCE[0-7]`: clip distance 0 to 7 (with `ext` being `WEBGL_clip_cull_distance`)
|
||||
* - `ext.DEPTH_CLAMP`: depth clamping (with `ext` being `EXT_depth_clamp`)
|
||||
*/
|
||||
disable: (cap: number) => void
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -59,14 +59,14 @@ export function getTarget(gl: GLRenderingContext, kind: TextureKind): number {
|
||||
export function getFormat(gl: GLRenderingContext, format: TextureFormat, type: TextureType): number {
|
||||
switch (format) {
|
||||
case 'alpha':
|
||||
if (isWebGL2(gl) && (type === 'float' || type === 'fp16')) return gl.RED;
|
||||
if (isWebGL2(gl) && type === 'float') return gl.RED;
|
||||
else if (isWebGL2(gl) && type === 'int') return gl.RED_INTEGER;
|
||||
else return gl.ALPHA;
|
||||
case 'rgb':
|
||||
if (isWebGL2(gl) && type === 'int') return gl.RGB_INTEGER;
|
||||
return gl.RGB;
|
||||
case 'rg':
|
||||
if (isWebGL2(gl) && (type === 'float' || type === 'fp16')) return gl.RG;
|
||||
if (isWebGL2(gl) && type === 'float') return gl.RG;
|
||||
else if (isWebGL2(gl) && type === 'int') return gl.RG_INTEGER;
|
||||
else throw new Error('texture format "rg" requires webgl2 and type "float" or int"');
|
||||
case 'rgba':
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.399, IHM 1.27, MA 1.4.6.
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.397, IHM 1.26, MA 1.4.6.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.399, IHM 1.27, MA 1.4.6.
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.397, IHM 1.26, MA 1.4.6.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.399, IHM 1.27, MA 1.4.6.
|
||||
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.397, IHM 1.26, MA 1.4.6.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
@@ -3854,7 +3854,7 @@ export const mmCIF_Schema = {
|
||||
/**
|
||||
* The name of the database containing the dataset entry.
|
||||
*/
|
||||
db_name: Aliased<'PDB' | 'PDB-Dev' | 'BMRB' | 'EMDB' | 'EMPIAR' | 'SASBDB' | 'PRIDE' | 'MODEL ARCHIVE' | 'MASSIVE' | 'BioGRID' | 'ProXL' | 'jPOSTrepo' | 'iProX' | 'AlphaFoldDB' | 'ProteomeXchange' | 'BMRbig' | 'Other'>(str),
|
||||
db_name: Aliased<'PDB' | 'PDB-Dev' | 'BMRB' | 'EMDB' | 'EMPIAR' | 'SASBDB' | 'PRIDE' | 'MODEL ARCHIVE' | 'MASSIVE' | 'BioGRID' | 'ProXL' | 'jPOSTrepo' | 'iProX' | 'AlphaFoldDB' | 'ProteomeXchange' | 'Other'>(str),
|
||||
/**
|
||||
* The accession code for the database entry.
|
||||
*/
|
||||
@@ -5216,74 +5216,6 @@ export const mmCIF_Schema = {
|
||||
*/
|
||||
metric_value: float,
|
||||
},
|
||||
/**
|
||||
* Data items in the MA_QA_METRIC_LOCAL_PAIRWISE category captures the
|
||||
* details of the local QA metrics, calculated at the pairwise residue level.
|
||||
*/
|
||||
ma_qa_metric_local_pairwise: {
|
||||
/**
|
||||
* A unique identifier for the category.
|
||||
*/
|
||||
ordinal_id: int,
|
||||
/**
|
||||
* The identifier for the structural model, for which local QA metric is provided.
|
||||
* This data item is a pointer to _ma_model_list.model_id
|
||||
* in the MA_MODEL_LIST category.
|
||||
*/
|
||||
model_id: int,
|
||||
/**
|
||||
* The identifier for the asym id of the first residue in the
|
||||
* pair, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_asym_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_asym_id_1: str,
|
||||
/**
|
||||
* The identifier for the sequence index of the first residue
|
||||
* in the pair, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_seq_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_seq_id_1: int,
|
||||
/**
|
||||
* The component identifier for the first residue in the
|
||||
* pair, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_comp_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_comp_id_1: str,
|
||||
/**
|
||||
* The identifier for the asym id of the second residue in the
|
||||
* pair, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_asym_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_asym_id_2: str,
|
||||
/**
|
||||
* The identifier for the sequence index of the second residue
|
||||
* in the pair, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_seq_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_seq_id_2: int,
|
||||
/**
|
||||
* The component identifier for the second residue in the
|
||||
* pair, for which local QA metric is provided.
|
||||
* This data item is a pointer to _atom_site.label_comp_id
|
||||
* in the ATOM_SITE category.
|
||||
*/
|
||||
label_comp_id_2: str,
|
||||
/**
|
||||
* The identifier for the QA metric.
|
||||
* This data item is a pointer to _ma_qa_metric.id in the
|
||||
* MA_QA_METRIC category.
|
||||
*/
|
||||
metric_id: int,
|
||||
/**
|
||||
* The value of the local QA metric.
|
||||
*/
|
||||
metric_value: float,
|
||||
},
|
||||
};
|
||||
|
||||
export type mmCIF_Schema = typeof mmCIF_Schema;
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { Task, RuntimeContext, chunkedSubtask } from '../../../../mol-task';
|
||||
import { Tokenizer, TokenBuilder } from '../../common/text/tokenizer';
|
||||
import { ReaderResult as Result } from '../../result';
|
||||
import { TokenColumnProvider as TokenColumn } from '../../common/text/column/token';
|
||||
import { Column } from '../../../../mol-data/db';
|
||||
import { LammpsDataFile } from '../schema';
|
||||
|
||||
const { readLine, skipWhitespace, eatValue, eatLine, markStart } = Tokenizer;
|
||||
|
||||
const reWhitespace = /\s+/;
|
||||
|
||||
function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext) {
|
||||
return {
|
||||
tokenizer,
|
||||
runtimeCtx,
|
||||
};
|
||||
}
|
||||
type State = ReturnType<typeof State>
|
||||
|
||||
async function handleAtoms(state: State, count: number, atom_style: 'full' | 'atomic' | 'bond'): Promise<LammpsDataFile['atoms']> {
|
||||
const { tokenizer } = state;
|
||||
// default atom style is atomic
|
||||
// depending on the atom style the number of columns can change
|
||||
const atomId = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const moleculeId = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const atomType = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const charge = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const x = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const y = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const z = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const columns = {
|
||||
full: [atomId, moleculeId, atomType, charge, x, y, z],
|
||||
atomic: [atomId, atomType, x, y, z],
|
||||
bond: [atomId, moleculeId, atomType, x, y, z],
|
||||
};
|
||||
const n = columns[atom_style].length;
|
||||
const { position } = tokenizer;
|
||||
readLine(tokenizer).trim();
|
||||
tokenizer.position = position;
|
||||
|
||||
const { length } = tokenizer;
|
||||
let linesAlreadyRead = 0;
|
||||
|
||||
await chunkedSubtask(state.runtimeCtx, 100000, void 0, chunkSize => {
|
||||
const linesToRead = Math.min(count - linesAlreadyRead, chunkSize);
|
||||
for (let i = 0; i < linesToRead; ++i) {
|
||||
for (let j = 0; j < n; ++j) {
|
||||
skipWhitespace(tokenizer);
|
||||
markStart(tokenizer);
|
||||
eatValue(tokenizer);
|
||||
const column = columns[atom_style][j];
|
||||
if (column) {
|
||||
TokenBuilder.addUnchecked(column, tokenizer.tokenStart, tokenizer.tokenEnd);
|
||||
}
|
||||
}
|
||||
// ignore any extra columns
|
||||
eatLine(tokenizer);
|
||||
markStart(tokenizer);
|
||||
}
|
||||
linesAlreadyRead += linesToRead;
|
||||
return linesToRead;
|
||||
}, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length }));
|
||||
|
||||
return {
|
||||
count,
|
||||
atomId: TokenColumn(atomId)(Column.Schema.int),
|
||||
moleculeId: TokenColumn(moleculeId)(Column.Schema.int),
|
||||
atomType: TokenColumn(atomType)(Column.Schema.int),
|
||||
charge: TokenColumn(charge)(Column.Schema.float),
|
||||
x: TokenColumn(x)(Column.Schema.float),
|
||||
y: TokenColumn(y)(Column.Schema.float),
|
||||
z: TokenColumn(z)(Column.Schema.float),
|
||||
};
|
||||
}
|
||||
|
||||
async function handleBonds(state: State, count: number): Promise<LammpsDataFile['bonds']> {
|
||||
const { tokenizer } = state;
|
||||
|
||||
const bondId = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const bondType = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const atomIdA = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const atomIdB = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
|
||||
const { length } = tokenizer;
|
||||
let bondsAlreadyRead = 0;
|
||||
await chunkedSubtask(state.runtimeCtx, 10, void 0, chunkSize => {
|
||||
const bondsToRead = Math.min(count - bondsAlreadyRead, chunkSize);
|
||||
for (let i = 0; i < bondsToRead; ++i) {
|
||||
for (let j = 0; j < 4; ++j) {
|
||||
skipWhitespace(tokenizer);
|
||||
markStart(tokenizer);
|
||||
eatValue(tokenizer);
|
||||
switch (j) {
|
||||
case 0: TokenBuilder.addUnchecked(bondId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case 1: TokenBuilder.addUnchecked(bondType, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case 2: TokenBuilder.addUnchecked(atomIdA, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case 3: TokenBuilder.addUnchecked(atomIdB, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bondsAlreadyRead += bondsToRead;
|
||||
return bondsToRead;
|
||||
}, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length }));
|
||||
|
||||
return {
|
||||
count,
|
||||
bondId: TokenColumn(bondId)(Column.Schema.int),
|
||||
bondType: TokenColumn(bondType)(Column.Schema.int),
|
||||
atomIdA: TokenColumn(atomIdA)(Column.Schema.int),
|
||||
atomIdB: TokenColumn(atomIdB)(Column.Schema.int),
|
||||
};
|
||||
}
|
||||
|
||||
const AtomStyles = ['full', 'atomic', 'bond'] as const;
|
||||
type AtomStyle = typeof AtomStyles[number];
|
||||
|
||||
async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<LammpsDataFile>> {
|
||||
const tokenizer = Tokenizer(data);
|
||||
const state = State(tokenizer, ctx);
|
||||
|
||||
let atoms = undefined as LammpsDataFile['atoms'] | undefined;
|
||||
let bonds = undefined as LammpsDataFile['bonds'] | undefined;
|
||||
let numAtoms = 0;
|
||||
let numBonds = 0;
|
||||
let atom_style: AtomStyle = 'full';
|
||||
// full list of atom_style
|
||||
// https://docs.lammps.org/atom_style.html
|
||||
while (tokenizer.tokenEnd < tokenizer.length) {
|
||||
const line = readLine(state.tokenizer).trim();
|
||||
if (line.includes('atoms')) {
|
||||
numAtoms = parseInt(line.split(reWhitespace)[0]);
|
||||
} else if (line.includes('bonds')) {
|
||||
numBonds = parseInt(line.split(reWhitespace)[0]);
|
||||
} else if (line.includes('Masses')) {
|
||||
// TODO: support masses
|
||||
} else if (line.includes('Atoms')) {
|
||||
// usually atom style is indicated as a comment after Atoms. e.g. Atoms # full
|
||||
const parts = line.split('#');
|
||||
if (parts.length > 1) {
|
||||
const atomStyle = parts[1].trim();
|
||||
if (AtomStyles.includes(atomStyle as AtomStyle)) {
|
||||
atom_style = atomStyle as AtomStyle;
|
||||
} else {
|
||||
console.warn(`Unknown atom style: ${atomStyle}`);
|
||||
}
|
||||
}
|
||||
atoms = await handleAtoms(state, numAtoms, atom_style);
|
||||
} else if (line.includes('Bonds')) {
|
||||
bonds = await handleBonds(state, numBonds);
|
||||
}
|
||||
}
|
||||
|
||||
if (atoms === undefined) {
|
||||
return Result.error('no atoms data');
|
||||
}
|
||||
|
||||
if (bonds === undefined) {
|
||||
bonds = {
|
||||
count: 0,
|
||||
bondId: Column.ofIntArray([]),
|
||||
bondType: Column.ofIntArray([]),
|
||||
atomIdA: Column.ofIntArray([]),
|
||||
atomIdB: Column.ofIntArray([]),
|
||||
};
|
||||
}
|
||||
|
||||
const result: LammpsDataFile = {
|
||||
atoms,
|
||||
bonds
|
||||
};
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
export function parseLammpsData(data: string) {
|
||||
return Task.create<Result<LammpsDataFile>>('Parse LammpsData', async ctx => {
|
||||
return await parseInternal(data, ctx);
|
||||
});
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column } from '../../../mol-data/db';
|
||||
|
||||
interface LammpsUnitStyle {
|
||||
mass: string;
|
||||
distance: string;
|
||||
time: string;
|
||||
energy: string;
|
||||
velocity: string;
|
||||
force: string;
|
||||
torque: string;
|
||||
temperature: string;
|
||||
pressure: string;
|
||||
viscosity: string;
|
||||
charge: string;
|
||||
dipole?: string;
|
||||
electricField?: string;
|
||||
density: string;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
export const lammpsUnitStyles: { [key: string]: LammpsUnitStyle } = {
|
||||
lj: {
|
||||
mass: 'unitless',
|
||||
distance: 'unitless',
|
||||
time: 'unitless',
|
||||
energy: 'unitless',
|
||||
velocity: 'unitless',
|
||||
force: 'unitless',
|
||||
torque: 'unitless',
|
||||
temperature: 'unitless',
|
||||
pressure: 'unitless',
|
||||
viscosity: 'unitless',
|
||||
charge: 'unitless',
|
||||
density: 'unitless',
|
||||
scale: 1.0,
|
||||
},
|
||||
real: {
|
||||
mass: 'grams/mole',
|
||||
distance: 'Angstroms',
|
||||
time: 'femtoseconds',
|
||||
energy: 'Kcal/mol',
|
||||
velocity: 'Angstroms/femtosecond',
|
||||
force: 'Kcal/mol-Angstrom',
|
||||
torque: 'Kcal/mol',
|
||||
temperature: 'Kelvin',
|
||||
pressure: 'atmospheres',
|
||||
viscosity: 'Poise',
|
||||
charge: 'multiple of electron charge',
|
||||
dipole: 'charge*Angstroms',
|
||||
electricField: 'volts/Angstrom',
|
||||
density: 'g/cm^3',
|
||||
scale: 1.0,
|
||||
},
|
||||
metal: {
|
||||
mass: 'grams/mole',
|
||||
distance: 'Angstroms',
|
||||
time: 'picoseconds',
|
||||
energy: 'eV',
|
||||
velocity: 'Angstroms/picosecond',
|
||||
force: 'eV/Angstrom',
|
||||
torque: 'eV',
|
||||
temperature: 'Kelvin',
|
||||
pressure: 'bars',
|
||||
viscosity: 'Poise',
|
||||
charge: 'multiple of electron charge',
|
||||
dipole: 'charge*Angstroms',
|
||||
electricField: 'volts/Angstrom',
|
||||
density: 'g/cm^3',
|
||||
scale: 1.0,
|
||||
},
|
||||
si: {
|
||||
mass: 'kilograms',
|
||||
distance: 'meters',
|
||||
time: 'seconds',
|
||||
energy: 'Joules',
|
||||
velocity: 'meters/second',
|
||||
force: 'Newtons',
|
||||
torque: 'Newton-meters',
|
||||
temperature: 'Kelvin',
|
||||
pressure: 'Pascals',
|
||||
viscosity: 'Pascal*second',
|
||||
charge: 'Coulombs',
|
||||
dipole: 'Coulombs*meters',
|
||||
electricField: 'volts/meter',
|
||||
density: 'kg/m^3',
|
||||
scale: 1.0, // leave as is
|
||||
},
|
||||
cgs: {
|
||||
mass: 'grams',
|
||||
distance: 'centimeters',
|
||||
time: 'seconds',
|
||||
energy: 'ergs',
|
||||
velocity: 'centimeters/second',
|
||||
force: 'dynes',
|
||||
torque: 'dyne-centimeters',
|
||||
temperature: 'Kelvin',
|
||||
pressure: 'dyne/cm^2',
|
||||
viscosity: 'Poise',
|
||||
charge: 'statcoulombs',
|
||||
dipole: 'statcoul-cm',
|
||||
electricField: 'statvolt/cm',
|
||||
density: 'g/cm^3',
|
||||
scale: 1.0, // leave as is
|
||||
},
|
||||
electron: {
|
||||
mass: 'atomic mass units',
|
||||
distance: 'Bohr',
|
||||
time: 'femtoseconds',
|
||||
energy: 'Hartrees',
|
||||
velocity: 'Bohr/atomic time units',
|
||||
force: 'Hartrees/Bohr',
|
||||
temperature: 'Kelvin',
|
||||
pressure: 'Pascals',
|
||||
charge: 'multiple of electron charge',
|
||||
dipole: 'Debye',
|
||||
electricField: 'volts/cm',
|
||||
density: 'unitless',
|
||||
torque: '',
|
||||
viscosity: '',
|
||||
scale: 0.529177,
|
||||
},
|
||||
micro: {
|
||||
mass: 'picograms',
|
||||
distance: 'micrometers',
|
||||
time: 'microseconds',
|
||||
energy: 'picogram-micrometer^2/microsecond^2',
|
||||
velocity: 'micrometers/microsecond',
|
||||
force: 'picogram-micrometer/microsecond^2',
|
||||
torque: 'picogram-micrometer^2/microsecond^2',
|
||||
temperature: 'Kelvin',
|
||||
pressure: 'picogram/(micrometer-microsecond^2)',
|
||||
viscosity: 'picogram/(micrometer-microsecond)',
|
||||
charge: 'picocoulombs',
|
||||
dipole: 'picocoulomb-micrometer',
|
||||
electricField: 'volt/micrometer',
|
||||
density: 'pg/μm^3',
|
||||
scale: 1.0, // leave as is
|
||||
},
|
||||
nano: {
|
||||
mass: 'attograms',
|
||||
distance: 'nanometers',
|
||||
time: 'nanoseconds',
|
||||
energy: 'attogram-nanometer^2/nanosecond^2',
|
||||
velocity: 'nanometers/nanosecond',
|
||||
force: 'attogram-nanometer/nanosecond^2',
|
||||
torque: 'attogram-nanometer^2/nanosecond^2',
|
||||
temperature: 'Kelvin',
|
||||
pressure: 'attogram/(nanometer-nanosecond^2)',
|
||||
viscosity: 'attogram/(nanometer-nanosecond)',
|
||||
charge: 'multiple of electron charge',
|
||||
dipole: 'charge-nanometer',
|
||||
electricField: 'volt/nanometer',
|
||||
density: 'ag/nm^3',
|
||||
scale: 10.0,
|
||||
}
|
||||
};
|
||||
|
||||
export const UnitStyles = ['real', 'metal', 'si', 'cgs', 'electron', 'micro', 'nano', 'lj'] as const;
|
||||
export type UnitStyle = typeof UnitStyles[number];
|
||||
|
||||
export interface LammpsDataFile {
|
||||
readonly atoms: {
|
||||
readonly count: number
|
||||
readonly atomId: Column<number>
|
||||
readonly moleculeId: Column<number>
|
||||
readonly atomType: Column<number>
|
||||
readonly charge: Column<number>
|
||||
readonly x: Column<number>,
|
||||
readonly y: Column<number>,
|
||||
readonly z: Column<number>,
|
||||
}
|
||||
readonly bonds: {
|
||||
readonly count: number
|
||||
readonly bondId: Column<number>
|
||||
readonly bondType: Column<number>
|
||||
readonly atomIdA: Column<number>
|
||||
readonly atomIdB: Column<number>
|
||||
}
|
||||
}
|
||||
|
||||
export interface LammpsBox {
|
||||
lower: [number, number, number],
|
||||
length: [number, number, number],
|
||||
periodicity: [string, string, string]
|
||||
}
|
||||
|
||||
export interface LammpsFrame {
|
||||
count: number,
|
||||
atomMode: string,
|
||||
atomId: Column<number>,
|
||||
moleculeId: Column<number>,
|
||||
atomType: Column<number>,
|
||||
x: Column<number>,
|
||||
y: Column<number>,
|
||||
z: Column<number>,
|
||||
}
|
||||
|
||||
export interface LammpsTrajectoryFile {
|
||||
frames: LammpsFrame[],
|
||||
times: number[],
|
||||
bounds: LammpsBox[],
|
||||
timeOffset: number,
|
||||
deltaTime: number
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { Task, RuntimeContext, chunkedSubtask } from '../../../../mol-task';
|
||||
import { Tokenizer, TokenBuilder } from '../../common/text/tokenizer';
|
||||
import { ReaderResult as Result } from '../../result';
|
||||
import { TokenColumnProvider as TokenColumn } from '../../common/text/column/token';
|
||||
import { Column } from '../../../../mol-data/db';
|
||||
import { LammpsFrame, LammpsTrajectoryFile } from '../schema';
|
||||
|
||||
const { readLine, skipWhitespace, eatValue, eatLine, markStart } = Tokenizer;
|
||||
|
||||
function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext) {
|
||||
return {
|
||||
tokenizer,
|
||||
runtimeCtx,
|
||||
};
|
||||
}
|
||||
type State = ReturnType<typeof State>
|
||||
|
||||
async function handleAtoms(state: State, count: number, parts: string[]): Promise<LammpsFrame> {
|
||||
const { tokenizer } = state;
|
||||
const columnIndexMap = Object.fromEntries(parts.map((colName, index) => [colName, index]));
|
||||
// declare column x, y, and z by check first caracter to 'x' or 'y' or 'z'
|
||||
// x,y,z = unscaled atom coordinates
|
||||
// xs,ys,zs = scaled atom coordinates this need the boundary box
|
||||
// xu,yu,zu = unwrapped atom coordinates
|
||||
// xsu,ysu,zsu = scaled unwrapped atom coordinates
|
||||
// ix,iy,iz = box image that the atom is in
|
||||
// how should we handle the different scenario ?
|
||||
const xCol = parts.findIndex(p => p[0] === 'x');
|
||||
const yCol = parts.findIndex(p => p[0] === 'y');
|
||||
const zCol = parts.findIndex(p => p[0] === 'z');
|
||||
// retrieve the atom type colum for x only
|
||||
const atomMode = parts[xCol]; // x,xs,xu,xsu
|
||||
const atomId = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const moleculeType = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const atomType = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const x = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const y = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
const z = TokenBuilder.create(tokenizer.data, count * 2);
|
||||
|
||||
const { position } = tokenizer;
|
||||
tokenizer.position = position;
|
||||
|
||||
const n = parts.length;
|
||||
|
||||
const { length } = tokenizer;
|
||||
let linesAlreadyRead = 0;
|
||||
await chunkedSubtask(state.runtimeCtx, 100000, void 0, chunkSize => {
|
||||
const linesToRead = Math.min(count - linesAlreadyRead, chunkSize);
|
||||
for (let i = 0; i < linesToRead; ++i) {
|
||||
for (let j = 0; j < n; ++j) {
|
||||
skipWhitespace(tokenizer);
|
||||
markStart(tokenizer);
|
||||
eatValue(tokenizer);
|
||||
switch (j) {
|
||||
case columnIndexMap['id']: TokenBuilder.addUnchecked(atomId, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case columnIndexMap['mol']: TokenBuilder.addUnchecked(moleculeType, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case columnIndexMap['type']: TokenBuilder.addUnchecked(atomType, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case xCol: TokenBuilder.addUnchecked(x, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case yCol: TokenBuilder.addUnchecked(y, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
case zCol: TokenBuilder.addUnchecked(z, tokenizer.tokenStart, tokenizer.tokenEnd); break;
|
||||
}
|
||||
}
|
||||
// ignore any extra columns
|
||||
eatLine(tokenizer);
|
||||
markStart(tokenizer);
|
||||
}
|
||||
linesAlreadyRead += linesToRead;
|
||||
return linesToRead;
|
||||
}, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length }));
|
||||
|
||||
return {
|
||||
count,
|
||||
atomMode: atomMode,
|
||||
atomId: TokenColumn(atomId)(Column.Schema.int),
|
||||
moleculeId: TokenColumn(moleculeType)(Column.Schema.int),
|
||||
atomType: TokenColumn(atomType)(Column.Schema.int),
|
||||
x: TokenColumn(x)(Column.Schema.float),
|
||||
y: TokenColumn(y)(Column.Schema.float),
|
||||
z: TokenColumn(z)(Column.Schema.float),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible Attributes from Lammps Dump
|
||||
* see https://docs.lammps.org/dump.html fro more details
|
||||
* possible attributes = id, mol, proc, procp1, type, element, mass,
|
||||
* x, y, z, xs, ys, zs, xu, yu, zu,
|
||||
* xsu, ysu, zsu, ix, iy, iz,
|
||||
* vx, vy, vz, fx, fy, fz,
|
||||
* q, mux, muy, muz, mu,
|
||||
* radius, diameter, omegax, omegay, omegaz,
|
||||
* angmomx, angmomy, angmomz, tqx, tqy, tqz,
|
||||
* c_ID, c_ID[I], f_ID, f_ID[I], v_name,
|
||||
* i_name, d_name, i2_name[I], d2_name[I]
|
||||
* ITEM: BOX BOUNDS xx yy zz
|
||||
* xlo xhi
|
||||
* ylo yhi
|
||||
* zlo zhi
|
||||
*/
|
||||
async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<LammpsTrajectoryFile>> {
|
||||
const tokenizer = Tokenizer(data);
|
||||
const state = State(tokenizer, ctx);
|
||||
const f: LammpsTrajectoryFile = {
|
||||
frames: [],
|
||||
times: [],
|
||||
bounds: [],
|
||||
timeOffset: 0.0,
|
||||
deltaTime: 0.0
|
||||
};
|
||||
const frames = f.frames;
|
||||
let numAtoms = 0;
|
||||
let timestep = 0;
|
||||
while (tokenizer.tokenEnd < tokenizer.length) {
|
||||
const line = readLine(state.tokenizer).trim();
|
||||
if (line.includes('ITEM: TIMESTEP')) {
|
||||
timestep = parseInt(readLine(state.tokenizer).trim());
|
||||
f.times.push(timestep);
|
||||
} else if (line.includes('ITEM: NUMBER OF ATOMS')) {
|
||||
numAtoms = parseInt(readLine(state.tokenizer).trim());
|
||||
} else if (line.includes('ITEM: ATOMS')) {
|
||||
// this line provide also the style of the output and will give the order of the columns
|
||||
const parts = line.split(' ').slice(2);
|
||||
const frame: LammpsFrame = await handleAtoms(state, numAtoms, parts);
|
||||
frames.push(frame);
|
||||
} else if (line.includes('ITEM: BOX BOUNDS')) {
|
||||
const tokens = line.split('ITEM: BOX BOUNDS ')[1].split(' ');
|
||||
// Periodicity of the box
|
||||
const px = tokens[0];
|
||||
const py = tokens[1];
|
||||
const pz = tokens[2];
|
||||
// the actual box bounds
|
||||
const xbound = readLine(state.tokenizer).trim().split(' ');
|
||||
const ybound = readLine(state.tokenizer).trim().split(' ');
|
||||
const zbound = readLine(state.tokenizer).trim().split(' ');
|
||||
const xlo = parseFloat(xbound[0]);
|
||||
const xhi = parseFloat(xbound[1]);
|
||||
const ylo = parseFloat(ybound[0]);
|
||||
const yhi = parseFloat(ybound[1]);
|
||||
const zlo = parseFloat(zbound[0]);
|
||||
const zhi = parseFloat(zbound[1]);
|
||||
f.bounds.push({
|
||||
lower: [xlo, ylo, zlo],
|
||||
length: [xhi - xlo, yhi - ylo, zhi - zlo],
|
||||
periodicity: [px, py, pz] });
|
||||
}
|
||||
}
|
||||
if (f.times.length >= 1) {
|
||||
f.timeOffset = f.times[0];
|
||||
}
|
||||
if (f.times.length >= 2) {
|
||||
f.deltaTime = f.times[1] - f.times[0];
|
||||
}
|
||||
return Result.success(f);
|
||||
}
|
||||
|
||||
export function parseLammpsTrajectory(data: string) {
|
||||
return Task.create<Result<LammpsTrajectoryFile>>('Parse Lammp Trajectory', async ctx => {
|
||||
return await parseInternal(data, ctx);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -43,7 +43,6 @@ export interface Lookup3D<T = number> {
|
||||
find(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T>,
|
||||
nearest(x: number, y: number, z: number, k: number, stopIf?: Function, result?: Result<T>): Result<T>,
|
||||
check(x: number, y: number, z: number, radius: number): boolean,
|
||||
approxNearest(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T>,
|
||||
readonly boundary: { readonly box: Box3D, readonly sphere: Sphere3D }
|
||||
/** transient result */
|
||||
readonly result: Result<T>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 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>
|
||||
@@ -14,7 +14,6 @@ import { Vec3 } from '../../linear-algebra';
|
||||
import { OrderedSet } from '../../../mol-data/int';
|
||||
import { Boundary } from '../boundary';
|
||||
import { FibonacciHeap } from '../../../mol-util/fibonacci-heap';
|
||||
import { memoize1 } from '../../../mol-util/memoize';
|
||||
|
||||
interface GridLookup3D<T = number> extends Lookup3D<T> {
|
||||
readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
|
||||
@@ -63,17 +62,6 @@ class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
|
||||
return query(this.ctx, this.result);
|
||||
}
|
||||
|
||||
approxNearest(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T> {
|
||||
this.ctx.x = x;
|
||||
this.ctx.y = y;
|
||||
this.ctx.z = z;
|
||||
this.ctx.radius = radius;
|
||||
this.ctx.isCheck = false;
|
||||
const ret = result ?? this.result;
|
||||
approxQueryNearest(this.ctx, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
constructor(data: PositionData, boundary: Boundary, cellSizeOrCount?: Vec3 | number) {
|
||||
const structure = build(data, boundary, cellSizeOrCount);
|
||||
this.ctx = createContext(structure);
|
||||
@@ -306,84 +294,6 @@ function query<T extends number = number>(ctx: QueryContext, result: Result<T>):
|
||||
return result.count > 0;
|
||||
}
|
||||
|
||||
function _insideOut(r: number) {
|
||||
const cells: Vec3[] = [];
|
||||
const n = r * 2 + 1;
|
||||
|
||||
for (let x = 0; x < n; ++x) {
|
||||
for (let y = 0; y < n; ++y) {
|
||||
for (let z = 0; z < n; ++z) {
|
||||
cells.push(Vec3.create(x - r, y - r, z - r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cells.sort((a, b) => Vec3.squaredMagnitude(a) - Vec3.squaredMagnitude(b));
|
||||
return cells.flat();
|
||||
}
|
||||
const insideOut = memoize1(_insideOut);
|
||||
|
||||
/**
|
||||
* The maximum error is on the order of cell size + max radius (if the grid has radii).
|
||||
*/
|
||||
function approxQueryNearest<T extends number = number>(ctx: QueryContext, result: Result<T>): boolean {
|
||||
const { min, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices }, delta } = ctx.grid;
|
||||
const { radius, x, y, z } = ctx;
|
||||
|
||||
const rSq = radius * radius;
|
||||
|
||||
Result.reset(result);
|
||||
|
||||
const loX = Math.max(0, Math.floor((x - radius - min[0]) / delta[0]));
|
||||
const loY = Math.max(0, Math.floor((y - radius - min[1]) / delta[1]));
|
||||
const loZ = Math.max(0, Math.floor((z - radius - min[2]) / delta[2]));
|
||||
|
||||
const hiX = Math.min(sX - 1, Math.floor((x + radius - min[0]) / delta[0]));
|
||||
const hiY = Math.min(sY - 1, Math.floor((y + radius - min[1]) / delta[1]));
|
||||
const hiZ = Math.min(sZ - 1, Math.floor((z + radius - min[2]) / delta[2]));
|
||||
|
||||
if (loX > hiX || loY > hiY || loZ > hiZ) return false;
|
||||
|
||||
const miX = Math.floor((x - min[0]) / delta[0]);
|
||||
const miY = Math.floor((y - min[1]) / delta[1]);
|
||||
const miZ = Math.floor((z - min[2]) / delta[2]);
|
||||
|
||||
const cells = insideOut(Math.max(hiX - loX, hiY - loY, hiZ - loZ) + 1);
|
||||
|
||||
for (let i = 0, _i = cells.length; i < _i; i += 3) {
|
||||
const ix = miX + cells[i];
|
||||
const iy = miY + cells[i + 1];
|
||||
const iz = miZ + cells[i + 2];
|
||||
if (ix < loX || ix > hiX || iy < loY || iy > hiY || iz < loZ || iz > hiZ) continue;
|
||||
|
||||
const bucketIdx = grid[(((ix * sY) + iy) * sZ) + iz];
|
||||
if (bucketIdx === 0) continue;
|
||||
|
||||
const k = bucketIdx - 1;
|
||||
const offset = bucketOffset[k];
|
||||
const count = bucketCounts[k];
|
||||
const end = offset + count;
|
||||
|
||||
let minDistSq = Number.MAX_VALUE;
|
||||
for (let i = offset; i < end; i++) {
|
||||
const idx = OrderedSet.getAt(indices, bucketArray[i]);
|
||||
|
||||
const dx = px[idx] - x;
|
||||
const dy = py[idx] - y;
|
||||
const dz = pz[idx] - z;
|
||||
const distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
if (distSq <= rSq && distSq < minDistSq) {
|
||||
Result.add(result, bucketArray[i], distSq);
|
||||
minDistSq = distSq;
|
||||
}
|
||||
}
|
||||
if (minDistSq !== Number.MAX_VALUE) return true;
|
||||
}
|
||||
|
||||
return result.count > 0;
|
||||
}
|
||||
|
||||
const tmpDirVec = Vec3();
|
||||
const tmpVec = Vec3();
|
||||
const tmpSetG = new Set<number>();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { UniqueArray } from '../../mol-data/generic';
|
||||
import { cantorPairing } from '../../mol-data/util';
|
||||
|
||||
export { InterUnitGraph };
|
||||
|
||||
@@ -15,8 +14,8 @@ class InterUnitGraph<UnitId extends number, VertexIndex extends number, EdgeProp
|
||||
readonly edgeCount: number;
|
||||
/** Array of inter-unit edges */
|
||||
readonly edges: ReadonlyArray<InterUnitGraph.Edge<UnitId, VertexIndex, EdgeProps>>;
|
||||
private readonly edgeKeyIndex: Map<number, Map<number, number>>;
|
||||
private readonly vertexKeyIndex: Map<number, number[]>;
|
||||
private readonly edgeKeyIndex: Map<string, number>;
|
||||
private readonly vertexKeyIndex: Map<string, number[]>;
|
||||
|
||||
/** Get an array of unit-pair-edges that are connected to the given unit */
|
||||
getConnectedUnits(unit: UnitId): ReadonlyArray<InterUnitGraph.UnitPairEdges<UnitId, VertexIndex, EdgeProps>> {
|
||||
@@ -26,10 +25,8 @@ class InterUnitGraph<UnitId extends number, VertexIndex extends number, EdgeProp
|
||||
|
||||
/** Index into this.edges */
|
||||
getEdgeIndex(indexA: VertexIndex, unitA: UnitId, indexB: VertexIndex, unitB: UnitId): number {
|
||||
const indices = this.edgeKeyIndex.get(InterUnitGraph.getEdgeUnitKey(unitA, unitB));
|
||||
if (indices === undefined) return -1;
|
||||
|
||||
const index = indices.get(InterUnitGraph.getEdgeIndexKey(indexA, indexB));
|
||||
const edgeKey = InterUnitGraph.getEdgeKey<UnitId, VertexIndex>(indexA, unitA, indexB, unitB);
|
||||
const index = this.edgeKeyIndex.get(edgeKey);
|
||||
return index !== undefined ? index : -1;
|
||||
}
|
||||
|
||||
@@ -52,8 +49,8 @@ class InterUnitGraph<UnitId extends number, VertexIndex extends number, EdgeProp
|
||||
constructor(protected readonly map: Map<number, InterUnitGraph.UnitPairEdges<UnitId, VertexIndex, EdgeProps>[]>) {
|
||||
let count = 0;
|
||||
const edges: (InterUnitGraph.Edge<UnitId, VertexIndex, EdgeProps>)[] = [];
|
||||
const edgeKeyIndex = new Map<number, Map<number, number>>();
|
||||
const vertexKeyIndex = new Map<number, number[]>();
|
||||
const edgeKeyIndex = new Map<string, number>();
|
||||
const vertexKeyIndex = new Map<string, number[]>();
|
||||
|
||||
this.map.forEach(pairEdgesArray => {
|
||||
pairEdgesArray.forEach(pairEdges => {
|
||||
@@ -62,16 +59,13 @@ class InterUnitGraph<UnitId extends number, VertexIndex extends number, EdgeProp
|
||||
pairEdges.getEdges(indexA).forEach(edgeInfo => {
|
||||
const { unitA, unitB } = pairEdges;
|
||||
|
||||
const edgeUnitKey = InterUnitGraph.getEdgeIndexKey(unitA, unitB);
|
||||
const edgeIndexKey = InterUnitGraph.getEdgeIndexKey(indexA, edgeInfo.indexB);
|
||||
const e = edgeKeyIndex.get(edgeUnitKey);
|
||||
if (e === undefined) edgeKeyIndex.set(edgeUnitKey, new Map([[edgeIndexKey, edges.length]]));
|
||||
else e.set(edgeIndexKey, edges.length);
|
||||
const edgeKey = InterUnitGraph.getEdgeKey(indexA, unitA, edgeInfo.indexB, unitB);
|
||||
edgeKeyIndex.set(edgeKey, edges.length);
|
||||
|
||||
const vertexKey = InterUnitGraph.getVertexKey(indexA, unitA);
|
||||
const v = vertexKeyIndex.get(vertexKey);
|
||||
if (v === undefined) vertexKeyIndex.set(vertexKey, [edges.length]);
|
||||
else v.push(edges.length);
|
||||
const e = vertexKeyIndex.get(vertexKey);
|
||||
if (e === undefined) vertexKeyIndex.set(vertexKey, [edges.length]);
|
||||
else e.push(edges.length);
|
||||
|
||||
edges.push({ ...edgeInfo, indexA, unitA, unitB });
|
||||
});
|
||||
@@ -123,16 +117,12 @@ namespace InterUnitGraph {
|
||||
readonly props: EdgeProps
|
||||
}
|
||||
|
||||
export function getEdgeUnitKey<UnitId extends number>(unitA: UnitId, unitB: UnitId) {
|
||||
return cantorPairing(unitA, unitB);
|
||||
}
|
||||
|
||||
export function getEdgeIndexKey<VertexIndex extends number>(indexA: VertexIndex, indexB: VertexIndex) {
|
||||
return cantorPairing(indexA, indexB);
|
||||
export function getEdgeKey<UnitId extends number, VertexIndex extends number>(indexA: VertexIndex, unitA: UnitId, indexB: VertexIndex, unitB: UnitId) {
|
||||
return `${indexA}|${unitA}|${indexB}|${unitB}`;
|
||||
}
|
||||
|
||||
export function getVertexKey<UnitId extends number, VertexIndex extends number>(index: VertexIndex, unit: UnitId) {
|
||||
return cantorPairing(index, unit);
|
||||
return `${index}|${unit}`;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -43,8 +43,7 @@ export function arcLength(angle: number, radius: number) {
|
||||
export function spiral2d(radius: number) {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let deltaX = 0;
|
||||
let deltaY = -1;
|
||||
const delta = [0, -1];
|
||||
const size = radius * 2 + 1;
|
||||
const halfSize = size / 2;
|
||||
const out: [number, number][] = [];
|
||||
@@ -55,15 +54,11 @@ export function spiral2d(radius: number) {
|
||||
}
|
||||
|
||||
if (x === y || (x < 0 && x === -y) || (x > 0 && x === 1 - y)) {
|
||||
// change direction
|
||||
const prevDeltaX = deltaX;
|
||||
const prevDeltaY = deltaY;
|
||||
deltaX = -prevDeltaY;
|
||||
deltaY = prevDeltaX;
|
||||
[delta[0], delta[1]] = [-delta[1], delta[0]]; // change direction
|
||||
}
|
||||
|
||||
x += deltaX;
|
||||
y += deltaY;
|
||||
x += delta[0];
|
||||
y += delta[1];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-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>
|
||||
@@ -40,6 +40,5 @@ export function getNormalizedAtomSite(atom_site: AtomSite) {
|
||||
substUndefinedColumn(normalized, 'label_comp_id', 'auth_comp_id');
|
||||
substUndefinedColumn(normalized, 'label_seq_id', 'auth_seq_id');
|
||||
substUndefinedColumn(normalized, 'label_asym_id', 'auth_asym_id');
|
||||
substUndefinedColumn(normalized, 'label_entity_id', 'label_asym_id');
|
||||
return normalized;
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Table } from '../../mol-data/db';
|
||||
import { Model } from '../../mol-model/structure/model';
|
||||
import { LammpsDataFile, lammpsUnitStyles, UnitStyle } from '../../mol-io/reader/lammps/schema';
|
||||
import { Trajectory, ArrayTrajectory } from '../../mol-model/structure';
|
||||
import { BondType, MoleculeType } from '../../mol-model/structure/model/types';
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { ModelFormat } from '../format';
|
||||
import { createModels } from './basic/parser';
|
||||
import { BasicSchema, createBasic } from './basic/schema';
|
||||
import { ComponentBuilder } from './common/component';
|
||||
import { EntityBuilder } from './common/entity';
|
||||
import { IndexPairBonds } from './property/bonds/index-pair';
|
||||
import { AtomPartialCharge } from './property/partial-charge';
|
||||
|
||||
async function getModels(mol: LammpsDataFile, ctx: RuntimeContext, unitsStyle: UnitStyle = 'real') {
|
||||
const { atoms, bonds } = mol;
|
||||
const models: Model[] = [];
|
||||
const count = atoms.count;
|
||||
const scale = lammpsUnitStyles[unitsStyle].scale;
|
||||
const type_symbols = new Array<string>(count);
|
||||
const id = new Int32Array(count);
|
||||
const cx = new Float32Array(count);
|
||||
const cy = new Float32Array(count);
|
||||
const cz = new Float32Array(count);
|
||||
const model_num = new Int32Array(count);
|
||||
|
||||
let offset = 0;
|
||||
for (let j = 0; j < count; j++) {
|
||||
type_symbols[offset] = atoms.atomType.value(j).toString();
|
||||
cx[offset] = atoms.x.value(j) * scale;
|
||||
cy[offset] = atoms.y.value(j) * scale;
|
||||
cz[offset] = atoms.z.value(j) * scale;
|
||||
id[offset] = atoms.atomId.value(j) - 1;
|
||||
model_num[offset] = 0;
|
||||
offset++;
|
||||
}
|
||||
|
||||
const MOL = Column.ofConst('MOL', count, Column.Schema.str);
|
||||
const asym_id = Column.ofLambda({
|
||||
value: (row: number) => atoms.moleculeId.value(row).toString(),
|
||||
rowCount: count,
|
||||
schema: Column.Schema.str,
|
||||
});
|
||||
const seq_id = Column.ofConst(1, count, Column.Schema.int);
|
||||
|
||||
const type_symbol = Column.ofStringArray(type_symbols);
|
||||
|
||||
const atom_site = Table.ofPartialColumns(BasicSchema.atom_site, {
|
||||
auth_asym_id: asym_id,
|
||||
auth_atom_id: type_symbol,
|
||||
auth_comp_id: MOL,
|
||||
auth_seq_id: seq_id,
|
||||
Cartn_x: Column.ofFloatArray(cx),
|
||||
Cartn_y: Column.ofFloatArray(cy),
|
||||
Cartn_z: Column.ofFloatArray(cz),
|
||||
id: Column.ofIntArray(id),
|
||||
|
||||
label_asym_id: asym_id,
|
||||
label_atom_id: type_symbol,
|
||||
label_comp_id: MOL,
|
||||
label_seq_id: seq_id,
|
||||
label_entity_id: Column.ofConst('1', count, Column.Schema.str),
|
||||
|
||||
occupancy: Column.ofConst(1, count, Column.Schema.float),
|
||||
type_symbol,
|
||||
|
||||
pdbx_PDB_model_num: Column.ofIntArray(model_num),
|
||||
}, count);
|
||||
|
||||
const entityBuilder = new EntityBuilder();
|
||||
entityBuilder.setNames([['MOL', 'Unknown Entity']]);
|
||||
entityBuilder.getEntityId('MOL', MoleculeType.Unknown, 'A');
|
||||
|
||||
const componentBuilder = new ComponentBuilder(seq_id, type_symbol);
|
||||
componentBuilder.setNames([['MOL', 'Unknown Molecule']]);
|
||||
componentBuilder.add('MOL', 0);
|
||||
|
||||
const basic = createBasic({
|
||||
entity: entityBuilder.getEntityTable(),
|
||||
chem_comp: componentBuilder.getChemCompTable(),
|
||||
atom_site
|
||||
});
|
||||
const _models = await createModels(basic, LammpsDataFormat.create(mol), ctx);
|
||||
if (_models.frameCount > 0) {
|
||||
const first = _models.representative;
|
||||
if (bonds.count !== 0) {
|
||||
const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdA, x => x - 1, Int32Array));
|
||||
const indexB = Column.ofIntArray(Column.mapToArray(bonds.atomIdB, x => x - 1, Int32Array));
|
||||
const key = bonds.bondId;
|
||||
const order = Column.ofConst(1, bonds.count, Column.Schema.int);
|
||||
const flag = Column.ofConst(BondType.Flag.Covalent, bonds.count, Column.Schema.int);
|
||||
const pairBonds = IndexPairBonds.fromData(
|
||||
{ pairs: { key, indexA, indexB, order, flag }, count: atoms.count },
|
||||
{ maxDistance: Infinity }
|
||||
);
|
||||
IndexPairBonds.Provider.set(first, pairBonds);
|
||||
}
|
||||
|
||||
AtomPartialCharge.Provider.set(first, {
|
||||
data: atoms.charge,
|
||||
type: 'NO_CHARGES'
|
||||
});
|
||||
|
||||
models.push(first);
|
||||
}
|
||||
return new ArrayTrajectory(models);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export { LammpsDataFormat };
|
||||
|
||||
type LammpsDataFormat = ModelFormat<LammpsDataFile>
|
||||
|
||||
namespace LammpsDataFormat {
|
||||
export function is(x?: ModelFormat): x is LammpsDataFormat {
|
||||
return x?.kind === 'data';
|
||||
}
|
||||
|
||||
export function create(mol: LammpsDataFile): LammpsDataFormat {
|
||||
return { kind: 'data', name: 'data', data: mol };
|
||||
}
|
||||
}
|
||||
|
||||
export function trajectoryFromLammpsData(mol: LammpsDataFile, unitsStyle?: UnitStyle): Task<Trajectory> {
|
||||
if (unitsStyle === void 0) unitsStyle = 'real';
|
||||
return Task.create('Parse Lammps Data', ctx => getModels(mol, ctx, unitsStyle));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user