Compare commits

...

104 Commits

Author SHA1 Message Date
Alexander Rose
50c1b667c5 3.34.0 2023-04-16 12:19:38 -07:00
Alexander Rose
360031d37c Merge branch 'master' of https://github.com/molstar/molstar 2023-04-16 12:09:47 -07:00
Alexander Rose
9ec873e0db changelog 2023-04-16 11:58:31 -07:00
Alexander Rose
c830a720b0 package updates 2023-04-16 11:56:32 -07:00
Alexander Rose
1aa7d1e0f7 Merge pull request #782 from molstar/eachLocation
add eachLocation to representation/visual interface
2023-04-16 11:53:44 -07:00
Alexander Rose
c5c8de8628 Merge branch 'master' of https://github.com/molstar/molstar into eachLocation 2023-04-16 11:44:45 -07:00
Alexander Rose
74c6d6f5a1 changelog 2023-04-16 11:44:03 -07:00
Russell Parker
2bff0faff7 Address Node incompatibility in mol-util/file-info (#787)
* Alter getFileInfo to avoid `instanceof WebAPIClass` and simple renames

* Remove note

* Fix potentially bad `window` reference
2023-04-13 23:41:21 +02:00
dsehnal
4df028aa77 readAllLinesAsync fix 2023-04-13 20:33:05 +02:00
dsehnal
47c2d153aa tweak readLinesAsync 2023-04-13 20:25:20 +02:00
Alexander Rose
18be09e9d5 fix .getAllLoci for representations with structure.child 2023-04-12 23:28:16 -07:00
Alexander Rose
55e940e88c fix rendering with very small viewport and SSAO 2023-04-12 23:25:57 -07:00
Alexander Rose
e246f4e5ca add eachLocation to representation/visual interface 2023-04-08 10:17:00 -07:00
Alexander Rose
5e1bb4b106 Merge pull request #778 from MadCatX/ntc-tube-missing-atoms
Fix rendering of NtC tube when some of the required atoms are missing
2023-04-08 09:55:51 -07:00
Alexander Rose
0b2889bb99 Merge branch 'master' into ntc-tube-missing-atoms 2023-04-08 09:55:44 -07:00
Alexander Rose
2994caf411 Merge pull request #779 from MadCatX/restore-vertex-array-per-program
Fix broken rendering caused by changes in 291d7abb78
2023-04-08 09:53:55 -07:00
Alexander Rose
e157993a0f Merge branch 'master' of https://github.com/molstar/molstar into pr/MadCatX/779 2023-04-08 09:42:53 -07:00
Alexander Rose
6c7c9afc34 fix spec 2023-04-08 09:40:10 -07:00
Alexander Rose
2d0b17d93c improve trackball keyState handling with modifiers 2023-04-08 09:29:57 -07:00
midlik
033c613c89 Added "Zoom All", "Orient Axes", "Reset Axes" buttons (#776)
* Added "Zoom All", "Orient Axes", "Reset Axes" buttons

* Addressed PR776 feedback
2023-04-08 10:06:39 +02:00
Michal Malý
1985eb59dd Do not reuse vertex arrays among programs 2023-04-06 15:00:54 +02:00
Michal Malý
1cf6cbf8a3 changelog 2023-04-06 12:32:21 +02:00
Michal Malý
0b42379c34 Do not draw a NtC tube segment unless we have all required atoms 2023-04-06 12:20:13 +02:00
Alexander Rose
414c349974 changelog 2023-04-03 21:56:59 -07:00
Alexander Rose
cf6d5f7194 Merge pull request #774 from giagitom/markingDepth-avoid-alpha-0-rendering
Marking depth avoid alpha 0 rendering
2023-04-03 21:54:18 -07:00
Alexander Rose
949f5207b4 add ModifiersKeys.areNone 2023-04-03 21:46:28 -07:00
Alexander Rose
a1da374b32 add ModifiersKeys.areNone 2023-04-03 21:44:11 -07:00
giagitom
5460322d4a Update changelog 2023-04-03 12:50:27 +02:00
giagitom
8b2da0b787 avoid rendering of alpha 0 renderables on renderMarkingDepth 2023-04-03 12:46:11 +02:00
Alexander Rose
3eaf4dacaf 3.33.0 2023-04-02 12:48:10 -07:00
Alexander Rose
d66d9b4dd7 changelog 2023-04-02 12:46:11 -07:00
Alexander Rose
cc52279e01 package updates 2023-04-02 12:45:42 -07:00
Alexander Rose
0def474f6d Merge pull request #773 from molstar/model-conf-fields
include occupancy & B_iso_or_equiv in model conformation
2023-04-02 12:31:34 -07:00
Alexander Rose
e0ea9a2855 Merge branch 'master' into model-conf-fields 2023-04-02 12:31:21 -07:00
midlik
2bc381fe05 Forsake lazy imports (#772)
* Removed LazyImports (gl, pngjs, jpeg-js required as param of HeadlessPluginConstructor)

* Added a few methods to HeadlessPluginContext for rendering image without saving to file

* Updated CHANGELOG

* Lint

* Rolled back removing @types/jpeg-js from deps
2023-04-02 18:47:22 +02:00
Alexander Rose
fb3cd3bf52 include occupancy & B_iso_or_equiv in model conformation 2023-04-01 19:49:24 -07:00
Alexander Rose
c4414c7cc4 Merge pull request #581 from molstar/mmcif/parse-all-blocks
add TrajectoryFromMmCif loadAllBlocks param
2023-04-01 16:25:56 -07:00
Alexander Rose
e2f2ceb7a9 Merge branch 'master' into mmcif/parse-all-blocks 2023-04-01 16:25:45 -07:00
Alexander Rose
641e7efb11 improve camera when toggeling pointer-lock/fly-mode 2023-04-01 16:13:31 -07:00
Alexander Rose
11f2ef50ef add Frustum3D and Plane3D math primitives 2023-04-01 11:43:52 -07:00
Alexander Rose
869ecfaf71 improve typing of toArray methods 2023-04-01 11:33:01 -07:00
Alexander Rose
cb8731815c changelog & param tweaks 2023-04-01 11:02:06 -07:00
Alexander Rose
a9177ad362 Merge branch 'master' of https://github.com/molstar/molstar into mmcif/parse-all-blocks 2023-04-01 10:44:49 -07:00
Alexander Rose
ad116df73b fix camera project/unproject
- was wrong when using offset viewport
2023-03-31 23:37:52 -07:00
Alexander Rose
f30b3a410c init camera for fly mode
- like for pointer-lock
2023-03-30 22:32:58 -07:00
Alexander Rose
c440ba2d4b gl tweaks
- add more docs
- clean schema types
2023-03-30 22:16:11 -07:00
Alexander Rose
a3267dafdb Merge pull request #762 from molstar/multi-scale-ssao
add multi-scale ssao
2023-03-30 22:13:43 -07:00
dsehnal
7a1e83733c throttle canvas resize events 2023-03-30 15:00:24 +02:00
Russell Parker
7cb96ce983 Handle resizing viewer element when window remains the same size (#763)
* Handle resizing viewer element when window remains the same size

* Fix bad rebase

* Fall back to window resize event listener when ResizeObserver not defined

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2023-03-30 14:51:20 +02:00
jpattle
a73633d0c3 Selection and focus improvements (#742)
* Updated measurements so that the toggle selection button is not shown if selection mode is turned off

* Updated selection controls so that they cannot be turned off if selection mode is not shown

* Extended camera focus bindings to allow override of behaviour to reset camera focus on click

* Exported default bindings for selection and focus so that they can be more easily selectively overridden

* Added description to changelog and headers to modified files

* Fixed spacing in text for toggling selection mode

* Updated camera bindings to not be a breaking change by setting the new bindings to be optional and using default values when undefined

* resolved linting issues with camera bindings

* updated superposition UI to hide selection toggle button when selection mode is disabled

* updated the default value for click to reset camera bindings
2023-03-30 14:49:35 +02:00
Alexander Rose
b2f8e8dd4e fix spec 2023-03-30 00:07:25 -07:00
Alexander Rose
291d7abb78 gl improvements
- remove unneccesary return values
- reuse vertex array among programs
- typo fixes
- cleanup material-textures
2023-03-29 23:50:30 -07:00
Alexander Rose
32873d787b fix theme smoothing not updated 2023-03-29 23:22:55 -07:00
Alexander Rose
e243d71abf cleanup level, light, clip assignments 2023-03-29 23:19:27 -07:00
Alexander Rose
2689d3f21a more input/controls fixes & tweaks
- no identify when pointer-lock & controls movement
- limit controls key bindings to viewport
- take controls minDistance into account for movement
2023-03-28 23:22:55 -07:00
dsehnal
c1bc008114 Fix JSX reference 2023-03-28 10:04:36 +02:00
Alexander Rose
254578460a input/controls fixes & tweaks
- assign trackball bindings in setProps
- remove cross element in input-observer dispose
- improve key event target handling
- add center dot to pointer-lock crosshair
2023-03-27 23:09:57 -07:00
Alexander Rose
f5467dd3b9 allow intra-residue contacts in single-residue models 2023-03-27 22:39:32 -07:00
Alexander Rose
9eb8714e11 add multi-scale ssao 2023-03-26 00:14:49 -07:00
Alexander Rose
847678ea56 improve canvas3d consoleStats 2023-03-26 00:14:05 -07:00
Alexander Rose
f08729a402 apply bumpiness as lightness with ignoreLight 2023-03-25 23:57:18 -07:00
Alexander Rose
a7c91257a7 Merge pull request #752 from molstar/input-controls
input/controls improvements
2023-03-25 23:52:10 -07:00
Alexander Rose
835369a91e change dragRotateZ binding to drag left+shift+ctrl 2023-03-25 10:50:38 -07:00
Alexander Rose
62554b522f add key bindings for fly mode & reset view 2023-03-25 10:32:02 -07:00
Alexander Rose
fd041cd4c3 change dragRotateZ binding to left+alt 2023-03-25 10:31:05 -07:00
Alexander Rose
cfbb68c8ef improve contrast of pointer-lock cross 2023-03-25 10:30:05 -07:00
dsehnal
d7acec4f7d tweak moveCamera 2023-03-25 11:58:06 +01:00
dsehnal
7da46bca8b scale move speed by frametime 2023-03-24 19:52:11 +01:00
Alexander Rose
c480579ca8 add web3dsurvey analytics code (only molstar.org) 2023-03-22 20:13:01 -07:00
Alexander Rose
00ff1a1eae Merge branch 'master' of https://github.com/molstar/molstar into input-controls 2023-03-21 20:19:33 -07:00
dsehnal
ae795f8ad3 3.32.0 2023-03-20 09:29:41 +01:00
dsehnal
9d3c071689 changelog 2023-03-20 09:26:48 +01:00
David Sehnal
01cb23f566 add setFSModule (#755) 2023-03-20 09:24:45 +01:00
Alexander Rose
fe8a9799ab add exposure parameter (#751)
* add exposure parameter

* add missing uniform

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2023-03-20 09:16:51 +01:00
David Sehnal
4f18154681 Marking improvements (#750)
* better marking identify

* changelog

* tweak

* type tweak

* simplify ui mouse move handling

---------

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>
2023-03-20 09:15:51 +01:00
Alexander Rose
2114c4a3ad type fixes 2023-03-19 15:12:42 -07:00
Alexander Rose
2ca41b2b51 package updates 2023-03-19 15:11:58 -07:00
Alexander Rose
6605a2019e Merge pull request #753 from giagitom/dpoit-avoid-alpha-0-rendering
Dpoit avoid alpha 0 rendering
2023-03-19 14:10:43 -07:00
giagitom
8b1ed5f183 Including wboit 2023-03-19 21:05:25 +01:00
giagitom
f11a1b788f Updated changelog 2023-03-19 19:54:30 +01:00
giagitom
7928e24c54 Avoid rendering of fully transparent renderables 2023-03-19 19:51:01 +01:00
Alexander Rose
5dbca41da6 fix blurry occlusion in screenshots 2023-03-18 19:01:33 -07:00
Alexander Rose
f3fa54addf input/controls improvements 2023-03-18 15:34:46 -07:00
Alexander Rose
e636397f90 ensure marking edges are at least one pixel wide 2023-03-15 20:56:08 -07:00
Russell Parker
6d76bf120d Change nodejs-shim conditional to avoid checking document (#740) 2023-03-08 17:43:38 +01:00
Alexander Rose
a50e81551f use ssao-scale for gl viewport/scissor 2023-03-06 22:48:31 -08:00
Alexander Rose
86512bcea1 tweak ssao-blur thresholds 2023-02-26 19:16:37 -08:00
Alexander Rose
975f45eb01 package updates 2023-02-25 15:02:21 -08:00
Alexander Rose
f2399d3179 Merge pull request #737 from molstar/pp-improvements
Post-processing improvements
2023-02-25 14:45:48 -08:00
Alexander Rose
b26d62a067 webgl1 compat 2023-02-25 14:09:13 -08:00
Alexander Rose
926d6cbd46 reduce over-blurring occlusion at larger view distances 2023-02-25 13:52:18 -08:00
Alexander Rose
7ea47d2a99 use pixel-size for max depth difference 2023-02-25 13:31:44 -08:00
Alexander Rose
89ad8cfc15 fix orthographic camera defines not updated 2023-02-25 13:17:03 -08:00
Alexander Rose
302a309aff add occlussion color 2023-02-25 13:03:12 -08:00
dsehnal
c3e62bc2e5 3.31.4 2023-02-24 13:13:06 +01:00
dsehnal
c2ab322bd2 Stop animation loop on dispose 2023-02-24 13:10:35 +01:00
jump2cn
aeab0f235c allow link cylinder/line dashCount set to '0' (#735) 2023-02-23 10:52:56 +01:00
dsehnal
ae2285599f 3.31.3 2023-02-22 20:44:32 +01:00
dsehnal
104ab757d2 Update fs import in data-source.ts 2023-02-22 20:37:34 +01:00
Alexander Rose
de84a8c8c5 tweak minNear param max 2023-02-18 11:42:45 -08:00
Alexander Rose
4fa135daf0 fix near clipping avoidance in impostor shaders 2023-02-18 11:33:36 -08:00
midlik
9870cb4082 Fixed degenerate case (1-point) in PCA (#725)
* Fixed degenerate case (1-point) in PCA - now correctly returns identity matrix

* Changelog
2023-02-17 20:08:21 +01:00
Alexander Rose
b2924761ab update impostor bond visuals on sizeFactor changes 2023-02-12 22:06:18 -08:00
dsehnal
509e6bc2d8 add TrajectoryFromMmCif.loadAllBlocks param 2022-10-09 14:36:33 +02:00
114 changed files with 4657 additions and 2629 deletions

View File

@@ -6,6 +6,66 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [v3.34.0] - 2023-04-16
- Avoid `renderMarkingDepth` for fully transparent renderables
- Remove `camera.far` doubling workaround
- Add `ModifiersKeys.areNone` helper function
- Do not render NtC tube segments unless all required atoms are present in the structure
- Fix rendering issues caused by VAO reuse
- Add "Zoom All", "Orient Axes", "Reset Axes" buttons to the "Reset Camera" button
- Improve trackball move-state handling when key bindings use modifiers
- Fix rendering with very small viewport and SSAO enabled
- Fix `.getAllLoci` for structure representations with `structure.child`
- Fix `readAllLinesAsync` refering to dom length property
- Make mol-util/file-info node compatible
- Add `eachLocation` to representation/visual interface
## [v3.33.0] - 2023-04-02
- Handle resizes of viewer element even when window remains the same size
- Throttle canvas resize events
- Selection toggle buttons hidden if selection mode is off
- Camera focus loci bindings allow reset on click-away to be overridden
- Input/controls improvements
- Move or fly around the scene using keys
- Pointer lock to look around scene
- Toggle spin/rock animation using keys
- Apply bumpiness as lightness variation with `ignoreLight`
- Remove `JSX` reference from `loci-labels.ts`
- Fix overpaint/transparency/substance smoothing not updated when geometry changes
- Fix camera project/unproject when using offset viewport
- Add support for loading all blocks from a mmcif file as a trajectory
- Add `Frustum3D` and `Plane3D` math primitives
- Include `occupancy` and `B_iso_or_equiv` when creating `Conformation` from `Model`
- Remove LazyImports (introduced in v3.31.1)
## [v3.32.0] - 2023-03-20
- Avoid rendering of fully transparent renderables
- Add occlusion color parameter
- Fix issue with outlines and orthographic camera
- Reduce over-blurring occlusion at larger view distances
- Fix occlusion artefact with non-canvas viewport and pixel-ratio > 1
- Update nodejs-shims conditionals to handle polyfilled document object in NodeJS environment.
- Ensure marking edges are at least one pixel wide
- Add exposure parameter to renderer
- Only trigger marking when mouse is directly over canvas
- Fix blurry occlusion in screenshots
- [Breaking] Add `setFSModule` to `mol-util/data-source` instead of trying to trick WebPack
## [v3.31.4] - 2023-02-24
- Allow link cylinder/line `dashCount` set to '0'
- Stop animation loop when disposing `PluginContext` (thanks @gfrn for identifying the issue)
## [v3.31.3] - 2023-02-22
- Fix impostor bond visuals not correctly updating on `sizeFactor` changes
- Fix degenerate case in PCA
- Fix near clipping avoidance in impostor shaders
- Update `fs` import in `data-source.ts`
## [v3.31.2] - 2023-02-12
- Fix exit code of volume pack executable (pack.ts). Now exits with non-0 status when an error happens

3973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "3.31.2",
"version": "3.34.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -95,51 +95,53 @@
"Gianluca Tomasello <giagitom@gmail.com>",
"Ke Ma <mark.ma@rcsb.org>",
"Jason Pattle <jpattle@exscientia.co.uk>",
"David Williams <dwilliams@nobiastx.com>"
"David Williams <dwilliams@nobiastx.com>",
"Zhenyu Zhang <jump2cn@gmail.com>",
"Russell Parker <russell@benchling.com>"
],
"license": "MIT",
"devDependencies": {
"@graphql-codegen/add": "^4.0.0",
"@graphql-codegen/cli": "^3.0.0",
"@graphql-codegen/add": "^4.0.1",
"@graphql-codegen/cli": "^3.3.0",
"@graphql-codegen/time": "^4.0.0",
"@graphql-codegen/typescript": "^3.0.0",
"@graphql-codegen/typescript": "^3.0.3",
"@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
"@graphql-codegen/typescript-operations": "^3.0.0",
"@graphql-codegen/typescript-graphql-request": "^4.5.9",
"@graphql-codegen/typescript-operations": "^3.0.3",
"@types/cors": "^2.8.13",
"@types/gl": "^6.0.2",
"@types/jpeg-js": "^0.3.7",
"@types/pngjs": "^6.0.1",
"@types/jest": "^29.4.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@types/jest": "^29.5.0",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"benchmark": "^2.1.4",
"concurrently": "^7.6.0",
"cpx2": "^4.2.0",
"concurrently": "^8.0.1",
"cpx2": "^4.2.3",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.3",
"eslint": "^8.33.0",
"eslint": "^8.38.0",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^6.2.0",
"fs-extra": "^11.1.0",
"fs-extra": "^11.1.1",
"graphql": "^16.6.0",
"http-server": "^14.1.1",
"jest": "^29.4.1",
"mini-css-extract-plugin": "^2.7.2",
"jest": "^29.5.0",
"mini-css-extract-plugin": "^2.7.5",
"path-browserify": "^1.0.1",
"raw-loader": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.58.0",
"sass-loader": "^13.2.0",
"simple-git": "^3.16.0",
"sass": "^1.62.0",
"sass-loader": "^13.2.2",
"simple-git": "^3.17.0",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.1",
"ts-jest": "^29.0.5",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
"style-loader": "^3.3.2",
"ts-jest": "^29.1.0",
"typescript": "^5.0.4",
"webpack": "^5.79.0",
"webpack-cli": "^5.0.1"
},
"dependencies": {
@@ -147,20 +149,20 @@
"@types/benchmark": "^2.1.2",
"@types/compression": "1.7.2",
"@types/express": "^4.17.17",
"@types/node": "^16.18.12",
"@types/node-fetch": "^2.6.2",
"@types/node": "^16.18.23",
"@types/node-fetch": "^2.6.3",
"@types/swagger-ui-dist": "3.30.1",
"argparse": "^2.0.1",
"body-parser": "^1.20.1",
"body-parser": "^1.20.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.18.2",
"h264-mp4-encoder": "^1.0.12",
"immer": "^9.0.19",
"immutable": "^4.2.3",
"immer": "^9.0.21",
"immutable": "^4.3.0",
"node-fetch": "^2.6.9",
"rxjs": "^7.8.0",
"swagger-ui-dist": "^4.15.5",
"swagger-ui-dist": "^4.18.2",
"tslib": "^2.5.0",
"util.promisify": "^1.1.1",
"xhr2": "^0.2.1"

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2021 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>
*/
@@ -15,7 +15,7 @@ const deployDir = path.resolve(buildDir, 'deploy/');
const localPath = path.resolve(deployDir, 'molstar.github.io/');
const analyticsTag = /<!-- __MOLSTAR_ANALYTICS__ -->/g;
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics -->`;
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics --><script defer src="https://web3dsurvey.com/collector.js"></script>`;
function log(command, stdout, stderr) {
if (command) {

View File

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

View File

@@ -12,15 +12,21 @@
import { ArgumentParser } from 'argparse';
import fs from 'fs';
import path from 'path';
import gl from 'gl';
import pngjs from 'pngjs';
import jpegjs from 'jpeg-js';
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
import { DefaultPluginSpec } from '../../mol-plugin/spec';
import { STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
import { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
import { setFSModule } from '../../mol-util/data-source';
setFSModule(fs);
interface Args {
pdbId: string,
outDirectory: string
@@ -42,7 +48,8 @@ async function main() {
console.log('Outputs:', args.outDirectory);
// Create a headless plugin
const plugin = new HeadlessPluginContext(DefaultPluginSpec(), { width: 800, height: 800 });
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec(), { width: 800, height: 800 });
await plugin.init();
// Download and visualize data in the plugin

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 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>
*/
@@ -24,9 +24,31 @@ const Canvas3DPresets = {
illustrative: {
canvas3d: <Preset>{
postprocessing: {
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000), includeTransparent: true, } },
shadow: { name: 'off', params: {} },
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
resolutionScale: 1,
color: Color(0x000000),
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.33,
color: Color(0x000000),
includeTransparent: true,
}
},
shadow: {
name: 'off',
params: {}
},
},
renderer: {
ambientIntensity: 1.0,
@@ -37,9 +59,25 @@ const Canvas3DPresets = {
occlusion: {
canvas3d: <Preset>{
postprocessing: {
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
outline: { name: 'off', params: {} },
shadow: { name: 'off', params: {} },
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
resolutionScale: 1,
}
},
outline: {
name: 'off',
params: {}
},
shadow: {
name: 'off',
params: {}
},
},
renderer: {
ambientIntensity: 0.4,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 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 Ludovic Autin <ludovic.autin@gmail.com>
@@ -600,10 +600,12 @@ export const LoadCellPackModel = StateAction.build({
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 8,
bias: 1,
blurKernelSize: 15,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {

View File

@@ -14,11 +14,12 @@ import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Un
function getAtomPosition(vec: Vec3, loc: StructureElement.Location, residue: DnatcoUtil.Residue, names: string[], altId: string, insCode: string) {
const eI = DnatcoUtil.getAtomIndex(loc, residue, names, altId, insCode);
if (eI !== -1)
if (eI !== -1) {
loc.unit.conformation.invariantPosition(eI, vec);
else {
vec[0] = 0; vec[1] = 0; vec[2] = 0;
return true;
}
return false; // Atom not found
}
const p_1 = Vec3();
@@ -29,19 +30,38 @@ const p3 = Vec3();
const p4 = Vec3();
const pP = Vec3();
const C5PrimeNames = ['C5\'', 'C5*'];
const O3PrimeNames = ['O3\'', 'O3*'];
const O5PrimeNames = ['O5\'', 'O5*'];
const PNames = ['P'];
function getPoints(
loc: StructureElement.Location,
r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue,
altId0: string, altId1: string, altId2: string,
insCode0: string, insCode1: string, insCode2: string,
) {
if (r0) getAtomPosition(p_1, loc, r0, ['C5\'', 'C5*'], altId0, insCode0);
r0 ? getAtomPosition(p0, loc, r0, ['O3\'', 'O3*'], altId0, insCode0) : getAtomPosition(p0, loc, r1, ['O5\'', 'O5*'], altId1, insCode1);
getAtomPosition(p1, loc, r1, ['C5\'', 'C5*'], altId1, insCode1);
getAtomPosition(p2, loc, r1, ['O3\'', 'O3*'], altId1, insCode1);
getAtomPosition(p3, loc, r2, ['C5\'', 'C5*'], altId2, insCode2);
getAtomPosition(p4, loc, r2, ['O3\'', 'O3*'], altId2, insCode2);
getAtomPosition(pP, loc, r2, ['P'], altId2, insCode2);
if (r0) {
if (!getAtomPosition(p_1, loc, r0, C5PrimeNames, altId0, insCode0))
return void 0;
if (!getAtomPosition(p0, loc, r0, O3PrimeNames, altId0, insCode0))
return void 0;
} else {
if (!getAtomPosition(p0, loc, r1, O5PrimeNames, altId1, insCode1))
return void 0;
}
if (!getAtomPosition(p1, loc, r1, C5PrimeNames, altId1, insCode1))
return void 0;
if (!getAtomPosition(p2, loc, r1, O3PrimeNames, altId1, insCode1))
return void 0;
if (!getAtomPosition(p3, loc, r2, C5PrimeNames, altId2, insCode2))
return void 0;
if (!getAtomPosition(p4, loc, r2, O3PrimeNames, altId2, insCode2))
return void 0;
if (!getAtomPosition(pP, loc, r2, PNames, altId2, insCode2))
return void 0;
return { p_1, p0, p1, p2, p3, p4, pP };
}
@@ -142,9 +162,12 @@ export class NtCTubeSegmentsIterator {
const insCodeTwo = step.PDB_ins_code_2;
const followsGap = !!r0 && hasGapElements(r0, this.loc.unit) && hasGapElements(r1, this.loc.unit);
const precedesDiscontinuity = r3 ? r3.index !== r2.index + 1 : false;
const points = getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo);
if (!points)
return void 0;
return {
...getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo),
...points,
stepIdx,
followsGap,
firstInChain: !r0,

View File

@@ -19,7 +19,7 @@ import { VolsegEntryFromRoot, VolsegGlobalStateFromRoot, VolsegStateFromEntry }
import { VolsegUI } from './ui';
const DEBUGGING = window.location.hostname === 'localhost';
const DEBUGGING = typeof window !== 'undefined' ? window?.location?.hostname === 'localhost' : false;
export const VolsegVolumeServerConfig = {
// DefaultServer: new PluginConfigItem('volseg-volume-server', DEFAULT_VOLUME_SERVER_V2),

View File

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

View File

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

View File

@@ -194,7 +194,7 @@ class Camera implements ICamera {
getPixelSize(point: Vec3) {
// project -> unproject of `point` does not exactly return the same
// to get a sufficiently accurate measure we unproject the original
// clip position in addition to the one shifted bey one pixel
// clip position in addition to the one shifted by one pixel
this.project(tmpClip, point);
this.unproject(tmpPos1, tmpClip);
tmpClip[0] += 1;
@@ -278,6 +278,7 @@ namespace Camera {
fog: 50,
clipFar: true,
minNear: 5,
minFar: 0,
};
}
@@ -294,6 +295,7 @@ namespace Camera {
fog: number
clipFar: boolean
minNear: number
minFar: number
}
export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
@@ -311,6 +313,7 @@ namespace Camera {
if (typeof source.fog !== 'undefined') out.fog = source.fog;
if (typeof source.clipFar !== 'undefined') out.clipFar = source.clipFar;
if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
if (typeof source.minFar !== 'undefined') out.minFar = source.minFar;
return out;
}
@@ -323,6 +326,7 @@ namespace Camera {
&& a.fog === b.fog
&& a.clipFar === b.clipFar
&& a.minNear === b.minNear
&& a.minFar === b.minFar
&& Vec3.exactEquals(a.position, b.position)
&& Vec3.exactEquals(a.up, b.up)
&& Vec3.exactEquals(a.target, b.target);
@@ -390,18 +394,14 @@ function updatePers(camera: Camera) {
}
function updateClip(camera: Camera) {
let { radius, radiusMax, mode, fog, clipFar, minNear } = camera.state;
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar } = camera.state;
if (radius < 0.01) radius = 0.01;
const normalizedFar = clipFar ? radius : radiusMax;
const normalizedFar = Math.max(clipFar ? radius : radiusMax, minFar);
const cameraDistance = Vec3.distance(camera.position, camera.target);
let near = cameraDistance - radius;
let far = cameraDistance + normalizedFar;
const fogNearFactor = -(50 - fog) / 50;
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
const fogFar = far;
if (mode === 'perspective') {
// set at least to 5 to avoid slow sphere impostor rendering
near = Math.max(Math.min(radiusMax, minNear), near);
@@ -417,8 +417,12 @@ function updateClip(camera: Camera) {
far = near + 0.01;
}
const fogNearFactor = -(50 - fog) / 50;
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
const fogFar = far;
camera.near = near;
camera.far = 2 * far; // avoid precision issues distingushing far objects from background
camera.far = far;
camera.fogNear = fogNear;
camera.fogFar = fogFar;
}

View File

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

View File

@@ -65,7 +65,7 @@ export const Canvas3DParams = {
cameraClipping: PD.Group({
radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
minNear: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
minNear: PD.Numeric(5, { min: 0.1, max: 100, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
}, { pivot: 'radius' }),
viewport: PD.MappedStatic('canvas', {
canvas: PD.Group({}),
@@ -332,12 +332,12 @@ namespace Canvas3D {
}, { x, y, width, height }, { pixelScale: attribs.pixelScale });
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
const controls = TrackballControls.create(input, camera, p.trackball);
const controls = TrackballControls.create(input, camera, scene, p.trackball);
const renderer = Renderer.create(webgl, p.renderer);
const helper = new Helper(webgl, scene, p);
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, controls, p.interaction);
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
@@ -615,22 +615,32 @@ namespace Canvas3D {
}
function consoleStats() {
console.table(scene.renderables.map(r => ({
const items = scene.renderables.map(r => ({
drawCount: r.values.drawCount.ref.value,
instanceCount: r.values.instanceCount.ref.value,
materialId: r.materialId,
renderItemId: r.id,
})));
console.log(webgl.stats);
}));
console.groupCollapsed(`${items.length} RenderItems`);
if (items.length < 50) {
console.table(items);
} else {
console.log(items);
}
console.log(JSON.stringify(webgl.stats, undefined, 4));
const { texture, attribute, elements } = webgl.resources.getByteCounts();
console.log({
console.log(JSON.stringify({
texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
});
}, undefined, 4));
console.log(webgl.timer.formatedStats());
console.log(JSON.stringify(webgl.timer.formatedStats(), undefined, 4));
console.groupEnd();
}
function add(repr: Representation.Any) {
@@ -908,6 +918,7 @@ namespace Canvas3D {
},
dispose: () => {
contextRestoredSub.unsubscribe();
cancelAnimationFrame(animationFrameHandle);
markBuffer = [];

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -13,6 +13,7 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
import { Camera } from '../camera';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Bond } from '../../mol-model/structure';
import { TrackballControls } from '../controls/trackball';
type Canvas3D = import('../canvas3d').Canvas3D
type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
@@ -68,7 +69,7 @@ export class Canvas3dInteractionHelper {
}
private identify(e: InputEvent, t: number) {
const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
if (e === InputEvent.Drag) {
if (xyChanged && !this.outsideViewport(this.startX, this.startY)) {
@@ -188,7 +189,7 @@ export class Canvas3dInteractionHelper {
this.ev.dispose();
}
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, props: Partial<Canvas3dInteractionHelperProps> = {}) {
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private controls: TrackballControls, props: Partial<Canvas3dInteractionHelperProps> = {}) {
this.props = { ...PD.getDefaultValues(Canvas3dInteractionHelperParams), ...props };
input.drag.subscribe(({ x, y, buttons, button, modifiers }) => {
@@ -197,8 +198,12 @@ export class Canvas3dInteractionHelper {
this.drag(x, y, buttons, button, modifiers);
});
input.move.subscribe(({ x, y, inside, buttons, button, modifiers }) => {
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement }) => {
if (!inside || this.isInteracting) return;
if (!onElement) {
this.leave();
return;
}
// console.log('move');
this.move(x, y, buttons, button, modifiers);
});

View File

@@ -106,7 +106,7 @@ export class MarkingPass {
const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength, highlightEdgeStrength, selectEdgeStrength } = props;
const { values: edgeValues } = this.edge;
const _edgeScale = Math.round(edgeScale * this.webgl.pixelRatio);
const _edgeScale = Math.max(1, Math.round(edgeScale * this.webgl.pixelRatio));
if (edgeValues.dEdgeScale.ref.value !== _edgeScale) {
ValueCell.update(edgeValues.dEdgeScale, _edgeScale);
this.edge.update();

View File

@@ -358,7 +358,7 @@ export class PickHelper {
const z = this.getDepth(xp, yp);
// console.log('z', z);
const position = Vec3.create(x, viewport.height - y, z);
const position = Vec3.create(x, y, z);
if (StereoCamera.is(camera)) {
const halfWidth = Math.floor(viewport.width / 2);
if (x > viewport.x + halfWidth) {

View File

@@ -11,7 +11,7 @@ import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/rende
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { ValueCell } from '../../mol-util';
import { deepEqual, ValueCell } from '../../mol-util';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
@@ -43,9 +43,9 @@ const OutlinesSchema = {
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uMaxPossibleViewZDiff: UniformSpec('f'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
@@ -63,9 +63,9 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uInvProjection: ValueCell.create(Mat4.identity()),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
@@ -137,6 +137,8 @@ function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): Shadows
const SsaoSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uSamples: UniformSpec('v3[]'),
dNSamples: DefineSpec('number'),
@@ -149,14 +151,23 @@ const SsaoSchema = {
uRadius: UniformSpec('f'),
uBias: UniformSpec('f'),
dMultiScale: DefineSpec('boolean'),
dLevels: DefineSpec('number'),
uLevelRadius: UniformSpec('f[]'),
uLevelBias: UniformSpec('f[]'),
uNearThreshold: UniformSpec('f'),
uFarThreshold: UniformSpec('f'),
};
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): 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),
uSamples: ValueCell.create(getSamples(32)),
dNSamples: ValueCell.create(32),
@@ -167,8 +178,15 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
uRadius: ValueCell.create(8.0),
uBias: ValueCell.create(0.025),
uRadius: ValueCell.create(Math.pow(2, 5)),
uBias: ValueCell.create(0.8),
dMultiScale: ValueCell.create(false),
dLevels: ValueCell.create(3),
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
uNearThreshold: ValueCell.create(10.0),
uFarThreshold: ValueCell.create(1500.0),
};
const schema = { ...SsaoSchema };
@@ -189,8 +207,7 @@ const SsaoBlurSchema = {
uBlurDirectionX: UniformSpec('f'),
uBlurDirectionY: UniformSpec('f'),
uMaxPossibleViewZDiff: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uBounds: UniformSpec('v4'),
@@ -211,8 +228,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
uInvProjection: ValueCell.create(Mat4.identity()),
uNear: ValueCell.create(0.0),
uFar: ValueCell.create(10000.0),
uBounds: ValueCell.create(Vec4()),
@@ -280,11 +296,9 @@ const PostprocessingSchema = {
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uOutlineColor: UniformSpec('v3'),
uOcclusionColor: UniformSpec('v3'),
uTransparentBackground: UniformSpec('b'),
uMaxPossibleViewZDiff: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
dOcclusionEnable: DefineSpec('boolean'),
uOcclusionOffset: UniformSpec('v2'),
@@ -292,13 +306,10 @@ const PostprocessingSchema = {
dOutlineEnable: DefineSpec('boolean'),
dOutlineScale: DefineSpec('number'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
const values: Values<typeof PostprocessingSchema> = {
...QuadValues,
@@ -317,11 +328,9 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
uFogFar: ValueCell.create(10000),
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
uOcclusionColor: ValueCell.create(Vec3.create(0, 0, 0)),
uTransparentBackground: ValueCell.create(false),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
uInvProjection: ValueCell.create(Mat4.identity()),
dOcclusionEnable: ValueCell.create(true),
uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
@@ -329,8 +338,6 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
dOutlineEnable: ValueCell.create(false),
dOutlineScale: ValueCell.create(1),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
@@ -345,10 +352,27 @@ export const PostprocessingParams = {
occlusion: PD.MappedStatic('on', {
on: PD.Group({
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
multiScale: PD.MappedStatic('off', {
on: PD.Group({
levels: PD.ObjectList({
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
{ radius: 2, bias: 1 },
{ radius: 5, bias: 1 },
{ radius: 8, bias: 1 },
{ radius: 11, bias: 1 },
] }),
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
}),
off: PD.Group({})
}, { cycle: true }),
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
color: PD.Color(Color(0x000000)),
}),
off: PD.Group({})
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
@@ -380,6 +404,27 @@ export const PostprocessingParams = {
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
type Levels = {
count: number
radius: number[]
bias: number[]
}
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
const count = props.length;
const { radius, bias } = levels || {
radius: (new Array(count * 3)).fill(0),
bias: (new Array(count * 3)).fill(0),
};
props = props.slice().sort((a, b) => a.radius - b.radius);
for (let i = 0; i < count; ++i) {
const p = props[i];
radius[i] = Math.pow(2, p.radius);
bias[i] = p.bias;
}
return { count, radius, bias };
}
export class PostprocessingPass {
static isEnabled(props: PostprocessingProps) {
return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
@@ -404,6 +449,12 @@ export class PostprocessingPass {
private readonly downsampledDepthTarget: RenderTarget;
private readonly downsampleDepthRenderable: CopyRenderable;
private readonly depthHalfTarget: RenderTarget;
private readonly depthHalfRenderable: CopyRenderable;
private readonly depthQuarterTarget: RenderTarget;
private readonly depthQuarterRenderable: CopyRenderable;
private readonly ssaoDepthTexture: Texture;
private readonly ssaoDepthBlurProxyTexture: Texture;
@@ -423,6 +474,8 @@ export class PostprocessingPass {
return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
}
private levels: { radius: number, bias: number }[];
private readonly bgColor = Vec3();
readonly background: BackgroundPass;
@@ -435,6 +488,7 @@ export class PostprocessingPass {
this.blurKernelSize = 1;
this.downsampleFactor = 1;
this.ssaoScale = this.calcSsaoScale();
this.levels = [];
// needs to be linear for anti-aliasing pass
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
@@ -452,11 +506,27 @@ export class PostprocessingPass {
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.downsampledDepthTarget = drawPass.packedDepth
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
this.depthHalfTarget = drawPass.packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
this.depthQuarterTarget = drawPass.packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
@@ -467,7 +537,7 @@ export class PostprocessingPass {
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
this.ssaoRenderable = getSsaoRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
this.ssaoRenderable = getSsaoRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture, this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true);
@@ -482,19 +552,30 @@ export class PostprocessingPass {
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
this.ssaoScale = ssaoScale;
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
this.target.setSize(width, height);
this.outlinesTarget.setSize(width, height);
this.shadowsTarget.setSize(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.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.depthQuarterTarget.setSize(qw, qh);
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
@@ -543,11 +624,14 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection);
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateSsaoBlur = true;
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
if (this.nSamples !== props.occlusion.params.samples) {
needsUpdateSsao = true;
@@ -556,7 +640,30 @@ export class PostprocessingPass {
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
const multiScale = props.occlusion.params.multiScale.name === 'on';
if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
needsUpdateSsao = true;
ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
}
if (props.occlusion.params.multiScale.name === 'on') {
const mp = props.occlusion.params.multiScale.params;
if (!deepEqual(this.levels, mp.levels)) {
needsUpdateSsao = true;
this.levels = mp.levels;
const levels = getLevels(mp.levels);
ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count);
ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius);
ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias);
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold);
ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold);
} else {
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
@@ -567,8 +674,8 @@ export class PostprocessingPass {
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
}
if (this.downsampleFactor !== props.occlusion.params.resolutionScale) {
@@ -579,22 +686,36 @@ export class PostprocessingPass {
const sw = Math.floor(w * this.ssaoScale);
const sh = Math.floor(h * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.floor(sw * 0.5);
const hh = Math.floor(sh * 0.5);
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.floor(sw * 0.25);
const qh = Math.floor(sh * 0.25);
this.depthQuarterTarget.setSize(qw, qh);
if (this.ssaoScale === 1) {
ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTextureOpaque);
} else {
ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture);
}
ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture);
ValueCell.update(this.ssaoRenderable.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.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
}
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color));
}
if (props.shadow.name === 'on') {
@@ -611,7 +732,10 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.shadowsRenderable.values.dOrthographic, orthographic);
if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic);
needsUpdateShadows = true;
}
ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
@@ -630,30 +754,33 @@ export class PostprocessingPass {
}
if (props.outline.name === 'on') {
let { threshold, includeTransparent } = props.outline.params;
const transparentOutline = includeTransparent ?? true;
// orthographic needs lower threshold
if (camera.state.mode === 'orthographic') threshold /= 5;
const factor = Math.pow(1000, threshold / 10) / 1000;
// use radiusMax for stable outlines when zooming
const maxPossibleViewZDiff = factor * camera.state.radiusMax;
const transparentOutline = props.outline.params.includeTransparent ?? true;
const outlineScale = props.outline.params.scale - 1;
const outlineThreshold = 50 * props.outline.params.threshold;
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateOutlines = true; }
ValueCell.updateIfChanged(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
ValueCell.update(this.outlinesRenderable.values.uInvProjection, invProjection);
if (this.outlinesRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
}
if (this.outlinesRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.outlinesRenderable.values.uOutlineThreshold, outlineThreshold);
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dTransparentOutline, transparentOutline);
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOutlineScale, outlineScale);
}
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline);
}
}
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
@@ -662,15 +789,23 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor));
ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground);
if (this.renderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
}
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, outlinesEnabled);
if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dShadowEnable, shadowsEnabled);
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOutlineEnable, outlinesEnabled);
}
if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dShadowEnable, shadowsEnabled);
}
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
}
if (needsUpdateOutlines) {
this.outlinesRenderable.update();
@@ -699,10 +834,6 @@ export class PostprocessingPass {
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
const { x, y, width, height } = camera.viewport;
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
}
private occlusionOffset: [x: number, y: number] = [0, 0];
@@ -721,25 +852,38 @@ export class PostprocessingPass {
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
this.updateState(camera, transparentBackground, backgroundColor, props, light);
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
}
if (props.shadow.name === 'on') {
this.shadowsTarget.bind();
this.shadowsRenderable.render();
}
const { gl, state } = this.webgl;
const { x, y, width, height } = camera.viewport;
// don't render occlusion if offset is given,
// which will reuse the existing occlusion
if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) {
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
const sx = Math.floor(x * this.ssaoScale);
const sy = Math.floor(y * this.ssaoScale);
const sw = Math.ceil(width * this.ssaoScale);
const sh = Math.ceil(height * this.ssaoScale);
state.viewport(sx, sy, sw, sh);
state.scissor(sx, sy, sw, sh);
if (this.ssaoScale < 1) {
if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
this.downsampledDepthTarget.bind();
this.downsampleDepthRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
}
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
this.depthHalfTarget.bind();
this.depthHalfRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
this.depthQuarterTarget.bind();
this.depthQuarterRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
this.ssaoFramebuffer.bind();
this.ssaoRenderable.render();
@@ -751,14 +895,25 @@ export class PostprocessingPass {
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
}
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
}
if (props.shadow.name === 'on') {
this.shadowsTarget.bind();
this.shadowsRenderable.render();
}
if (toDrawingBuffer) {
this.webgl.unbindFramebuffer();
} else {
this.target.bind();
}
const { gl, state } = this.webgl;
this.background.update(camera, props.background);
if (this.background.isEnabled(props.background)) {
if (this.transparentBackground) {
@@ -844,4 +999,3 @@ export class AntialiasingPass {
}
}
}

View File

@@ -16,7 +16,7 @@ const c = {
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84FF,
MAX_TEXTURE_IMAGE_UNITS_NV: 0x8872
};
} as const;
const gl = {
ACTIVE_ATTRIBUTES: 35721,
@@ -316,7 +316,7 @@ const gl = {
VERTEX_SHADER: 35633,
VIEWPORT: 2978,
ZERO: 0
};
} as const;
type gl = typeof gl
export function createGl(width: number, height: number, contextAttributes: WebGLContextAttributes): WebGLRenderingContext {
@@ -371,66 +371,66 @@ export function createGl(width: number, height: number, contextAttributes: WebGL
case 'EXT_blend_minmax': return {
MAX_EXT: 0,
MIN_EXT: 0
} as EXT_blend_minmax;
} as unknown as EXT_blend_minmax;
case 'EXT_texture_filter_anisotropic': return {
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0,
TEXTURE_MAX_ANISOTROPY_EXT: 0
} as EXT_texture_filter_anisotropic;
} as unknown as EXT_texture_filter_anisotropic;
case 'EXT_frag_depth': return {} as EXT_frag_depth;
case 'EXT_shader_texture_lod': return {} as EXT_shader_texture_lod;
case 'EXT_shader_texture_lod': return {} as unknown as EXT_shader_texture_lod;
case 'EXT_sRGB': return {
FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT: 0,
SRGB8_ALPHA8_EXT: 0,
SRGB_ALPHA_EXT: 0,
SRGB_EXT: 0
} as EXT_sRGB;
} as unknown as EXT_sRGB;
case 'OES_vertex_array_object': return {
VERTEX_ARRAY_BINDING_OES: 0,
bindVertexArrayOES: function (arrayObject: WebGLVertexArrayObjectOES) { },
createVertexArrayOES: function (): WebGLVertexArrayObjectOES { return {}; },
deleteVertexArrayOES: function (arrayObject: WebGLVertexArrayObjectOES) { },
isVertexArrayOES: function (value: any) { return true; }
} as OES_vertex_array_object;
} as unknown as OES_vertex_array_object;
case 'WEBGL_color_buffer_float': return {
FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0,
RGB32F_EXT: 0,
RGBA32F_EXT: 0,
UNSIGNED_NORMALIZED_EXT: 0
} as WEBGL_color_buffer_float;
} as unknown as WEBGL_color_buffer_float;
case 'WEBGL_compressed_texture_astc': return null;
case 'WEBGL_compressed_texture_s3tc_srgb': return null;
case 'WEBGL_debug_shaders': return {
getTranslatedShaderSource(shader: WebGLShader) { return ''; }
} as WEBGL_debug_shaders;
} as unknown as WEBGL_debug_shaders;
case 'WEBGL_draw_buffers': return null;
case 'WEBGL_lose_context': return {
loseContext: function () { },
restoreContext: function () { },
} as WEBGL_lose_context;
} as unknown as WEBGL_lose_context;
case 'WEBGL_depth_texture': return {
UNSIGNED_INT_24_8_WEBGL: 0
} as WEBGL_depth_texture;
} as unknown as WEBGL_depth_texture;
case 'WEBGL_debug_renderer_info': return {
UNMASKED_RENDERER_WEBGL: 0,
UNMASKED_VENDOR_WEBGL: 0
} as WEBGL_debug_renderer_info;
} as unknown as WEBGL_debug_renderer_info;
case 'WEBGL_compressed_texture_s3tc': return null;
case 'OES_texture_half_float_linear': return {} as OES_texture_half_float_linear;
case 'OES_texture_half_float_linear': return {} as unknown as OES_texture_half_float_linear;
case 'OES_texture_half_float': return {
HALF_FLOAT_OES: 0
} as OES_texture_half_float;
case 'OES_texture_float_linear': return {} as OES_texture_float_linear;
case 'OES_texture_float': return {} as OES_texture_float;
} as unknown as OES_texture_half_float;
case 'OES_texture_float_linear': return {} as unknown as OES_texture_float_linear;
case 'OES_texture_float': return {} as unknown as OES_texture_float;
case 'OES_standard_derivatives': return {
FRAGMENT_SHADER_DERIVATIVE_HINT_OES: 0
} as OES_standard_derivatives;
case 'OES_element_index_uint': return {} as OES_element_index_uint;
} as unknown as OES_standard_derivatives;
case 'OES_element_index_uint': return {} as unknown as OES_element_index_uint;
case 'ANGLE_instanced_arrays': return {
drawArraysInstancedANGLE: function (mode: number, first: number, count: number, primcount: number) {},
drawElementsInstancedANGLE: function (mode: number, count: number, type: number, offset: number, primcount: number) {},
vertexAttribDivisorANGLE: function (index: number, divisor: number) {},
VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE: 0
} as ANGLE_instanced_arrays;
} as unknown as ANGLE_instanced_arrays;
}
return null;
},

View File

@@ -76,13 +76,15 @@ export function AttributeSpec<K extends AttributeKind>(kind: K, itemSize: Attrib
return { type: 'attribute', kind, itemSize, divisor };
}
export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K, variant?: 'material' | 'buffered' }
export function UniformSpec<K extends UniformKind>(kind: K, variant?: 'material' | 'buffered'): UniformSpec<K> {
type UniformVariant = 'material' | 'buffered'
export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K, variant?: UniformVariant }
export function UniformSpec<K extends UniformKind>(kind: K, variant?: UniformVariant): UniformSpec<K> {
return { type: 'uniform', kind, variant };
}
export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: 'material' }
export function TextureSpec<K extends TextureKind>(kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: 'material'): TextureSpec<K> {
type TextureVariant = 'material'
export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: TextureVariant }
export function TextureSpec<K extends TextureKind>(kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: TextureVariant): TextureSpec<K> {
return { type: 'texture', kind, format, dataType, filter, variant };
}
@@ -160,6 +162,7 @@ export const GlobalUniformSchema = {
uMarkerAverage: UniformSpec('f'),
uXrayEdgeFalloff: UniformSpec('f'),
uExposure: UniformSpec('f'),
uRenderMask: UniformSpec('i'),
uMarkingDepthTest: UniformSpec('b'),

View File

@@ -104,6 +104,7 @@ export const RendererParams = {
markerPriority: PD.Select(1, [[1, 'Highlight'], [2, 'Select']]),
xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
exposure: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.01 }),
light: PD.ObjectList({
inclination: PD.Numeric(150, { min: 0, max: 180, step: 1 }),
@@ -130,18 +131,19 @@ export type Light = {
const tmpDir = Vec3();
const tmpColor = Vec3();
function getLight(props: RendererProps['light'], light?: Light): Light {
const count = props.length;
const { direction, color } = light || {
direction: (new Array(5 * 3)).fill(0),
color: (new Array(5 * 3)).fill(0),
direction: (new Array(count * 3)).fill(0),
color: (new Array(count * 3)).fill(0),
};
for (let i = 0, il = props.length; i < il; ++i) {
for (let i = 0; i < count; ++i) {
const p = props[i];
Vec3.directionFromSpherical(tmpDir, degToRad(p.inclination), degToRad(p.azimuth), 1);
Vec3.toArray(tmpDir, direction, i * 3);
Vec3.scale(tmpColor, Color.toVec3Normalized(tmpColor, p.color), p.intensity);
Vec3.toArray(tmpColor, color, i * 3);
}
return { count: props.length, direction, color };
return { count, direction, color };
}
namespace Renderer {
@@ -242,6 +244,7 @@ namespace Renderer {
uMarkerAverage: ValueCell.create(0),
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
uExposure: ValueCell.create(p.exposure),
};
const globalUniformList = Object.entries(globalUniforms);
@@ -460,7 +463,8 @@ namespace Renderer {
for (let i = 0, il = renderables.length; i < il; ++i) {
const r = renderables[i];
if (r.values.markerAverage.ref.value !== 1) {
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
if (alpha !== 0 && r.values.markerAverage.ref.value !== 1) {
renderObject(renderables[i], 'marking', Flag.None);
}
}
@@ -607,7 +611,7 @@ namespace Renderer {
// TODO: simplify, handle in renderable.state???
// uAlpha is updated in "render" so we need to recompute it here
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || r.values.dXrayShaded?.ref.value) {
if ((alpha < 1 && alpha !== 0) || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || r.values.dXrayShaded?.ref.value) {
renderObject(r, 'colorWboit', Flag.None);
}
}
@@ -655,7 +659,7 @@ namespace Renderer {
// TODO: simplify, handle in renderable.state???
// uAlpha is updated in "render" so we need to recompute it here
const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
if ((alpha < 1 && alpha !== 0) || r.values.transparencyAverage.ref.value > 0 || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || r.values.dXrayShaded?.ref.value) {
renderObject(r, 'colorDpoit', Flag.None);
}
}
@@ -787,6 +791,10 @@ namespace Renderer {
p.xrayEdgeFalloff = props.xrayEdgeFalloff;
ValueCell.update(globalUniforms.uXrayEdgeFalloff, p.xrayEdgeFalloff);
}
if (props.exposure !== undefined && props.exposure !== p.exposure) {
p.exposure = props.exposure;
ValueCell.update(globalUniforms.uExposure, p.exposure);
}
if (props.light !== undefined && !deepEqual(props.light, p.light)) {
p.light = props.light;

View File

@@ -8,7 +8,7 @@
import { WebGLContext } from './webgl/context';
import { GraphicsRenderObject, createRenderable } from './render-object';
import { Object3D } from './object3d';
import { Sphere3D } from '../mol-math/geometry';
import { Sphere3D } from '../mol-math/geometry/primitives/sphere3d';
import { CommitQueue } from './commit-queue';
import { now } from '../mol-util/now';
import { arraySetRemove } from '../mol-util/array';
@@ -129,10 +129,8 @@ namespace Scene {
renderableMap.set(o, renderable);
boundingSphereDirty = true;
boundingSphereVisibleDirty = true;
return renderable;
} else {
console.warn(`RenderObject with id '${o.id}' already present`);
return renderableMap.get(o)!;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*
@@ -9,6 +9,13 @@
export const apply_light_color = `
#ifdef dIgnoreLight
#ifdef bumpEnabled
if (uBumpFrequency > 0.0 && uBumpAmplitude > 0.0 && bumpiness > 0.0) {
material.rgb += fbm(vModelPosition * uBumpFrequency) * (uBumpAmplitude * bumpiness) / uBumpFrequency;
material.rgb -= bumpiness / (2.0 * uBumpFrequency);
}
#endif
gl_FragColor = material;
#else
#ifdef bumpEnabled
@@ -65,4 +72,6 @@ export const apply_light_color = `
#ifdef dXrayShaded
gl_FragColor.a *= 1.0 - pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
#endif
gl_FragColor.rgb *= uExposure;
`;

View File

@@ -72,6 +72,7 @@ uniform vec3 uInteriorColor;
bool interior;
uniform float uXrayEdgeFalloff;
uniform float uExposure;
uniform mat4 uProjection;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -71,8 +71,10 @@ void main() {
vViewPosition = mvPosition.xyz;
gl_Position = uProjection * mvPosition;
mvPosition.z -= 2.0 * (length(vEnd - vStart) + vSize); // avoid clipping
gl_Position.z = (uProjection * mvPosition).z;
if (gl_Position.z < -gl_Position.w) {
mvPosition.z -= 2.0 * (length(vEnd - vStart) + vSize); // avoid clipping
gl_Position.z = (uProjection * mvPosition).z;
}
#include clip_instance
}

View File

@@ -75,6 +75,7 @@ uniform vec3 uFogColor;
uniform float uAlpha;
uniform bool uTransparentBackground;
uniform float uXrayEdgeFalloff;
uniform float uExposure;
uniform int uRenderMask;

View File

@@ -16,8 +16,9 @@ uniform vec2 uTexSize;
uniform float uNear;
uniform float uFar;
uniform mat4 uInvProjection;
uniform float uMaxPossibleViewZDiff;
uniform float uOutlineThreshold;
#include common
@@ -49,17 +50,25 @@ bool isBackground(const in float depth) {
return depth == 1.0;
}
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);
return distance(viewPos0, viewPos1);
}
void main(void) {
float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
float backgroundViewZ = 2.0 * uFar;
vec2 coords = gl_FragCoord.xy / uTexSize;
vec2 invTexSize = 1.0 / uTexSize;
float selfDepthOpaque = getDepthOpaque(coords);
float selfViewZOpaque = isBackground(selfDepthOpaque) ? backgroundViewZ : getViewZ(selfDepthOpaque);
float pixelSizeOpaque = getPixelSize(coords, selfDepthOpaque) * uOutlineThreshold;
float selfDepthTransparent = getDepthTransparent(coords);
float selfViewZTransparent = isBackground(selfDepthTransparent) ? backgroundViewZ : getViewZ(selfDepthTransparent);
float pixelSizeTransparent = getPixelSize(coords, selfDepthTransparent) * uOutlineThreshold;
float outline = 1.0;
float bestDepth = 1.0;
@@ -73,14 +82,14 @@ void main(void) {
float sampleDepthTransparent = getDepthTransparent(sampleCoords);
float sampleViewZOpaque = isBackground(sampleDepthOpaque) ? backgroundViewZ : getViewZ(sampleDepthOpaque);
if (abs(selfViewZOpaque - sampleViewZOpaque) > uMaxPossibleViewZDiff && selfDepthOpaque > sampleDepthOpaque && sampleDepthOpaque <= bestDepth) {
if (abs(selfViewZOpaque - sampleViewZOpaque) > pixelSizeOpaque && selfDepthOpaque > sampleDepthOpaque && sampleDepthOpaque <= bestDepth) {
outline = 0.0;
bestDepth = sampleDepthOpaque;
}
if (sampleDepthTransparent < sampleDepthOpaque) {
float sampleViewZTransparent = isBackground(sampleDepthTransparent) ? backgroundViewZ : getViewZ(sampleDepthTransparent);
if (abs(selfViewZTransparent - sampleViewZTransparent) > uMaxPossibleViewZDiff && selfDepthTransparent > sampleDepthTransparent && sampleDepthTransparent <= bestDepth) {
if (abs(selfViewZTransparent - sampleViewZTransparent) > pixelSizeTransparent && selfDepthTransparent > sampleDepthTransparent && sampleDepthTransparent <= bestDepth) {
outline = 0.0;
bestDepth = sampleDepthTransparent;
transparentFlag = 1.0;

View File

@@ -24,16 +24,10 @@ uniform float uFogNear;
uniform float uFogFar;
uniform vec3 uFogColor;
uniform vec3 uOutlineColor;
uniform vec3 uOcclusionColor;
uniform bool uTransparentBackground;
uniform vec2 uOcclusionOffset;
uniform float uMaxPossibleViewZDiff;
uniform mat4 uInvProjection;
const float outlineDistanceFactor = 5.0;
const vec3 occlusionColor = vec3(0.0);
#include common
float getViewZ(const in float depth) {
@@ -64,21 +58,14 @@ bool isBackground(const in float depth) {
return depth == 1.0;
}
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);
return distance(viewPos0, viewPos1);
}
float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
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 pixelSize = getPixelSize(coords, selfDepth);
float outline = 1.0;
closestTexel = 1.0;
@@ -96,7 +83,7 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel && abs(selfViewZ - sampleOutlineViewZ) > uMaxPossibleViewZDiff + (pixelSize * outlineDistanceFactor)) {
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
outline = 0.0;
closestTexel = sampleOutlineDepth;
}
@@ -130,9 +117,9 @@ void main(void) {
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
float occlusionFactor = getSsao(coords + uOcclusionOffset);
if (!uTransparentBackground) {
color.rgb = mix(mix(occlusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
color.rgb = mix(mix(uOcclusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
} else {
color.rgb = mix(occlusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
color.rgb = mix(uOcclusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
}
}
#endif

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 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>
*/
@@ -96,8 +96,10 @@ void main(void){
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
mvPosition.z -= 2.0 * vRadius; // avoid clipping
gl_Position.z = (uProjection * vec4(mvPosition.xyz, 1.0)).z;
if (gl_Position.z < -gl_Position.w) {
mvPosition.z -= 2.0 * vRadius; // avoid clipping
gl_Position.z = (uProjection * vec4(mvPosition.xyz, 1.0)).z;
}
#include clip_instance
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2021 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>
@@ -19,8 +19,7 @@ uniform float uKernel[dOcclusionKernelSize];
uniform float uBlurDirectionX;
uniform float uBlurDirectionY;
uniform float uMaxPossibleViewZDiff;
uniform mat4 uInvProjection;
uniform float uNear;
uniform float uFar;
@@ -42,6 +41,12 @@ bool outsideBounds(const in vec2 p) {
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
}
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);
return distance(viewPos0, viewPos1);
}
void main(void) {
vec2 coords = gl_FragCoord.xy / uTexSize;
@@ -60,6 +65,8 @@ void main(void) {
}
float selfViewZ = getViewZ(selfDepth);
float pixelSize = getPixelSize(coords, selfDepth);
float maxDiffViewZ = pixelSize * 10.0;
vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
@@ -67,6 +74,8 @@ void main(void) {
float kernelSum = 0.0;
// only if kernelSize is odd
for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
if (abs(float(i)) > 1.0 && abs(float(i)) * pixelSize > 0.8) continue;
vec2 sampleCoords = coords + float(i) * offset;
if (outsideBounds(sampleCoords)) {
continue;
@@ -79,9 +88,9 @@ void main(void) {
continue;
}
if (abs(float(i)) > 1.0) { // abs is not defined for int in webgl1
if (abs(float(i)) > 1.0) {
float sampleViewZ = getViewZ(sampleDepth);
if (abs(selfViewZ - sampleViewZ) > uMaxPossibleViewZDiff) {
if (abs(selfViewZ - sampleViewZ) > maxDiffViewZ) {
continue;
}
}

View File

@@ -13,6 +13,8 @@ precision highp sampler2D;
#include common
uniform sampler2D tDepth;
uniform sampler2D tDepthHalf;
uniform sampler2D tDepthQuarter;
uniform vec2 uTexSize;
uniform vec4 uBounds;
@@ -21,7 +23,14 @@ uniform vec3 uSamples[dNSamples];
uniform mat4 uProjection;
uniform mat4 uInvProjection;
uniform float uRadius;
#ifdef dMultiScale
uniform float uLevelRadius[dLevels];
uniform float uLevelBias[dLevels];
uniform float uNearThreshold;
uniform float uFarThreshold;
#else
uniform float uRadius;
#endif
uniform float uBias;
float smootherstep(float edge0, float edge1, float x) {
@@ -46,20 +55,38 @@ bool isBackground(const in float depth) {
return depth == 1.0;
}
bool outsideBounds(const in vec2 p) {
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
float getDepth(const in vec2 coords) {
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
#ifdef depthTextureSupport
return texture2D(tDepth, c).r;
#else
return unpackRGBAToDepth(texture2D(tDepth, c));
#endif
}
float getDepth(const in vec2 coords) {
if (outsideBounds(coords)) {
return 1.0;
} else {
#ifdef depthTextureSupport
return texture2D(tDepth, coords).r;
#else
return unpackRGBAToDepth(texture2D(tDepth, coords));
#endif
}
#define dQuarterThreshold 0.1
#define dHalfThreshold 0.05
float getMappedDepth(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);
#ifdef depthTextureSupport
if (d > dQuarterThreshold) {
return texture2D(tDepthQuarter, c).r;
} else if (d > dHalfThreshold) {
return texture2D(tDepthHalf, c).r;
} else {
return texture2D(tDepth, c).r;
}
#else
if (d > dQuarterThreshold) {
return unpackRGBAToDepth(texture2D(tDepthQuarter, c));
} else if (d > dHalfThreshold) {
return unpackRGBAToDepth(texture2D(tDepthHalf, c));
} else {
return unpackRGBAToDepth(texture2D(tDepth, c));
}
#endif
}
vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {
@@ -72,6 +99,12 @@ vec3 normalFromDepth(const in float depth, const in float depth1, const in float
return normalize(normal);
}
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);
return distance(viewPos0, viewPos1);
}
// StarCraft II Ambient Occlusion by [Filion and McNaughton 2008]
void main(void) {
vec2 invTexSize = 1.0 / uTexSize;
@@ -95,24 +128,50 @@ void main(void) {
vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
vec3 randomVec = normalize(vec3(getNoiseVec2(selfCoords) * 2.0 - 1.0, 0.0));
vec3 tangent = normalize(randomVec - selfViewNormal * dot(randomVec, selfViewNormal));
vec3 bitangent = cross(selfViewNormal, tangent);
mat3 TBN = mat3(tangent, bitangent, selfViewNormal);
float occlusion = 0.0;
for(int i = 0; i < dNSamples; i++){
vec3 sampleViewPos = TBN * uSamples[i];
sampleViewPos = selfViewPos + sampleViewPos * uRadius;
#ifdef dMultiScale
float pixelSize = getPixelSize(selfCoords, selfDepth);
vec4 offset = vec4(sampleViewPos, 1.0);
offset = uProjection * offset;
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
for(int l = 0; l < dLevels; l++) {
// TODO: smooth transition
if (pixelSize * uNearThreshold > uLevelRadius[l]) continue;
if (pixelSize * uFarThreshold < uLevelRadius[l]) continue;
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, getDepth(offset.xy)), uInvProjection).z;
float levelOcclusion = 0.0;
for(int i = 0; i < dNSamples; i++) {
vec3 sampleViewPos = TBN * uSamples[i];
sampleViewPos = selfViewPos + sampleViewPos * uLevelRadius[l];
occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
}
vec4 offset = vec4(sampleViewPos, 1.0);
offset = uProjection * offset;
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
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);
}
#else
for(int i = 0; i < dNSamples; i++) {
vec3 sampleViewPos = TBN * uSamples[i];
sampleViewPos = selfViewPos + sampleViewPos * uRadius;
vec4 offset = vec4(sampleViewPos, 1.0);
offset = uProjection * offset;
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
float sampleDepth = getMappedDepth(offset.xy, selfCoords);
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
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));
vec2 packedOcclusion = packUnitIntervalToRG(clamp(occlusion, 0.01, 1.0));

View File

@@ -23,8 +23,28 @@ export function isWebGL2(gl: any): gl is WebGL2RenderingContext {
* See https://registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/
*/
export interface COMPAT_instanced_arrays {
/**
* Renders primitives from array data like the `drawArrays` method. In addition, it can execute multiple instances of the range of elements.
* @param mode the type primitive to render.
* @param first the starting index in the array of vector points.
* @param count the number of indices to be rendered.
* @param primcount the number of instances of the range of elements to execute.
*/
drawArraysInstanced(mode: number, first: number, count: number, primcount: number): void;
/**
* Renders primitives from array data like the `drawElements` method. In addition, it can execute multiple instances of a set of elements.
* @param mode the type primitive to render.
* @param count the number of elements to be rendered.
* @param type the type of the values in the element array buffer.
* @param offset an offset in the element array buffer. Must be a valid multiple of the size of the given `type`.
* @param primcount the number of instances of the set of elements to execute.
*/
drawElementsInstanced(mode: number, count: number, type: number, offset: number, primcount: number): void;
/**
* Modifies the rate at which generic vertex attributes advance when rendering multiple instances of primitives with `drawArraysInstanced` and `drawElementsInstanced`
* @param index the index of the generic vertex attributes.
* @param divisor the number of instances that will pass between updates of the generic attribute.
*/
vertexAttribDivisor(index: number, divisor: number): void;
readonly VERTEX_ATTRIB_ARRAY_DIVISOR: number;
}
@@ -109,6 +129,9 @@ export function getVertexArrayObject(gl: GLRenderingContext): COMPAT_vertex_arra
}
}
/**
* See https://registry.khronos.org/webgl/extensions/OES_texture_float/
*/
export interface COMPAT_texture_float {
}
@@ -116,6 +139,9 @@ export function getTextureFloat(gl: GLRenderingContext): COMPAT_texture_float |
return isWebGL2(gl) ? {} : gl.getExtension('OES_texture_float');
}
/**
* See https://registry.khronos.org/webgl/extensions/OES_texture_float_linear/
*/
export interface COMPAT_texture_float_linear {
}
@@ -123,6 +149,9 @@ export function getTextureFloatLinear(gl: GLRenderingContext): COMPAT_texture_fl
return gl.getExtension('OES_texture_float_linear');
}
/**
* See https://registry.khronos.org/webgl/extensions/OES_texture_half_float/
*/
export interface COMPAT_texture_half_float {
readonly HALF_FLOAT: number
}
@@ -137,6 +166,9 @@ export function getTextureHalfFloat(gl: GLRenderingContext): COMPAT_texture_half
}
}
/**
* See https://registry.khronos.org/webgl/extensions/OES_texture_half_float_linear/
*/
export interface COMPAT_texture_half_float_linear {
}
@@ -172,6 +204,9 @@ export function getFragDepth(gl: GLRenderingContext): COMPAT_frag_depth | null {
return isWebGL2(gl) ? {} : gl.getExtension('EXT_frag_depth');
}
/**
* See https://registry.khronos.org/webgl/extensions/EXT_color_buffer_float/
*/
export interface COMPAT_color_buffer_float {
readonly RGBA32F: number;
}
@@ -193,6 +228,9 @@ export function getColorBufferFloat(gl: GLRenderingContext): COMPAT_color_buffer
}
}
/**
* See https://registry.khronos.org/webgl/extensions/EXT_color_buffer_half_float/
*/
export interface COMPAT_color_buffer_half_float {
readonly RGBA16F: number;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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>
*/
@@ -30,6 +30,8 @@ export interface Program {
destroy: () => void
}
export type Programs = { [k: string]: Program }
type Locations = { [k: string]: number }
function getLocations(gl: GLRenderingContext, program: WebGLProgram, schema: RenderableSchema) {

View File

@@ -9,7 +9,7 @@ import { createAttributeBuffers, ElementsBuffer, AttributeKind } from './buffer'
import { createTextures, Texture } from './texture';
import { WebGLContext, checkError } from './context';
import { ShaderCode, DefineValues } from '../shader-code';
import { Program } from './program';
import { Program, Programs } from './program';
import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, DefineSpec } from '../renderable/schema';
import { idFactory } from '../../mol-util/id-factory';
import { ValueCell } from '../../mol-util';
@@ -44,7 +44,7 @@ export interface RenderItem<T extends string> {
getProgram: (variant: T) => Program
render: (variant: T, sharedTexturesCount: number) => void
update: () => Readonly<ValueChanges>
update: () => void
destroy: () => void
}
@@ -71,9 +71,6 @@ function createProgramVariant(ctx: WebGLContext, variant: string, defineValues:
//
type ProgramVariants = Record<string, Program>
type VertexArrayVariants = Record<string, VertexArray | null>
function createValueChanges() {
return {
attributes: false,
@@ -132,7 +129,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
const glDrawMode = getDrawMode(ctx, drawMode);
const programs: ProgramVariants = {};
const programs: Programs = {};
for (const k of renderVariants) {
programs[k] = createProgramVariant(ctx, k, defineValues, shaderCode, schema);
}
@@ -147,7 +144,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
elementsBuffer = resources.elements(elements.ref.value);
}
const vertexArrays: VertexArrayVariants = {};
const vertexArrays: Record<string, VertexArray | null> = {};
for (const k of renderVariants) {
vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null;
}
@@ -328,7 +325,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
if (value.ref.version !== versions[k]) {
// update of textures with kind 'texture' is done externally
if (schema[k].kind !== 'texture') {
// console.log('texture version changed, uploading image', k);
// console.log('materialTexture version changed, uploading image', k);
texture.load(value.ref.value as TextureImage<any> | TextureVolume<any>);
valueChanges.textures = true;
} else {
@@ -346,8 +343,6 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
versions[k] = uniform.ref.version;
}
}
return valueChanges;
},
destroy: () => {
if (!destroyed) {
@@ -358,9 +353,11 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
}
textures.forEach(([k, texture]) => {
// lifetime of textures with kind 'texture' is defined externally
if (schema[k].kind !== 'texture') {
texture.destroy();
}
if (schema[k].kind !== 'texture') texture.destroy();
});
materialTextures.forEach(([k, texture]) => {
// lifetime of textures with kind 'texture' is defined externally
if (schema[k].kind !== 'texture') texture.destroy();
});
attributeBuffers.forEach(([_, buffer]) => buffer.destroy());
if (elementsBuffer) elementsBuffer.destroy();

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -59,7 +59,7 @@ export interface WebGLResources {
renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer
shader: (type: ShaderType, source: string) => Shader
texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture,
cubeTexture: (faces: CubeFaces, mipaps: boolean, onload?: () => void) => Texture,
cubeTexture: (faces: CubeFaces, mipmaps: boolean, onload?: () => void) => Texture,
vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
getByteCounts: () => ByteCounts

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 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>
*/

View File

@@ -16,7 +16,7 @@ export interface FileHandle {
* @param position The offset from the beginning of the file from which data should be read.
* @param sizeOrBuffer The buffer the data will be read from.
* @param length The number of bytes to read.
* @param byteOffset The offset in the buffer at which to start writing.
* @param byteOffset The offset in the buffer at which to start reading.
*/
readBuffer(position: number, sizeOrBuffer: SimpleBuffer | number, length?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: SimpleBuffer }>

View File

@@ -134,7 +134,6 @@ namespace Tokenizer {
/** Advance the state by the given number of lines and return line starts/ends as tokens. */
export async function readLinesAsync(state: Tokenizer, count: number, ctx: RuntimeContext, initialLineCount = 100000): Promise<Tokens> {
const { length } = state;
const lineTokens = TokenBuilder.create(state.data, count * 2);
let linesAlreadyRead = 0;
@@ -143,7 +142,7 @@ namespace Tokenizer {
readLinesChunk(state, linesToRead, lineTokens);
linesAlreadyRead += linesToRead;
return linesToRead;
}, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: length }));
}, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: state.length }));
return lineTokens;
}
@@ -174,7 +173,7 @@ namespace Tokenizer {
await chunkedSubtask(ctx, chunkSize, state, (chunkSize, state) => {
readLinesChunkChecked(state, chunkSize, tokens);
return state.position < state.length ? chunkSize : 0;
}, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: length }));
}, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: state.length }));
return tokens;
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mat4, Vec3 } from '../../linear-algebra';
import { Box3D } from '../primitives/box3d';
import { Frustum3D } from '../primitives/frustum3d';
import { Sphere3D } from '../primitives/sphere3d';
const v3 = Vec3.create;
const s3 = Sphere3D.create;
describe('frustum3d', () => {
it('intersectsSphere3D', () => {
const f = Frustum3D();
const m = Mat4.perspective(Mat4(), -1, 1, 1, -1, 1, 100);
Frustum3D.fromProjectionMatrix(f, m);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, 0), 0))).toBe(false);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, 0), 0.9))).toBe(false);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, 0), 1.1))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, -50), 0))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, -1.001), 0))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(-1, -1, -1.001), 0))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(-1.1, -1.1, -1.001), 0))).toBe(false);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(-1.1, -1.1, -1.001), 0.5))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(1, 1, -1.001), 0))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(1.1, 1.1, -1.001), 0))).toBe(false);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(1.1, 1.1, -1.001), 0.5))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, -99.999), 0))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(-99.999, -99.999, -99.999), 0))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(-100.1, -100.1, -100.1), 0))).toBe(false);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(-100.1, -100.1, -100.1), 0.5))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(99.999, 99.999, -99.999), 0))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(100.1, 100.1, -100.1), 0))).toBe(false);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(100.1, 100.1, -100.1), 0.2))).toBe(true);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, -101), 0))).toBe(false);
expect(Frustum3D.intersectsSphere3D(f, s3(v3(0, 0, -101), 1.1))).toBe(true);
});
it('intersectsBox3D', () => {
const f = Frustum3D();
const m = Mat4.perspective(Mat4(), -1, 1, 1, -1, 1, 100);
Frustum3D.fromProjectionMatrix(f, m);
const b0 = Box3D.create(v3(0, 0, 0), v3(1, 1, 1));
expect(Frustum3D.intersectsBox3D(f, b0)). toBe(false);
const b1 = Box3D.create(v3(-1.1, -1.1, -1.1), v3(-0.1, -0.1, -0.1));
expect(Frustum3D.intersectsBox3D(f, b1)). toBe(true);
});
it('containsPoint', () => {
const f = Frustum3D();
const m = Mat4.perspective(Mat4(), -1, 1, 1, -1, 1, 100);
Frustum3D.fromProjectionMatrix(f, m);
expect(Frustum3D.containsPoint(f, v3(0, 0, 0))).toBe(false);
expect(Frustum3D.containsPoint(f, v3(0, 0, -50))).toBe(true);
expect(Frustum3D.containsPoint(f, v3(0, 0, -1.001))).toBe(true);
expect(Frustum3D.containsPoint(f, v3(-1, -1, -1.001))).toBe(true);
expect(Frustum3D.containsPoint(f, v3(-1.1, -1.1, -1.001))).toBe(false);
expect(Frustum3D.containsPoint(f, v3(1, 1, -1.001))).toBe(true);
expect(Frustum3D.containsPoint(f, v3(1.1, 1.1, -1.001))).toBe(false);
expect(Frustum3D.containsPoint(f, v3(0, 0, -99.999))).toBe(true);
expect(Frustum3D.containsPoint(f, v3(-99.999, -99.999, -99.999))).toBe(true);
expect(Frustum3D.containsPoint(f, v3(-100.1, -100.1, -100.1))).toBe(false);
expect(Frustum3D.containsPoint(f, v3(99.999, 99.999, -99.999))).toBe(true);
expect(Frustum3D.containsPoint(f, v3(100.1, 100.1, -100.1))).toBe(false);
expect(Frustum3D.containsPoint(f, v3(0, 0, -101))).toBe(false);
});
});

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3 } from '../../linear-algebra';
import { Plane3D } from '../primitives/plane3d';
describe('plane3d', () => {
it('fromNormalAndCoplanarPoint', () => {
const normal = Vec3.create(1, 1, 1);
Vec3.normalize(normal, normal);
const p = Plane3D();
Plane3D.fromNormalAndCoplanarPoint(p, normal, Vec3.zero());
expect(p.normal).toEqual(normal);
expect(p.constant).toBe(-0);
});
it('fromCoplanarPoints', () => {
const a = Vec3.create(2.0, 0.5, 0.25);
const b = Vec3.create(2.0, -0.5, 1.25);
const c = Vec3.create(2.0, -3.5, 2.2);
const p = Plane3D();
Plane3D.fromCoplanarPoints(p, a, b, c);
expect(p.normal).toEqual(Vec3.create(1, 0, 0));
expect(p.constant).toBe(-2);
});
it('distanceToPoint', () => {
const p = Plane3D.create(Vec3.create(2, 0, 0), -2);
Plane3D.normalize(p, p);
expect(Plane3D.distanceToPoint(p, Vec3.create(0, 0, 0))).toBe(-1);
expect(Plane3D.distanceToPoint(p, Vec3.create(4, 0, 0))).toBe(3);
expect(Plane3D.distanceToPoint(p, Plane3D.projectPoint(Vec3(), p, Vec3.zero()))).toBe(0);
});
});

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec2 } from '../../linear-algebra';
import { pointInPolygon } from '../polygon';
describe('pointInPolygon', () => {
it('basic', () => {
const polygon = [
-1, -1,
1, -1,
1, 1,
-1, 1
];
expect(pointInPolygon(Vec2.create(0, 0), polygon, 4)).toBe(true);
expect(pointInPolygon(Vec2.create(2, 2), polygon, 4)).toBe(false);
});
});

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { NumberArray } from '../../mol-util/type-helpers';
import { Vec2 } from '../linear-algebra';
/** raycast along x-axis and apply even-odd rule */
export function pointInPolygon(point: Vec2, polygon: NumberArray, count: number): boolean {
const [x, y] = point;
let inside = false;
for (let i = 0, j = count - 1; i < count; j = i++) {
const xi = polygon[i * 2], yi = polygon[i * 2 + 1];
const xj = polygon[j * 2], yj = polygon[j * 2 + 1];
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
inside = !inside;
}
}
return inside;
}

View File

@@ -5,10 +5,11 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3, Mat4 } from '../../linear-algebra';
import { PositionData } from '../common';
import { OrderedSet } from '../../../mol-data/int';
import { Sphere3D } from './sphere3d';
import { Vec3 } from '../../linear-algebra/3d/vec3';
import { Mat4 } from '../../linear-algebra/3d/mat4';
interface Box3D { min: Vec3, max: Vec3 }
@@ -30,26 +31,48 @@ namespace Box3D {
return copy(zero(), a);
}
const tmpV = Vec3();
/** Get box from sphere, uses extrema if available */
export function fromSphere3D(out: Box3D, sphere: Sphere3D): Box3D {
if (Sphere3D.hasExtrema(sphere) && sphere.extrema.length >= 14) { // 14 extrema with coarse boundary helper
return fromVec3Array(out, sphere.extrema);
}
const r = Vec3.create(sphere.radius, sphere.radius, sphere.radius);
Vec3.sub(out.min, sphere.center, r);
Vec3.add(out.max, sphere.center, r);
Vec3.set(tmpV, sphere.radius, sphere.radius, sphere.radius);
Vec3.sub(out.min, sphere.center, tmpV);
Vec3.add(out.max, sphere.center, tmpV);
return out;
}
/** Get box from sphere, uses extrema if available */
export function fromVec3Array(out: Box3D, array: Vec3[]): Box3D {
Box3D.setEmpty(out);
export function addVec3Array(out: Box3D, array: Vec3[]): Box3D {
for (let i = 0, il = array.length; i < il; i++) {
Box3D.add(out, array[i]);
add(out, array[i]);
}
return out;
}
export function fromVec3Array(out: Box3D, array: Vec3[]): Box3D {
setEmpty(out);
addVec3Array(out, array);
return out;
}
export function addSphere3D(out: Box3D, sphere: Sphere3D): Box3D {
if (Sphere3D.hasExtrema(sphere) && sphere.extrema.length >= 14) { // 14 extrema with coarse boundary helper
return addVec3Array(out, sphere.extrema);
}
add(out, Vec3.subScalar(tmpV, sphere.center, sphere.radius));
add(out, Vec3.addScalar(tmpV, sphere.center, sphere.radius));
return out;
}
export function intersectsSphere3D(box: Box3D, sphere: Sphere3D) {
// Find the point on the AABB closest to the sphere center.
Vec3.clamp(tmpV, sphere.center, box.min, box.max);
// If that point is inside the sphere, the AABB and sphere intersect.
return Vec3.squaredDistance(tmpV, sphere.center) <= (sphere.radius * sphere.radius);
}
export function computeBounding(data: PositionData): Box3D {
const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
@@ -139,7 +162,16 @@ namespace Box3D {
);
}
// const tmpTransformV = Vec3();
export function containsSphere3D(box: Box3D, s: Sphere3D) {
const c = s.center;
const r = s.radius;
return (
c[0] - r < box.min[0] || c[0] + r > box.max[0] ||
c[1] - r < box.min[1] || c[1] + r > box.max[1] ||
c[2] - r < box.min[2] || c[2] + r > box.max[2]
) ? false : true;
}
export function nearestIntersectionWithRay(out: Vec3, box: Box3D, origin: Vec3, dir: Vec3): Vec3 {
const [minX, minY, minZ] = box.min;
const [maxX, maxY, maxZ] = box.max;

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*
* This code has been modified from https://github.com/mrdoob/three.js/,
* copyright (c) 2010-2022 three.js authors. MIT License
*/
import { Mat4 } from '../../linear-algebra/3d/mat4';
import { Vec3 } from '../../linear-algebra/3d/vec3';
import { Box3D } from './box3d';
import { Plane3D } from './plane3d';
import { Sphere3D } from './sphere3d';
interface Frustum3D { 0: Plane3D, 1: Plane3D, 2: Plane3D, 3: Plane3D, 4: Plane3D, 5: Plane3D; length: 6; }
function Frustum3D() {
return Frustum3D.create(Plane3D(), Plane3D(), Plane3D(), Plane3D(), Plane3D(), Plane3D());
}
namespace Frustum3D {
export const enum PlaneIndex {
Right = 0,
Left = 1,
Bottom = 2,
Top = 3,
Far = 4,
Near = 5,
};
export function create(right: Plane3D, left: Plane3D, bottom: Plane3D, top: Plane3D, far: Plane3D, near: Plane3D): Frustum3D {
return [right, left, bottom, top, far, near];
}
export function copy(out: Frustum3D, f: Frustum3D): Frustum3D {
for (let i = 0 as PlaneIndex; i < 6; ++i) Plane3D.copy(out[i], f[i]);
return out;
}
export function clone(f: Frustum3D): Frustum3D {
return copy(Frustum3D(), f);
}
export function fromProjectionMatrix(out: Frustum3D, m: Mat4) {
const a00 = m[0], a01 = m[1], a02 = m[2], a03 = m[3];
const a10 = m[4], a11 = m[5], a12 = m[6], a13 = m[7];
const a20 = m[8], a21 = m[9], a22 = m[10], a23 = m[11];
const a30 = m[12], a31 = m[13], a32 = m[14], a33 = m[15];
Plane3D.setUnnormalized(out[0], a03 - a00, a13 - a10, a23 - a20, a33 - a30);
Plane3D.setUnnormalized(out[1], a03 + a00, a13 + a10, a23 + a20, a33 + a30);
Plane3D.setUnnormalized(out[2], a03 + a01, a13 + a11, a23 + a21, a33 + a31);
Plane3D.setUnnormalized(out[3], a03 - a01, a13 - a11, a23 - a21, a33 - a31);
Plane3D.setUnnormalized(out[4], a03 - a02, a13 - a12, a23 - a22, a33 - a32);
Plane3D.setUnnormalized(out[5], a03 + a02, a13 + a12, a23 + a22, a33 + a32);
return out;
}
export function intersectsSphere3D(frustum: Frustum3D, sphere: Sphere3D) {
const center = sphere.center;
const negRadius = -sphere.radius;
for (let i = 0 as PlaneIndex; i < 6; ++i) {
const distance = Plane3D.distanceToPoint(frustum[i], center);
if (distance < negRadius) return false;
}
return true;
}
const boxTmpV = Vec3();
export function intersectsBox3D(frustum: Frustum3D, box: Box3D) {
for (let i = 0 as PlaneIndex; i < 6; ++i) {
const plane = frustum[i];
// corner at max distance
boxTmpV[0] = plane.normal[0] > 0 ? box.max[0] : box.min[0];
boxTmpV[1] = plane.normal[1] > 0 ? box.max[1] : box.min[1];
boxTmpV[2] = plane.normal[2] > 0 ? box.max[2] : box.min[2];
if (Plane3D.distanceToPoint(plane, boxTmpV) < 0) {
return false;
}
}
return true;
}
export function containsPoint(frustum: Frustum3D, point: Vec3) {
for (let i = 0 as PlaneIndex; i < 6; ++i) {
if (Plane3D.distanceToPoint(frustum[i], point) < 0) {
return false;
}
}
return true;
}
}
export { Frustum3D };

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*
* This code has been modified from https://github.com/mrdoob/three.js/,
* copyright (c) 2010-2022 three.js authors. MIT License
*/
import { NumberArray } from '../../../mol-util/type-helpers';
import { Vec3 } from '../../linear-algebra/3d/vec3';
import { Sphere3D } from './sphere3d';
interface Plane3D { normal: Vec3, constant: number }
function Plane3D() {
return Plane3D.create(Vec3.create(1, 0, 0), 0);
}
namespace Plane3D {
export function create(normal: Vec3, constant: number): Plane3D { return { normal, constant }; }
export function copy(out: Plane3D, p: Plane3D): Plane3D {
Vec3.copy(out.normal, p.normal);
out.constant = p.constant;
return out;
}
export function clone(p: Plane3D): Plane3D {
return copy(Plane3D(), p);
}
export function normalize(out: Plane3D, p: Plane3D): Plane3D {
// Note: will lead to a divide by zero if the plane is invalid.
const inverseNormalLength = 1.0 / Vec3.magnitude(p.normal);
Vec3.scale(out.normal, p.normal, inverseNormalLength);
out.constant = p.constant * inverseNormalLength;
return out;
}
export function negate(out: Plane3D, p: Plane3D): Plane3D {
Vec3.negate(out.normal, p.normal);
out.constant = -p.constant;
return out;
}
export function toArray<T extends NumberArray>(p: Plane3D, out: T, offset: number) {
Vec3.toArray(p.normal, out, offset);
out[offset + 3] = p.constant;
return out;
}
export function fromArray(out: Plane3D, array: NumberArray, offset: number) {
Vec3.fromArray(out.normal, array, offset);
out.constant = array[offset + 3];
return out;
}
export function fromNormalAndCoplanarPoint(out: Plane3D, normal: Vec3, point: Vec3) {
Vec3.copy(out.normal, normal);
out.constant = -Vec3.dot(out.normal, point);
return out;
}
export function fromCoplanarPoints(out: Plane3D, a: Vec3, b: Vec3, c: Vec3) {
const normal = Vec3.triangleNormal(Vec3(), a, b, c);
fromNormalAndCoplanarPoint(out, normal, a);
return out;
}
const unnormTmpV = Vec3();
export function setUnnormalized(out: Plane3D, nx: number, ny: number, nz: number, constant: number) {
Vec3.set(unnormTmpV, nx, ny, nz);
const inverseNormalLength = 1.0 / Vec3.magnitude(unnormTmpV);
Vec3.scale(out.normal, unnormTmpV, inverseNormalLength);
out.constant = constant * inverseNormalLength;
return out;
}
export function distanceToPoint(plane: Plane3D, point: Vec3) {
return Vec3.dot(plane.normal, point) + plane.constant;
}
export function distanceToSpher3D(plane: Plane3D, sphere: Sphere3D) {
return distanceToPoint(plane, sphere.center) - sphere.radius;
}
export function projectPoint(out: Vec3, plane: Plane3D, point: Vec3) {
return Vec3.scaleAndAdd(out, out, plane.normal, -distanceToPoint(plane, point));
}
}
export { Plane3D };

View File

@@ -109,9 +109,10 @@ namespace Sphere3D {
return out;
}
export function toArray(s: Sphere3D, out: NumberArray, offset: number) {
export function toArray<T extends NumberArray>(s: Sphere3D, out: T, offset: number) {
Vec3.toArray(s.center, out, offset);
out[offset + 3] = s.radius;
return out;
}
export function fromArray(out: Sphere3D, array: NumberArray, offset: number) {

View File

@@ -64,7 +64,7 @@ namespace Mat3 {
return mat;
}
export function toArray(a: Mat3, out: NumberArray, offset: number) {
export function toArray<T extends NumberArray>(a: Mat3, out: T, offset: number) {
out[offset + 0] = a[0];
out[offset + 1] = a[1];
out[offset + 2] = a[2];
@@ -454,6 +454,14 @@ namespace Mat3 {
}
export const Identity: ReadonlyMat3 = identity();
/** Return the Frobenius inner product of two matrices (= dot product of the flattened matrices).
* Can be used as a measure of similarity between two rotation matrices. */
export function innerProduct(a: Mat3, b: Mat3) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
+ a[3] * b[3] + a[4] * b[4] + a[5] * b[5]
+ a[6] * b[6] + a[7] * b[7] + a[8] * b[8];
}
}
export { Mat3 };

View File

@@ -124,7 +124,7 @@ namespace Mat4 {
return a[4 * j + i];
}
export function toArray(a: Mat4, out: NumberArray, offset: number) {
export function toArray<T extends NumberArray>(a: Mat4, out: T, offset: number) {
out[offset + 0] = a[0];
out[offset + 1] = a[1];
out[offset + 2] = a[2];

View File

@@ -314,7 +314,7 @@ namespace Quat {
return out;
}
export function toArray(a: Quat, out: NumberArray, offset: number) {
export function toArray<T extends NumberArray>(a: Quat, out: T, offset: number) {
out[offset + 0] = a[0];
out[offset + 1] = a[1];
out[offset + 2] = a[2];

View File

@@ -50,7 +50,7 @@ namespace Vec2 {
return isNaN(a[0]) || isNaN(a[1]);
}
export function toArray(a: Vec2, out: NumberArray, offset: number) {
export function toArray<T extends NumberArray>(a: Vec2, out: T, offset: number) {
out[offset + 0] = a[0];
out[offset + 1] = a[1];
return out;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2021 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>
@@ -18,7 +18,7 @@
*/
import { Mat4 } from './mat4';
import { spline as _spline, quadraticBezier as _quadraticBezier, clamp } from '../../interpolate';
import { spline as _spline, quadraticBezier as _quadraticBezier, clamp as _clamp } from '../../interpolate';
import { NumberArray } from '../../../mol-util/type-helpers';
import { Mat3 } from './mat3';
import { Quat } from './quat';
@@ -74,7 +74,7 @@ namespace Vec3 {
return v;
}
export function toArray(v: Vec3, out: NumberArray, offset: number) {
export function toArray<T extends NumberArray>(v: Vec3, out: T, offset: number) {
out[offset + 0] = v[0];
out[offset + 1] = v[1];
out[offset + 2] = v[2];
@@ -246,6 +246,16 @@ namespace Vec3 {
return out;
}
/**
* Assumes min < max, componentwise
*/
export function clamp(out: Vec3, a: Vec3, min: Vec3, max: Vec3) {
out[0] = Math.max(min[0], Math.min(max[0], a[0]));
out[1] = Math.max(min[1], Math.min(max[1], a[1]));
out[2] = Math.max(min[2], Math.min(max[2], a[2]));
return out;
}
export function distance(a: Vec3, b: Vec3) {
const x = b[0] - a[0],
y = b[1] - a[1],
@@ -341,7 +351,7 @@ namespace Vec3 {
const slerpRelVec = zero();
export function slerp(out: Vec3, a: Vec3, b: Vec3, t: number) {
const d = clamp(dot(a, b), -1, 1);
const d = _clamp(dot(a, b), -1, 1);
const theta = Math.acos(d) * t;
scaleAndAdd(slerpRelVec, b, a, -d);
normalize(slerpRelVec, slerpRelVec);
@@ -429,6 +439,14 @@ namespace Vec3 {
return out;
}
export function transformDirection(out: Vec3, a: Vec3, m: Mat4) {
const x = a[0], y = a[1], z = a[2];
out[0] = m[0] * x + m[4] * y + m[8] * z;
out[1] = m[1] * x + m[5] * y + m[9] * z;
out[2] = m[2] * x + m[6] * y + m[10] * z;
return normalize(out, out);
}
/**
* Like `transformMat4` but with offsets into arrays
*/
@@ -477,7 +495,7 @@ namespace Vec3 {
const denominator = Math.sqrt(squaredMagnitude(a) * squaredMagnitude(b));
if (denominator === 0) return Math.PI / 2;
const theta = dot(a, b) / denominator;
return Math.acos(clamp(theta, -1, 1)); // clamp to avoid numerical problems
return Math.acos(_clamp(theta, -1, 1)); // clamp to avoid numerical problems
}
const tmp_dh_ab = zero();

View File

@@ -70,7 +70,7 @@ namespace Vec4 {
return isNaN(a[0]) || isNaN(a[1]) || isNaN(a[2]) || isNaN(a[3]);
}
export function toArray(a: Vec4, out: NumberArray, offset: number) {
export function toArray<T extends NumberArray>(a: Vec4, out: T, offset: number) {
out[offset + 0] = a[0];
out[offset + 1] = a[1];
out[offset + 2] = a[2];

View File

@@ -27,7 +27,7 @@ namespace PrincipalAxes {
export function calculateMomentsAxes(positions: NumberArray): Axes3D {
if (positions.length === 3) {
return Axes3D.create(Vec3.fromArray(Vec3(), positions, 0), Vec3.create(1, 0, 0), Vec3.create(0, 1, 0), Vec3.create(0, 1, 0));
return Axes3D.create(Vec3.fromArray(Vec3(), positions, 0), Vec3.create(1, 0, 0), Vec3.create(0, 1, 0), Vec3.create(0, 0, 1));
}
const points = Matrix.fromArray(positions, 3, positions.length / 3);
@@ -143,4 +143,4 @@ namespace PrincipalAxes {
return Axes3D.create(origin, dirA, dirB, dirC);
}
}
}

View File

@@ -44,7 +44,7 @@ function validPair(structure: Structure, infoA: Features.Info, infoB: Features.I
const altA = altLoc(infoA.unit, indexA);
const altB = altLoc(infoB.unit, indexB);
if (altA && altB && altA !== altB) return false; // incompatible alternate location id
if (infoA.unit.residueIndex[infoA.unit.elements[indexA]] === infoB.unit.residueIndex[infoB.unit.elements[indexB]] && infoA.unit === infoB.unit) return false; // same residue
if (infoA.unit === infoB.unit && infoA.unit.model.atomicHierarchy.residueAtomSegments.count > 1 && infoA.unit.residueIndex[infoA.unit.elements[indexA]] === infoB.unit.residueIndex[infoB.unit.elements[indexB]]) return false; // same residue (and more than one residue)
// e.g. no hbond if donor and acceptor are bonded
if (connectedTo(structure, infoA.unit, indexA, infoB.unit, indexB)) return false;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2021 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 David Sehnal <david.sehnal@gmail.com>
@@ -99,7 +99,7 @@ namespace Coordinates {
/**
* Only use ordering if it's not identity.
*/
export function getAtomicConformation(frame: Frame, atomId: Column<number>, ordering?: ArrayLike<number>): AtomicConformation {
export function getAtomicConformation(frame: Frame, fields: { atomId: Column<number>, occupancy?: Column<number>, B_iso_or_equiv?: Column<number> }, ordering?: ArrayLike<number>): AtomicConformation {
let { x, y, z } = frame;
if (frame.xyzOrdering.frozen) {
@@ -143,9 +143,9 @@ namespace Coordinates {
return {
id: UUID.create22(),
atomId,
occupancy: Column.ofConst(1, frame.elementCount, Column.Schema.int),
B_iso_or_equiv: Column.ofConst(0, frame.elementCount, Column.Schema.float),
atomId: fields.atomId,
occupancy: fields.occupancy ?? Column.ofConst(1, frame.elementCount, Column.Schema.int),
B_iso_or_equiv: fields.B_iso_or_equiv ?? Column.ofConst(0, frame.elementCount, Column.Schema.float),
xyzDefined: true,
x,
y,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2023 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>
@@ -112,7 +112,11 @@ export namespace Model {
...model,
id: UUID.create22(),
modelNum: i,
atomicConformation: Coordinates.getAtomicConformation(f, model.atomicConformation.atomId, srcIndexArray),
atomicConformation: Coordinates.getAtomicConformation(f, {
atomId: model.atomicConformation.atomId,
occupancy: model.atomicConformation.occupancy,
B_iso_or_equiv: model.atomicConformation.B_iso_or_equiv
}, srcIndexArray),
// TODO: add support for supplying sphere and gaussian coordinates in addition to atomic coordinates?
// coarseConformation: coarse.conformation,
customProperties: new CustomProperties(),

View File

@@ -55,13 +55,17 @@ namespace Location {
return a.unit === b.unit && a.element === b.element;
}
const pA = Vec3.zero(), pB = Vec3.zero();
const pA = Vec3(), pB = Vec3();
export function distance(a: Location, b: Location) {
a.unit.conformation.position(a.element, pA);
b.unit.conformation.position(b.element, pB);
return Vec3.distance(pA, pB);
}
export function position(out: Vec3, l: Location): Vec3 {
return l.unit.conformation.position(l.element, out);
}
export function residueIndex(l: Location) {
return l.unit.model.atomicHierarchy.residueAtomSegments.index[l.element];
}

View File

@@ -148,7 +148,7 @@ export namespace Loci {
* The loc argument of the callback is mutable, use Location.clone() if you intend to keep
* the value around.
*/
export function forEachLocation(loci: Loci, f: (loc: Location) => any) {
export function forEachLocation(loci: Loci, f: (loc: Location) => void) {
if (Loci.isEmpty(loci)) return;
const location = Location.create(loci.structure);

View File

@@ -8,13 +8,13 @@ import { PluginContext } from '../../mol-plugin/context';
import { StateAction } from '../../mol-state';
import { Task } from '../../mol-task';
import { Asset } from '../../mol-util/assets';
import { getFileInfo } from '../../mol-util/file-info';
import { getFileNameInfo } from '../../mol-util/file-info';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { unzip } from '../../mol-util/zip/zip';
import { PluginStateObject } from '../objects';
async function processFile(file: Asset.File, plugin: PluginContext, format: string, visuals: boolean) {
const info = getFileInfo(file.file!);
const info = getFileNameInfo(file.file?.name ?? '');
const isBinary = plugin.dataFormats.binaryExtensions.has(info.ext);
const { data } = await plugin.builders.data.readFile({ file, isBinary });
const provider = format === 'auto'
@@ -111,8 +111,8 @@ export const DownloadFile = StateAction.build({
}
} else {
const url = Asset.getUrl(params.url);
const info = getFileInfo(url);
await processFile(Asset.File(new File([data.obj?.data as Uint8Array], info.name)), plugin, 'auto', params.visuals);
const fileName = getFileNameInfo(url).name;
await processFile(Asset.File(new File([data.obj?.data as Uint8Array], fileName)), plugin, 'auto', params.visuals);
}
} else {
const provider = plugin.dataFormats.get(params.format);

View File

@@ -18,7 +18,7 @@ import { Download } from '../transforms/data';
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, TrajectoryFromModelAndCoordinates } from '../transforms/model';
import { Asset } from '../../mol-util/assets';
import { PluginConfig } from '../../mol-plugin/config';
import { getFileInfo } from '../../mol-util/file-info';
import { getFileNameInfo } from '../../mol-util/file-info';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { TopologyFormatCategory } from '../formats/topology';
import { CoordinatesFormatCategory } from '../formats/coordinates';
@@ -184,7 +184,7 @@ const DownloadStructure = StateAction.build({
for (const download of downloadParams) {
const data = await plugin.builders.data.download(download, { state: { isGhost: true } });
const provider = format === 'auto'
? plugin.dataFormats.auto(getFileInfo(Asset.getUrl(download.url)), data.cell?.obj!)
? plugin.dataFormats.auto(getFileNameInfo(Asset.getUrl(download.url)), data.cell?.obj!)
: plugin.dataFormats.get(format);
if (!provider) throw new Error('unknown file format');
const trajectory = await plugin.builders.structure.parseTrajectory(data, provider);
@@ -385,7 +385,7 @@ export const LoadTrajectory = StateAction.build({
const processFile = async (file: Asset.File | null) => {
if (!file) throw new Error('No file selected');
const info = getFileInfo(file.file!);
const info = getFileNameInfo(file.file?.name ?? '');
const isBinary = ctx.dataFormats.binaryExtensions.has(info.ext);
const { data } = await ctx.builders.data.readFile({ file, isBinary });
const provider = ctx.dataFormats.auto(info, data.cell?.obj!);

View File

@@ -8,7 +8,7 @@
import { PluginContext } from '../../mol-plugin/context';
import { StateAction, StateTransformer, StateSelection } from '../../mol-state';
import { Task } from '../../mol-task';
import { getFileInfo } from '../../mol-util/file-info';
import { getFileNameInfo } from '../../mol-util/file-info';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { PluginStateObject } from '../objects';
import { Download } from '../transforms/data';
@@ -119,7 +119,7 @@ const DownloadDensity = StateAction.build({
switch (src.name) {
case 'url':
downloadParams = src.params;
provider = src.params.format === 'auto' ? plugin.dataFormats.auto(getFileInfo(Asset.getUrl(downloadParams.url)), data.cell?.obj!) : plugin.dataFormats.get(src.params.format);
provider = src.params.format === 'auto' ? plugin.dataFormats.auto(getFileNameInfo(Asset.getUrl(downloadParams.url)), data.cell?.obj!) : plugin.dataFormats.get(src.params.format);
break;
case 'pdb-xray':
entryId = src.params.provider.id;

View File

@@ -7,7 +7,7 @@
import { StateTransformer, StateTransform } from '../../mol-state';
import { PluginContext } from '../../mol-plugin/context';
import { Download, ReadFile, DownloadBlob, RawData } from '../transforms/data';
import { getFileInfo } from '../../mol-util/file-info';
import { getFileNameInfo } from '../../mol-util/file-info';
export class DataBuilder {
private get dataState() {
@@ -31,7 +31,7 @@ export class DataBuilder {
async readFile(params: StateTransformer.Params<ReadFile>, options?: Partial<StateTransform.Options>) {
const data = await this.dataState.build().toRoot().apply(ReadFile, params, options).commit({ revertOnError: true });
const fileInfo = getFileInfo(params.file?.file || '');
const fileInfo = getFileNameInfo(params.file?.file?.name ?? '');
return { data: data, fileInfo };
}

View File

@@ -8,7 +8,7 @@
import { decodeMsgPack } from '../../mol-io/common/msgpack/decode';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectRef } from '../../mol-state';
import { FileInfo } from '../../mol-util/file-info';
import { FileNameInfo } from '../../mol-util/file-info';
import { PluginStateObject } from '../objects';
export interface DataFormatProvider<P = any, R = any, V = any> {
@@ -17,7 +17,7 @@ export interface DataFormatProvider<P = any, R = any, V = any> {
category?: string,
stringExtensions?: string[],
binaryExtensions?: string[],
isApplicable?(info: FileInfo, data: string | Uint8Array): boolean,
isApplicable?(info: FileNameInfo, data: string | Uint8Array): boolean,
parse(plugin: PluginContext, data: StateObjectRef<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, params?: P): Promise<R>,
visuals?(plugin: PluginContext, data: R): Promise<V> | undefined
}
@@ -25,7 +25,7 @@ export interface DataFormatProvider<P = any, R = any, V = any> {
export function DataFormatProvider<P extends DataFormatProvider>(provider: P): P { return provider; }
type cifVariants = 'dscif' | 'segcif' | 'coreCif' | -1
export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
export function guessCifVariant(info: FileNameInfo, data: Uint8Array | string): cifVariants {
if (info.ext === 'bcif') {
try {
// TODO: find a way to run msgpackDecode only once

View File

@@ -5,7 +5,7 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { FileInfo } from '../../mol-util/file-info';
import { FileNameInfo } from '../../mol-util/file-info';
import { PluginStateObject } from '../objects';
import { DataFormatProvider } from './provider';
import { BuiltInTrajectoryFormats } from './trajectory';
@@ -78,7 +78,7 @@ export class DataFormatRegistry {
this._map.delete(name);
}
auto(info: FileInfo, dataStateObject: PluginStateObject.Data.Binary | PluginStateObject.Data.String) {
auto(info: FileNameInfo, dataStateObject: PluginStateObject.Data.Binary | PluginStateObject.Data.String) {
for (let i = 0, il = this.list.length; i < il; ++i) {
const p = this._list[i].provider;

View File

@@ -4,18 +4,22 @@
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ke Ma <mark.ma@rcsb.org>
* @author Adam Midlik <midlik@gmail.com>
*/
import { Sphere3D } from '../../mol-math/geometry';
import { PluginContext } from '../../mol-plugin/context';
import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
import { Camera } from '../../mol-canvas3d/camera';
import { Loci } from '../../mol-model/loci';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { StructureElement } from '../../mol-model/structure';
import { Sphere3D } from '../../mol-math/geometry';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
import { Mat3 } from '../../mol-math/linear-algebra';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
import { Loci } from '../../mol-model/loci';
import { Structure, StructureElement } from '../../mol-model/structure';
import { PluginContext } from '../../mol-plugin/context';
import { PluginStateObject } from '../objects';
import { pcaFocus } from './focus-camera/focus-first-residue';
import { changeCameraRotation, structureLayingTransform } from './focus-camera/orient-axes';
// TODO: make this customizable somewhere?
const DefaultCameraFocusOptions = {
@@ -125,6 +129,26 @@ export class CameraManager {
}
}
/** Align PCA axes of `structures` (default: all loaded structures) to the screen axes. */
orientAxes(structures?: Structure[], durationMs?: number) {
if (!this.plugin.canvas3d) return;
if (!structures) {
const structCells = this.plugin.state.data.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure));
const rootStructCells = structCells.filter(cell => cell.obj && !cell.transform.transformer.definition.isDecorator && !cell.obj.data.parent);
structures = rootStructCells.map(cell => cell.obj?.data).filter(struct => !!struct) as Structure[];
}
const { rotation } = structureLayingTransform(structures);
const newSnapshot = changeCameraRotation(this.plugin.canvas3d.camera.getSnapshot(), rotation);
this.setSnapshot(newSnapshot, durationMs);
}
/** Align Cartesian axes to the screen axes (X right, Y up). */
resetAxes(durationMs?: number) {
if (!this.plugin.canvas3d) return;
const newSnapshot = changeCameraRotation(this.plugin.canvas3d.camera.getSnapshot(), Mat3.Identity);
this.setSnapshot(newSnapshot, durationMs);
}
setSnapshot(snapshot: Partial<Camera.Snapshot>, durationMs?: number) {
// TODO: setState and requestCameraReset are very similar now: unify them?
this.plugin.canvas3d?.requestCameraReset({ snapshot, durationMs });

View File

@@ -0,0 +1,218 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Camera } from '../../../mol-canvas3d/camera';
import { Mat3, Vec3 } from '../../../mol-math/linear-algebra';
import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
/** Minimum number of atoms necessary for running PCA.
* If enough atoms cannot be selected, XYZ axes will be used instead of PCA axes. */
const MIN_ATOMS_FOR_PCA = 3;
/** Rotation matrices for the basic rotations by 90 degrees */
export const ROTATION_MATRICES = {
// The order of elements in the matrices in column-wise (F-style)
identity: Mat3.create(1, 0, 0, 0, 1, 0, 0, 0, 1),
rotX90: Mat3.create(1, 0, 0, 0, 0, 1, 0, -1, 0),
rotY90: Mat3.create(0, 0, -1, 0, 1, 0, 1, 0, 0),
rotZ90: Mat3.create(0, 1, 0, -1, 0, 0, 0, 0, 1),
rotX270: Mat3.create(1, 0, 0, 0, 0, -1, 0, 1, 0),
rotY270: Mat3.create(0, 0, 1, 0, 1, 0, -1, 0, 0),
rotZ270: Mat3.create(0, -1, 0, 1, 0, 0, 0, 0, 1),
rotX180: Mat3.create(1, 0, 0, 0, -1, 0, 0, 0, -1),
rotY180: Mat3.create(-1, 0, 0, 0, 1, 0, 0, 0, -1),
rotZ180: Mat3.create(-1, 0, 0, 0, -1, 0, 0, 0, 1),
};
/** Return transformation which will align the PCA axes of an atomic structure
* (or multiple structures) to the Cartesian axes x, y, z
* (transformed = rotation * (coords - origin)).
*
* There are always 4 equally good rotations to do this (4 flips).
* If `referenceRotation` is provided, select the one nearest to `referenceRotation`.
* Otherwise use arbitrary rules to ensure the orientation after transform does not depend on the original orientation.
*/
export function structureLayingTransform(structures: Structure[], referenceRotation?: Mat3): { rotation: Mat3, origin: Vec3 } {
const coords = smartSelectCoords(structures, MIN_ATOMS_FOR_PCA);
return layingTransform(coords, referenceRotation);
}
/** Return transformation which will align the PCA axes of a sequence
* of points to the Cartesian axes x, y, z
* (transformed = rotation * (coords - origin)).
*
* `coords` is a flattened array of 3D coordinates (i.e. the first 3 values are x, y, and z of the first point etc.).
*
* There are always 4 equally good rotations to do this (4 flips).
* If `referenceRotation` is provided, select the one nearest to `referenceRotation`.
* Otherwise use arbitrary rules to ensure the orientation after transform does not depend on the original orientation.
*/
export function layingTransform(coords: number[], referenceRotation?: Mat3): { rotation: Mat3, origin: Vec3 } {
if (coords.length === 0) {
console.warn('Skipping PCA, no atoms');
return { rotation: ROTATION_MATRICES.identity, origin: Vec3.zero() };
}
const axes = PrincipalAxes.calculateMomentsAxes(coords);
const normAxes = PrincipalAxes.calculateNormalizedAxes(axes);
const R = mat3FromRows(normAxes.dirA, normAxes.dirB, normAxes.dirC);
avoidMirrorRotation(R); // The SVD implementation seems to always provide proper rotation, but just to be sure
const flip = referenceRotation ? minimalFlip(R, referenceRotation) : canonicalFlip(coords, R, axes.origin);
Mat3.mul(R, flip, R);
return { rotation: R, origin: normAxes.origin };
}
/** Try these selection strategies until having at least `minAtoms` atoms:
* 1. only trace atoms (e.g. C-alpha and O3')
* 2. all non-hydrogen atoms with exception of water (HOH)
* 3. all atoms
* Return the coordinates in a flattened array (in triples).
* If the total number of atoms is less than `minAtoms`, return only those. */
function smartSelectCoords(structures: Structure[], minAtoms: number): number[] {
let coords: number[];
coords = selectCoords(structures, { onlyTrace: true });
if (coords.length >= 3 * minAtoms) return coords;
coords = selectCoords(structures, { skipHydrogens: true, skipWater: true });
if (coords.length >= 3 * minAtoms) return coords;
coords = selectCoords(structures, {});
return coords;
}
/** Select coordinates of atoms in `structures` as a flattened array (in triples).
* If `onlyTrace`, include only trace atoms (CA, O3');
* if `skipHydrogens`, skip all hydrogen atoms;
* if `skipWater`, skip all water residues. */
function selectCoords(structures: Structure[], options: { onlyTrace?: boolean, skipHydrogens?: boolean, skipWater?: boolean }): number[] {
const { onlyTrace, skipHydrogens, skipWater } = options;
const { x, y, z, type_symbol, label_comp_id } = StructureProperties.atom;
const coords: number[] = [];
for (const struct of structures) {
const loc = StructureElement.Location.create(struct);
for (const unit of struct.units) {
loc.unit = unit;
const elements = onlyTrace ? unit.polymerElements : unit.elements;
for (let i = 0; i < elements.length; i++) {
loc.element = elements[i];
if (skipHydrogens && type_symbol(loc) === 'H') continue;
if (skipWater && label_comp_id(loc) === 'HOH') continue;
coords.push(x(loc), y(loc), z(loc));
}
}
}
return coords;
}
/** Return a flip around XYZ axes which minimizes the difference between flip*rotation and referenceRotation. */
function minimalFlip(rotation: Mat3, referenceRotation: Mat3): Mat3 {
let bestFlip = ROTATION_MATRICES.identity;
let bestScore = 0; // there will always be at least one positive score
const aux = Mat3();
for (const flip of [ROTATION_MATRICES.identity, ROTATION_MATRICES.rotX180, ROTATION_MATRICES.rotY180, ROTATION_MATRICES.rotZ180]) {
const score = Mat3.innerProduct(Mat3.mul(aux, flip, rotation), referenceRotation);
if (score > bestScore) {
bestFlip = flip;
bestScore = score;
}
}
return bestFlip;
}
/** Return a rotation matrix (flip) that should be applied to `coords` (after being rotated by `rotation`)
* to ensure a deterministic "canonical" rotation.
* There are 4 flips to choose from (one identity and three 180-degree rotations around the X, Y, and Z axes).
* One of these 4 possible results is selected so that:
* 1) starting and ending coordinates tend to be more in front (z > 0), middle more behind (z < 0).
* 2) starting coordinates tend to be more left-top (x < y), ending more right-bottom (x > y).
* These rules are arbitrary, but try to avoid ties for at least some basic symmetries.
* Provided `origin` parameter MUST be the mean of the coordinates, otherwise it will not work!
*/
function canonicalFlip(coords: number[], rotation: Mat3, origin: Vec3): Mat3 {
const pcaX = Vec3.create(Mat3.getValue(rotation, 0, 0), Mat3.getValue(rotation, 0, 1), Mat3.getValue(rotation, 0, 2));
const pcaY = Vec3.create(Mat3.getValue(rotation, 1, 0), Mat3.getValue(rotation, 1, 1), Mat3.getValue(rotation, 1, 2));
const pcaZ = Vec3.create(Mat3.getValue(rotation, 2, 0), Mat3.getValue(rotation, 2, 1), Mat3.getValue(rotation, 2, 2));
const n = Math.floor(coords.length / 3);
const v = Vec3();
let xCum = 0;
let yCum = 0;
let zCum = 0;
for (let i = 0; i < n; i++) {
Vec3.fromArray(v, coords, 3 * i);
Vec3.sub(v, v, origin);
xCum += i * Vec3.dot(v, pcaX);
yCum += i * Vec3.dot(v, pcaY);
zCum += veeSlope(i, n) * Vec3.dot(v, pcaZ);
// Thanks to subtracting `origin` from `coords` the slope functions `i` and `veeSlope(i, n)`
// don't have to have zero sum (can be shifted up or down):
// sum{(slope[i]+shift)*(coords[i]-origin).PCA} =
// = sum{slope[i]*coords[i].PCA - slope[i]*origin.PCA + shift*coords[i].PCA - shift*origin.PCA} =
// = sum{slope[i]*(coords[i]-origin).PCA} + shift*sum{coords[i]-origin}.PCA =
// = sum{slope[i]*(coords[i]-origin).PCA}
}
const wrongFrontBack = zCum < 0;
const wrongLeftTopRightBottom = wrongFrontBack ? xCum + yCum < 0 : xCum - yCum < 0;
if (wrongLeftTopRightBottom && wrongFrontBack) {
return ROTATION_MATRICES.rotY180; // flip around Y = around X then Z
} else if (wrongFrontBack) {
return ROTATION_MATRICES.rotX180; // flip around X
} else if (wrongLeftTopRightBottom) {
return ROTATION_MATRICES.rotZ180; // flip around Z
} else {
return ROTATION_MATRICES.identity; // do not flip
}
}
/** Auxiliary function defined for i in [0, n), linearly decreasing from 0 to n/2
* and then increasing back from n/2 to n, resembling letter V. */
function veeSlope(i: number, n: number) {
const mid = Math.floor(n / 2);
if (i < mid) {
if (n % 2) return mid - i;
else return mid - i - 1;
} else {
return i - mid;
}
}
function mat3FromRows(row0: Vec3, row1: Vec3, row2: Vec3): Mat3 {
const m = Mat3();
Mat3.setValue(m, 0, 0, row0[0]);
Mat3.setValue(m, 0, 1, row0[1]);
Mat3.setValue(m, 0, 2, row0[2]);
Mat3.setValue(m, 1, 0, row1[0]);
Mat3.setValue(m, 1, 1, row1[1]);
Mat3.setValue(m, 1, 2, row1[2]);
Mat3.setValue(m, 2, 0, row2[0]);
Mat3.setValue(m, 2, 1, row2[1]);
Mat3.setValue(m, 2, 2, row2[2]);
return m;
}
/** Check if a rotation matrix includes mirroring and invert Z axis in such case, to ensure a proper rotation (in-place). */
function avoidMirrorRotation(rot: Mat3) {
if (Mat3.determinant(rot) < 0) {
Mat3.setValue(rot, 2, 0, -Mat3.getValue(rot, 2, 0));
Mat3.setValue(rot, 2, 1, -Mat3.getValue(rot, 2, 1));
Mat3.setValue(rot, 2, 2, -Mat3.getValue(rot, 2, 2));
}
}
/** Return a new camera snapshot with the same target and camera distance from the target as `old`
* but with diferent orientation.
* The actual rotation applied to the camera is the inverse of `rotation`,
* which creates the same effect as if `rotation` were applied to the whole scene without moving the camera.
* The rotation is relative to the default camera orientation (not to the current orientation). */
export function changeCameraRotation(old: Camera.Snapshot, rotation: Mat3): Camera.Snapshot {
const cameraRotation = Mat3.invert(Mat3(), rotation);
const dist = Vec3.distance(old.position, old.target);
const relPosition = Vec3.transformMat3(Vec3(), Vec3.create(0, 0, dist), cameraRotation);
const newUp = Vec3.transformMat3(Vec3(), Vec3.create(0, 1, 0), cameraRotation);
const newPosition = Vec3.add(Vec3(), old.target, relPosition);
return { ...old, position: newPosition, up: newUp };
}

View File

@@ -11,7 +11,8 @@ import { Representation } from '../../mol-repr/representation';
import { MarkerAction } from '../../mol-util/marker-action';
import { arrayRemoveAtInPlace } from '../../mol-util/array';
export type LociLabel = JSX.Element | string
// any represents React element. For compatibility to including the type
export type LociLabel = string | any
export type LociLabelProvider = {
label: (loci: Loci, repr?: Representation<any>) => LociLabel | undefined
group?: (entry: LociLabel) => string

View File

@@ -282,7 +282,7 @@ const ParseCif = PluginStateTransform.BuiltIn({
})({
apply({ a }) {
return Task.create('Parse CIF', async ctx => {
const parsed = await (SO.Data.String.is(a) ? CIF.parse(a.data) : CIF.parseBinary(a.data)).runInContext(ctx);
const parsed = await (typeof a.data === 'string' ? CIF.parse(a.data) : CIF.parseBinary(a.data)).runInContext(ctx);
if (parsed.isError) throw new Error(parsed.message);
return new SO.Format.Cif(parsed.result);
});

View File

@@ -272,25 +272,42 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
params(a) {
if (!a) {
return {
blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
loadAllBlocks: PD.Optional(PD.Boolean(false, { description: 'If True, ignore Block Header parameter and parse all datablocks into a single trajectory.' })),
blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.', hideIf: p => p.loadAllBlocks === true })),
};
}
const { blocks } = a.data;
return {
blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
loadAllBlocks: PD.Optional(PD.Boolean(false, { description: 'If True, ignore Block Header parameter and parse all data blocks into a single trajectory.' })),
blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse', hideIf: p => p.loadAllBlocks === true })),
};
}
})({
isApplicable: a => a.data.blocks.length > 0,
apply({ a, params }) {
return Task.create('Parse mmCIF', async ctx => {
const header = params.blockHeader || a.data.blocks[0].header;
const block = a.data.blocks.find(b => b.header === header);
if (!block) throw new Error(`Data block '${[header]}' not found.`);
const models = await trajectoryFromMmCIF(block).runInContext(ctx);
if (models.frameCount === 0) throw new Error('No models found.');
const props = trajectoryProps(models);
return new SO.Molecule.Trajectory(models, props);
let trajectory: Trajectory;
if (params.loadAllBlocks) {
const models: Model[] = [];
for (const block of a.data.blocks) {
if (ctx.shouldUpdate) {
await ctx.update(`Parsing ${block.header}...`);
}
const t = await trajectoryFromMmCIF(block).runInContext(ctx);
for (let i = 0; i < t.frameCount; i++) {
models.push(await Task.resolveInContext(t.getFrameAtIndex(i), ctx));
}
}
trajectory = new ArrayTrajectory(models);
} else {
const header = params.blockHeader || a.data.blocks[0].header;
const block = a.data.blocks.find(b => b.header === header);
if (!block) throw new Error(`Data block '${[header]}' not found.`);
trajectory = await trajectoryFromMmCIF(block).runInContext(ctx);
}
if (trajectory.frameCount === 0) throw new Error('No models found.');
const props = trajectoryProps(trajectory);
return new SO.Molecule.Trajectory(trajectory, props);
});
}
});

View File

@@ -351,7 +351,7 @@ const OverpaintStructureRepresentation3DFromScript = PluginStateTransform.BuiltI
const newGeometryVersion = a.data.repr.geometryVersion;
// smoothing needs to be re-calculated when geometry changes
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Recreate;
const oldOverpaint = b.data.state.overpaint!;
const newOverpaint = Overpaint.ofScript(newParams.layers, newStructure);
@@ -409,7 +409,7 @@ const OverpaintStructureRepresentation3DFromBundle = PluginStateTransform.BuiltI
const newGeometryVersion = a.data.repr.geometryVersion;
// smoothing needs to be re-calculated when geometry changes
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Recreate;
const oldOverpaint = b.data.state.overpaint!;
const newOverpaint = Overpaint.ofBundle(newParams.layers, newStructure);
@@ -464,7 +464,7 @@ const TransparencyStructureRepresentation3DFromScript = PluginStateTransform.Bui
const newGeometryVersion = a.data.repr.geometryVersion;
// smoothing needs to be re-calculated when geometry changes
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Recreate;
const oldTransparency = b.data.state.transparency!;
const newTransparency = Transparency.ofScript(newParams.layers, newStructure);
@@ -520,7 +520,7 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
const newGeometryVersion = a.data.repr.geometryVersion;
// smoothing needs to be re-calculated when geometry changes
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Recreate;
const oldTransparency = b.data.state.transparency!;
const newTransparency = Transparency.ofBundle(newParams.layers, newStructure);
@@ -577,7 +577,7 @@ const SubstanceStructureRepresentation3DFromScript = PluginStateTransform.BuiltI
const newGeometryVersion = a.data.repr.geometryVersion;
// smoothing needs to be re-calculated when geometry changes
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Recreate;
const oldSubstance = b.data.state.substance!;
const newSubstance = Substance.ofScript(newParams.layers, newStructure);
@@ -635,7 +635,7 @@ const SubstanceStructureRepresentation3DFromBundle = PluginStateTransform.BuiltI
const newGeometryVersion = a.data.repr.geometryVersion;
// smoothing needs to be re-calculated when geometry changes
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Recreate;
const oldSubstance = b.data.state.substance!;
const newSubstance = Substance.ofBundle(newParams.layers, newStructure);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -240,14 +240,9 @@ export class SelectionViewportControls extends PluginUIComponent {
this.subscribe(this.plugin.behaviors.interaction.selectionMode, () => this.forceUpdate());
}
onMouseMove = (e: React.MouseEvent) => {
// ignore mouse moves when no button is held
if (e.buttons === 0) e.stopPropagation();
};
render() {
if (!this.plugin.selectionMode) return null;
return <div className='msp-selection-viewport-controls' onMouseMove={this.onMouseMove}>
return <div className='msp-selection-viewport-controls'>
<StructureSelectionActionsControls />
</div>;
}

View File

@@ -626,9 +626,9 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
const value = bounds[handle];
let direction = 0;
if (bounds[handle + 1] - value < threshold!) {
if (bounds[handle + 1] - value < +threshold!) {
direction = +1;
} else if (value - bounds[handle - 1] < threshold!) {
} else if (value - bounds[handle - 1] < +threshold!) {
direction = -1;
}

View File

@@ -6,6 +6,7 @@
*/
import * as React from 'react';
import { throttleTime } from 'rxjs';
import { Canvas3DParams } from '../mol-canvas3d/canvas3d';
import { PluginCommands } from '../mol-plugin/commands';
import { LeftPanelTabName } from '../mol-plugin/layout';
@@ -13,12 +14,12 @@ import { StateTransform } from '../mol-state';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base';
import { IconButton, SectionHeader } from './controls/common';
import { AccountTreeOutlinedSvg, DeleteOutlinedSvg, HelpOutlineSvg, HomeOutlinedSvg, SaveOutlinedSvg, TuneSvg } from './controls/icons';
import { ParameterControls } from './controls/parameters';
import { StateObjectActions } from './state/actions';
import { RemoteStateSnapshots, StateSnapshots } from './state/snapshots';
import { StateTree } from './state/tree';
import { HelpContent } from './viewport/help';
import { HomeOutlinedSvg, AccountTreeOutlinedSvg, TuneSvg, HelpOutlineSvg, SaveOutlinedSvg, DeleteOutlinedSvg } from './controls/icons';
export class CustomImportControls extends PluginUIComponent<{ initiallyCollapsed?: boolean }> {
componentDidMount() {
@@ -142,7 +143,7 @@ class FullSettings extends PluginUIComponent {
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
if (this.plugin.canvas3d) {
this.subscribe(this.plugin.canvas3d.camera.stateChanged, state => {
this.subscribe(this.plugin.canvas3d.camera.stateChanged.pipe(throttleTime(500, undefined, { leading: true, trailing: true })), state => {
if (state.radiusMax !== undefined || state.radius !== undefined) {
this.forceUpdate();
}

View File

@@ -7,7 +7,7 @@
background: $default-background;
.msp-btn-link {
background: rgba(0,0,0,0.2);
background: rgba(0, 0, 0, 0.2);
}
}
@@ -25,14 +25,14 @@
bottom: 0;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-touch-callout: none;
touch-action: manipulation;
> canvas {
background-color: $default-background;
background-image: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey),
linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey);
linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey);
background-size: 60px 60px;
background-position: 0 0, 30px 30px;
}
@@ -82,6 +82,33 @@
height: 100%;
}
.msp-hover-box-wrapper {
position: relative;
.msp-hover-box-body {
visibility: hidden;
position: absolute;
right: $row-height + 4px;
top: 0;
width: 100px;
background-color: $default-background;
}
.msp-hover-box-spacer {
visibility: hidden;
position: absolute;
right: $row-height;
top: 0;
width: 4px;
height: $row-height;
}
&:hover .msp-hover-box-body,
&:hover .msp-hover-box-spacer {
visibility: visible;
}
}
.msp-viewport-controls-panel {
width: 290px;
top: 0;
@@ -134,4 +161,4 @@
font-size: 85%;
display: inline-block;
color: $highlight-info-additional-font-color;
}
}

View File

@@ -3,6 +3,7 @@
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Jason Pattle <jpattle.exscientia.co.uk>
*/
import * as React from 'react';
@@ -11,6 +12,7 @@ import { StructureElement } from '../../mol-model/structure';
import { StructureMeasurementCell, StructureMeasurementOptions, StructureMeasurementParams } from '../../mol-plugin-state/manager/structure/measurement';
import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
import { AngleData } from '../../mol-repr/shape/loci/angle';
import { DihedralData } from '../../mol-repr/shape/loci/dihedral';
import { DistanceData } from '../../mol-repr/shape/loci/distance';
@@ -208,13 +210,16 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
entries.push(this.historyEntry(history[i], i + 1));
}
const shouldShowToggleHint = this.plugin.config.get(PluginConfig.Viewport.ShowSelectionMode);
const toggleHint = shouldShowToggleHint ? (<>{' '}(toggle <ToggleSelectionModeButton inline /> mode)</>) : null;
return <>
<ActionMenu items={this.actions} onSelect={this.selectAction} />
{entries.length > 0 && <div className='msp-control-offset'>
{entries}
</div>}
{entries.length === 0 && <div className='msp-control-offset msp-help-text'>
<div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add one or more selections (toggle <ToggleSelectionModeButton inline /> mode)</div>
<div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add one or more selections{toggleHint}</div>
</div>}
</>;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -56,11 +56,24 @@ export class QuickStyles extends PurePluginUIComponent {
postprocessing: {
outline: {
name: 'on',
params: { scale: 1, color: Color(0x000000), threshold: 0.25, includeTransparent: true }
params: {
scale: 1,
color: Color(0x000000),
threshold: 0.25,
includeTransparent: true,
}
},
occlusion: {
name: 'on',
params: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1 }
params: {
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
samples: 32,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: { name: 'off', params: {} },
}
@@ -79,13 +92,26 @@ export class QuickStyles extends PurePluginUIComponent {
name: 'on',
params: pp.outline.name === 'on'
? pp.outline.params
: { scale: 1, color: Color(0x000000), threshold: 0.33, includeTransparent: true }
: {
scale: 1,
color: Color(0x000000),
threshold: 0.33,
includeTransparent: true,
}
},
occlusion: {
name: 'on',
params: pp.occlusion.name === 'on'
? pp.occlusion.params
: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1 }
: {
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
samples: 32,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: { name: 'off', params: {} },
}

View File

@@ -3,6 +3,7 @@
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Jason Pattle <jpattle.exscientia.co.uk>
*/
import * as React from 'react';
@@ -12,6 +13,7 @@ import { InteractivityManager } from '../../mol-plugin-state/manager/interactivi
import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
import { StructureComponentRef, StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
import { StructureSelectionModifier } from '../../mol-plugin-state/manager/structure/selection';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginContext } from '../../mol-plugin/context';
import { compileIdListSelection } from '../../mol-script/util/id-list';
import { memoizeLatest } from '../../mol-util/memoize';
@@ -272,7 +274,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
<IconButton svg={RestoreSvg} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} title={undoTitle} />
<ToggleButton icon={HelpOutlineSvg} title='Show/hide help' toggle={this.toggleHelp} style={{ marginLeft: '10px' }} isSelected={this.state.action === 'help'} />
<IconButton svg={CancelOutlinedSvg} title='Turn selection mode off' onClick={this.turnOff} />
{this.plugin.config.get(PluginConfig.Viewport.ShowSelectionMode) && (<IconButton svg={CancelOutlinedSvg} title='Turn selection mode off' onClick={this.turnOff} />)}
</div>
{children}
</>;

View File

@@ -16,6 +16,7 @@ import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/s
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
import { StateObjectCell, StateObjectRef } from '../../mol-state';
import { elementLabel, structureElementStatsLabel } from '../../mol-theme/label';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
@@ -324,6 +325,11 @@ export class SuperpositionControls extends PurePluginUIComponent<{ }, Superposit
return entries;
}
toggleHint() {
const shouldShowToggleHint = this.plugin.config.get(PluginConfig.Viewport.ShowSelectionMode);
return shouldShowToggleHint ? (<>{' '}(toggle <ToggleSelectionModeButton inline /> mode)</>) : null;
}
addByChains() {
const entries = this.chainEntries;
return <>
@@ -331,7 +337,7 @@ export class SuperpositionControls extends PurePluginUIComponent<{ }, Superposit
{entries.map((e, i) => this.lociEntry(e, i))}
</div>}
{entries.length < 2 && <div className='msp-control-offset msp-help-text'>
<div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add 2 or more selections (toggle <ToggleSelectionModeButton inline /> mode) from separate structures. Selections must be limited to single polymer chains or residues therein.</div>
<div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add 2 or more selections{this.toggleHint()} from separate structures. Selections must be limited to single polymer chains or residues therein.</div>
</div>}
{entries.length > 1 && <Button title='Superpose structures by selected chains.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeChains} style={{ marginTop: '1px' }}>
Superpose
@@ -346,7 +352,7 @@ export class SuperpositionControls extends PurePluginUIComponent<{ }, Superposit
{entries.map((e, i) => this.atomsLociEntry(e, i))}
</div>}
{entries.length < 2 && <div className='msp-control-offset msp-help-text'>
<div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add 1 or more selections (toggle <ToggleSelectionModeButton inline /> mode) from
<div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add 1 or more selections{this.toggleHint()} from
separate structures. Selections must be limited to single atoms.</div>
</div>}
{entries.length > 1 && <Button title='Superpose structures by selected atoms.' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeAtoms} style={{ marginTop: '1px' }}>

View File

@@ -1,16 +1,18 @@
/**
* Copyright (c) 2018-2019 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 David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import * as React from 'react';
import { throttleTime } from 'rxjs';
import { PluginCommands } from '../mol-plugin/commands';
import { PluginConfig } from '../mol-plugin/config';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base';
import { ControlGroup, IconButton } from './controls/common';
import { Button, ControlGroup, IconButton } from './controls/common';
import { AutorenewSvg, BuildOutlinedSvg, CameraOutlinedSvg, CloseSvg, FullscreenSvg, TuneSvg } from './controls/icons';
import { ToggleSelectionModeButton } from './structure/selection';
import { ViewportCanvas } from './viewport/canvas';
@@ -19,19 +21,23 @@ import { SimpleSettingsControl } from './viewport/simple-settings';
interface ViewportControlsState {
isSettingsExpanded: boolean,
isScreenshotExpanded: boolean
isScreenshotExpanded: boolean,
isCameraResetEnabled: boolean
}
interface ViewportControlsProps {
}
export class ViewportControls extends PluginUIComponent<ViewportControlsProps, ViewportControlsState> {
private allCollapsedState: ViewportControlsState = {
private allCollapsedState = {
isSettingsExpanded: false,
isScreenshotExpanded: false
isScreenshotExpanded: false,
};
state = { ...this.allCollapsedState } as ViewportControlsState;
state: ViewportControlsState = {
...this.allCollapsedState,
isCameraResetEnabled: true,
};
resetCamera = () => {
PluginCommands.Camera.Reset(this.plugin, {});
@@ -39,7 +45,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
private toggle(panel: keyof ViewportControlsState) {
return (e?: React.MouseEvent<HTMLButtonElement>) => {
this.setState({ ...this.allCollapsedState, [panel]: !this.state[panel] });
this.setState(old => ({ ...old, ...this.allCollapsedState, [panel]: !this.state[panel] }));
e?.currentTarget.blur();
};
}
@@ -67,26 +73,51 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
this.plugin.helpers.viewportScreenshot?.download();
};
enableCameraReset = (enable: boolean) => {
this.setState(old => ({ ...old, isCameraResetEnabled: enable }));
};
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
if (this.plugin.canvas3d) {
this.subscribe(
this.plugin.canvas3d.camera.stateChanged.pipe(throttleTime(500, undefined, { leading: true, trailing: true })),
snapshot => this.enableCameraReset(snapshot.radius !== 0 && snapshot.radiusMax !== 0)
);
}
}
icon(icon: React.FC, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
return <IconButton svg={icon} toggleState={isOn} onClick={onClick} title={title} style={{ background: 'transparent' }} />;
}
onMouseMove = (e: React.MouseEvent) => {
// ignore mouse moves when no button is held
if (e.buttons === 0) e.stopPropagation();
};
render() {
return <div className={'msp-viewport-controls'} onMouseMove={this.onMouseMove}>
return <div className={'msp-viewport-controls'}>
<div className='msp-viewport-controls-buttons'>
<div>
<div className='msp-hover-box-wrapper'>
<div className='msp-semi-transparent-background' />
{this.icon(AutorenewSvg, this.resetCamera, 'Reset Camera')}
{this.icon(AutorenewSvg, this.resetCamera, 'Reset Zoom')}
<div className='msp-hover-box-body'>
<div className='msp-flex-column'>
<div className='msp-flex-row'>
<Button onClick={() => this.resetCamera()} disabled={!this.state.isCameraResetEnabled} title='Set camera zoom to fit the visible scene into view'>
Reset Zoom
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.OrientAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align principal component axes of the loaded structures to the screen axes (“lay flat”)'>
Orient Axes
</Button>
</div>
<div className='msp-flex-row'>
<Button onClick={() => PluginCommands.Camera.ResetAxes(this.plugin)} disabled={!this.state.isCameraResetEnabled} title='Align Cartesian axes to the screen axes'>
Reset Axes
</Button>
</div>
</div>
</div>
<div className='msp-hover-box-spacer'></div>
</div>
<div>
<div className='msp-semi-transparent-background' />

View File

@@ -99,7 +99,7 @@ export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolea
{(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
<BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
</HelpGroup>}
{!!interactionBindings && <HelpGroup key='interactions' header='Mouse Controls'>
{!!interactionBindings && <HelpGroup key='interactions' header='Mouse & Key Controls'>
<BindingsHelp bindings={interactionBindings} />
</HelpGroup>}
</>;

View File

@@ -6,6 +6,7 @@
*/
import { produce } from 'immer';
import { throttleTime } from 'rxjs';
import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
@@ -26,7 +27,7 @@ export class SimpleSettingsControl extends PluginUIComponent {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => {
this.subscribe(this.plugin.canvas3d!.camera.stateChanged.pipe(throttleTime(500, undefined, { leading: true, trailing: true })), state => {
if (state.radiusMax !== undefined || state.radius !== undefined) {
this.forceUpdate();
}

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2018-2021 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Jason Pattle <jpattle.exscientia.co.uk>
*/
import { Loci } from '../../../mol-model/loci';
@@ -17,8 +18,25 @@ import { Vec3 } from '../../../mol-math/linear-algebra';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const Key = Binding.TriggerKey;
const DefaultFocusLociBindings = {
export const DefaultClickResetCameraOnEmpty = Binding([
Trigger(B.Flag.Primary, M.create()),
Trigger(B.Flag.Secondary, M.create()),
Trigger(B.Flag.Primary, M.create({ control: true }))
], 'Reset camera focus', 'Click on nothing using ${triggers}');
export const DefaultClickResetCameraOnEmptySelectMode = Binding([
Trigger(B.Flag.Secondary, M.create()),
Trigger(B.Flag.Primary, M.create({ control: true }))
], 'Reset camera focus', 'Click on nothing using ${triggers}');
type FocusLociBindings = {
clickCenterFocus: Binding
clickCenterFocusSelectMode: Binding
clickResetCameraOnEmpty?: Binding
clickResetCameraOnEmptySelectMode?: Binding
}
export const DefaultFocusLociBindings: FocusLociBindings = {
clickCenterFocus: Binding([
Trigger(B.Flag.Primary, M.create()),
Trigger(B.Flag.Secondary, M.create()),
@@ -28,6 +46,8 @@ const DefaultFocusLociBindings = {
Trigger(B.Flag.Secondary, M.create()),
Trigger(B.Flag.Primary, M.create({ control: true }))
], 'Camera center and focus', 'Click element using ${triggers}'),
clickResetCameraOnEmpty: DefaultClickResetCameraOnEmpty,
clickResetCameraOnEmptySelectMode: DefaultClickResetCameraOnEmptySelectMode,
};
const FocusLociParams = {
minRadius: PD.Numeric(8, { min: 1, max: 50, step: 1 }),
@@ -50,12 +70,16 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
? this.params.bindings.clickCenterFocusSelectMode
: this.params.bindings.clickCenterFocus;
if (Binding.match(binding, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
const resetBinding = this.ctx.selectionMode
? (this.params.bindings.clickResetCameraOnEmptySelectMode ?? DefaultClickResetCameraOnEmptySelectMode)
: (this.params.bindings.clickResetCameraOnEmpty ?? DefaultClickResetCameraOnEmpty);
if (Loci.isEmpty(current.loci) && Binding.match(resetBinding, button, modifiers)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
if (Binding.match(binding, button, modifiers)) {
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
this.ctx.managers.camera.focusLoci(loci, this.params);
}
@@ -127,4 +151,79 @@ export const CameraAxisHelper = PluginBehavior.create<{}>({
},
params: () => ({}),
display: { name: 'Camera Axis Helper' }
});
const DefaultCameraControlsBindings = {
keySpinAnimation: Binding([Key('KeyI')], 'Spin Animation', 'Press ${triggers}'),
keyRockAnimation: Binding([Key('KeyO')], 'Rock Animation', 'Press ${triggers}'),
keyToggleFlyMode: Binding([Key('Space', M.create({ shift: true }))], 'Toggle Fly Mode', 'Press ${triggers}'),
keyResetView: Binding([Key('KeyT')], 'Reset View', 'Press ${triggers}'),
};
const CameraControlsParams = {
bindings: PD.Value(DefaultCameraControlsBindings, { isHidden: true }),
};
type CameraControlsProps = PD.Values<typeof CameraControlsParams>
export const CameraControls = PluginBehavior.create<CameraControlsProps>({
name: 'camera-controls',
category: 'interaction',
ctor: class extends PluginBehavior.Handler<CameraControlsProps> {
register(): void {
this.subscribeObservable(this.ctx.behaviors.interaction.key, ({ code, modifiers }) => {
if (!this.ctx.canvas3d) return;
// include defaults for backwards state compatibility
const b = { ...DefaultCameraControlsBindings, ...this.params.bindings };
const p = this.ctx.canvas3d.props.trackball;
if (Binding.matchKey(b.keySpinAnimation, code, modifiers)) {
const name = p.animate.name !== 'spin' ? 'spin' : 'off';
if (name === 'off') {
this.ctx.canvas3d.setProps({
trackball: { animate: { name, params: {} } }
});
} else {
this.ctx.canvas3d.setProps({
trackball: { animate: {
name, params: { speed: 1 } }
}
});
}
}
if (Binding.matchKey(b.keyRockAnimation, code, modifiers)) {
const name = p.animate.name !== 'rock' ? 'rock' : 'off';
if (name === 'off') {
this.ctx.canvas3d.setProps({
trackball: { animate: { name, params: {} } }
});
} else {
this.ctx.canvas3d.setProps({
trackball: { animate: {
name, params: { speed: 0.3, angle: 10 } }
}
});
}
}
if (Binding.matchKey(b.keyToggleFlyMode, code, modifiers)) {
const flyMode = !p.flyMode;
this.ctx.canvas3d.setProps({
trackball: { flyMode }
});
if (this.ctx.canvas3dContext) {
this.ctx.canvas3dContext.canvas.style.cursor = flyMode ? 'crosshair' : 'unset';
}
}
if (Binding.matchKey(b.keyResetView, code, modifiers)) {
PluginCommands.Camera.Reset(this.ctx, {});
}
});
}
},
params: () => CameraControlsParams,
display: { name: 'Camera Controls on Canvas' }
});

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2018-2022 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Jason Pattle <jpattle.exscientia.co.uk>
*/
import { MarkerAction } from '../../../mol-util/marker-action';
@@ -92,11 +93,11 @@ export const HighlightLoci = PluginBehavior.create({
//
const DefaultSelectLociBindings = {
export const DefaultSelectLociBindings = {
clickSelect: Binding.Empty,
clickToggleExtend: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Toggle extended selection', '${triggers} to extend selection along polymer'),
clickToggleExtend: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Toggle extended selection', 'Click on element using ${triggers} to extend selection along polymer'),
clickSelectOnly: Binding.Empty,
clickToggle: Binding([Trigger(B.Flag.Primary, M.create())], 'Toggle selection', '${triggers} on element'),
clickToggle: Binding([Trigger(B.Flag.Primary, M.create())], 'Toggle selection', 'Click on element using ${triggers}'),
clickDeselect: Binding.Empty,
clickDeselectAllOnEmpty: Binding([Trigger(B.Flag.Primary, M.create())], 'Deselect all', 'Click on nothing using ${triggers}'),
};
@@ -236,7 +237,7 @@ export const DefaultLociLabelProvider = PluginBehavior.create({
//
const DefaultFocusLociBindings = {
export const DefaultFocusLociBindings = {
clickFocus: Binding([
Trigger(B.Flag.Primary, M.create()),
], 'Representation Focus', 'Click element using ${triggers}'),

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2018 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 David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginContext } from '../../../mol-plugin/context';
@@ -11,6 +12,8 @@ export function registerDefault(ctx: PluginContext) {
Reset(ctx);
Focus(ctx);
SetSnapshot(ctx);
OrientAxes(ctx);
ResetAxes(ctx);
}
export function Reset(ctx: PluginContext) {
@@ -30,4 +33,16 @@ export function Focus(ctx: PluginContext) {
ctx.managers.camera.focusSphere({ center, radius }, { durationMs });
ctx.events.canvas3d.settingsUpdated.next(void 0);
});
}
}
export function OrientAxes(ctx: PluginContext) {
PluginCommands.Camera.OrientAxes.subscribe(ctx, ({ structures, durationMs }) => {
ctx.managers.camera.orientAxes(structures, durationMs);
});
}
export function ResetAxes(ctx: PluginContext) {
PluginCommands.Camera.ResetAxes.subscribe(ctx, ({ durationMs }) => {
ctx.managers.camera.resetAxes(durationMs);
});
}

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2018-2020 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/
import { Camera } from '../mol-canvas3d/camera';
@@ -10,7 +11,7 @@ import { PluginCommand } from './command';
import { StateTransform, State, StateAction } from '../mol-state';
import { Canvas3DProps } from '../mol-canvas3d/canvas3d';
import { PluginLayoutStateProps } from './layout';
import { StructureElement } from '../mol-model/structure';
import { Structure, StructureElement } from '../mol-model/structure';
import { PluginState } from './state';
import { PluginToast } from './util/toast';
import { Vec3 } from '../mol-math/linear-algebra';
@@ -62,7 +63,9 @@ export const PluginCommands = {
Camera: {
Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(),
SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(),
Focus: PluginCommand<{ center: Vec3, radius: number, durationMs?: number }>()
Focus: PluginCommand<{ center: Vec3, radius: number, durationMs?: number }>(),
OrientAxes: PluginCommand<{ structures?: Structure[], durationMs?: number }>(),
ResetAxes: PluginCommand<{ durationMs?: number }>(),
},
Canvas3D: {
SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void) }>(),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -8,7 +8,7 @@
import produce, { setAutoFreeze } from 'immer';
import { List } from 'immutable';
import { merge, Subscription } from 'rxjs';
import { filter, take } from 'rxjs/operators';
import { debounceTime, filter, take, throttleTime } from 'rxjs/operators';
import { Canvas3D, Canvas3DContext, DefaultCanvas3DParams } from '../mol-canvas3d/canvas3d';
import { resizeCanvas } from '../mol-canvas3d/util';
import { Vec2 } from '../mol-math/linear-algebra';
@@ -43,7 +43,7 @@ import { AssetManager } from '../mol-util/assets';
import { Color } from '../mol-util/color';
import { ajaxGet } from '../mol-util/data-source';
import { isDebugMode, isProductionMode } from '../mol-util/debug';
import { ModifiersKeys } from '../mol-util/input/input-observer';
import { EmptyKeyInput, KeyInput, ModifiersKeys } from '../mol-util/input/input-observer';
import { LogEntry } from '../mol-util/log-entry';
import { objectForEach } from '../mol-util/object';
import { RxEventHelper } from '../mol-util/rx-event-helper';
@@ -95,7 +95,8 @@ export class PluginContext {
hover: this.ev.behavior<InteractivityManager.HoverEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 }),
click: this.ev.behavior<InteractivityManager.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 }),
drag: this.ev.behavior<InteractivityManager.DragEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0, pageStart: Vec2(), pageEnd: Vec2() }),
selectionMode: this.ev.behavior<boolean>(false)
key: this.ev.behavior<KeyInput>(EmptyKeyInput),
selectionMode: this.ev.behavior<boolean>(false),
},
labels: {
highlight: this.ev.behavior<{ labels: ReadonlyArray<LociLabel> }>({ labels: [] })
@@ -292,7 +293,8 @@ export class PluginContext {
this.subs.push(this.canvas3d!.interaction.click.subscribe(e => this.behaviors.interaction.click.next(e)));
this.subs.push(this.canvas3d!.interaction.drag.subscribe(e => this.behaviors.interaction.drag.next(e)));
this.subs.push(this.canvas3d!.interaction.hover.subscribe(e => this.behaviors.interaction.hover.next(e)));
this.subs.push(this.canvas3d!.input.resize.subscribe(() => this.handleResize()));
this.subs.push(this.canvas3d!.input.resize.pipe(debounceTime(50), throttleTime(100, undefined, { leading: false, trailing: true })).subscribe(() => this.handleResize()));
this.subs.push(this.canvas3d!.input.keyDown.subscribe(e => this.behaviors.interaction.key.next(e)));
this.subs.push(this.layout.events.updated.subscribe(() => requestAnimationFrame(() => this.handleResize())));
this.handleResize();
@@ -362,6 +364,7 @@ export class PluginContext {
}
this.subs = [];
this.animationLoop.stop();
this.commands.dispose();
this.canvas3d?.dispose();
this.canvas3dContext?.dispose(options);

View File

@@ -5,33 +5,55 @@
*/
import fs from 'fs';
import { type PNG } from 'pngjs'; // Only import type here, the actual import must be provided by the caller
import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import must be provided by the caller
import { Canvas3D } from '../mol-canvas3d/canvas3d';
import { PostprocessingProps } from '../mol-canvas3d/passes/postprocessing';
import { PluginContext } from './context';
import { PluginSpec } from './spec';
import { HeadlessScreenshotHelper, HeadlessScreenshotHelperOptions } from './util/headless-screenshot';
import { HeadlessScreenshotHelper, HeadlessScreenshotHelperOptions, ExternalModules, RawImageData } from './util/headless-screenshot';
/** PluginContext that can be used in Node.js (without DOM) */
export class HeadlessPluginContext extends PluginContext {
renderer: HeadlessScreenshotHelper;
constructor(spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: HeadlessScreenshotHelperOptions) {
/** External modules (`gl` and optionally `pngjs` and `jpeg-js`) must be provided to the constructor (this is to avoid Mol* being dependent on these packages which are only used here) */
constructor(externalModules: ExternalModules, spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: HeadlessScreenshotHelperOptions) {
super(spec);
this.renderer = new HeadlessScreenshotHelper(canvasSize, undefined, rendererOptions);
this.renderer = new HeadlessScreenshotHelper(externalModules, canvasSize, undefined, rendererOptions);
(this.canvas3d as Canvas3D) = this.renderer.canvas3d;
}
/** Render the current plugin state to a PNG or JPEG file */
/** Render the current plugin state and save to a PNG or JPEG file */
async saveImage(outPath: string, imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>, format?: 'png' | 'jpeg', jpegQuality = 90) {
this.canvas3d!.commit(true);
return await this.renderer.saveImage(outPath, imageSize, props, format, jpegQuality);
}
/** Get the current plugin state */
getStateSnapshot() {
/** Render the current plugin state and return as raw image data */
async getImageRaw(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>): Promise<RawImageData> {
this.canvas3d!.commit(true);
return this.managers.snapshot.getStateSnapshot({ params: {} });
return await this.renderer.getImageRaw(imageSize, props);
}
/** Render the current plugin state and return as a PNG object */
async getImagePng(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>): Promise<PNG> {
this.canvas3d!.commit(true);
return await this.renderer.getImagePng(imageSize, props);
}
/** Render the current plugin state and return as a JPEG object */
async getImageJpeg(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>, jpegQuality: number = 90): Promise<JpegBufferRet> {
this.canvas3d!.commit(true);
return await this.renderer.getImageJpeg(imageSize, props);
}
/** Get the current plugin state */
async getStateSnapshot() {
this.canvas3d!.commit(true);
return await this.managers.snapshot.getStateSnapshot({ params: {} });
}
/** Save the current plugin state to a MOLJ file */

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -120,6 +120,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
PluginSpec.Behavior(PluginBehaviors.Representation.FocusLoci),
PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
PluginSpec.Behavior(PluginBehaviors.Camera.CameraControls),
PluginSpec.Behavior(StructureFocusRepresentation),
PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo),

View File

@@ -10,8 +10,8 @@
import fs from 'fs';
import path from 'path';
import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import is done by LazyImports
import { type PNG } from 'pngjs'; // Only import type here, the actual import is done by LazyImports
import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import must be provided by the caller
import { type PNG } from 'pngjs'; // Only import type here, the actual import must be provided by the caller
import { Canvas3D, Canvas3DContext, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
import { ImagePass, ImageProps } from '../../mol-canvas3d/passes/image';
@@ -22,16 +22,14 @@ import { AssetManager } from '../../mol-util/assets';
import { ColorNames } from '../../mol-util/color/names';
import { PixelData } from '../../mol-util/image';
import { InputObserver } from '../../mol-util/input/input-observer';
import { LazyImports } from '../../mol-util/lazy-imports';
import { ParamDefinition } from '../../mol-util/param-definition';
const lazyImports = LazyImports.create('gl', 'jpeg-js', 'pngjs') as {
export interface ExternalModules {
'gl': typeof import('gl'),
'jpeg-js': typeof import('jpeg-js'),
'pngjs': typeof import('pngjs'),
};
'jpeg-js'?: typeof import('jpeg-js'),
'pngjs'?: typeof import('pngjs'),
}
export type HeadlessScreenshotHelperOptions = {
webgl?: WebGLContextAttributes,
@@ -51,11 +49,11 @@ export class HeadlessScreenshotHelper {
readonly canvas3d: Canvas3D;
readonly imagePass: ImagePass;
constructor(readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: HeadlessScreenshotHelperOptions) {
constructor(readonly externalModules: ExternalModules, readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: HeadlessScreenshotHelperOptions) {
if (canvas3d) {
this.canvas3d = canvas3d;
} else {
const glContext = lazyImports.gl(this.canvasSize.width, this.canvasSize.height, options?.webgl ?? defaultWebGLAttributes());
const glContext = this.externalModules.gl(this.canvasSize.width, this.canvasSize.height, options?.webgl ?? defaultWebGLAttributes());
const webgl = createContext(glContext);
const input = InputObserver.create();
const attribs = { ...Canvas3DContext.DefaultAttribs };
@@ -93,14 +91,20 @@ export class HeadlessScreenshotHelper {
async getImagePng(imageSize?: { width: number, height: number }, postprocessing?: Partial<PostprocessingProps>): Promise<PNG> {
const imageData = await this.getImageRaw(imageSize, postprocessing);
const generatedPng = new lazyImports.pngjs.PNG({ width: imageData.width, height: imageData.height });
if (!this.externalModules.pngjs) {
throw new Error("External module 'pngjs' was not provided. If you want to use getImagePng, you must import 'pngjs' and provide it to the HeadlessPluginContext/HeadlessScreenshotHelper constructor.");
}
const generatedPng = new this.externalModules.pngjs.PNG({ width: imageData.width, height: imageData.height });
generatedPng.data = Buffer.from(imageData.data.buffer);
return generatedPng;
}
async getImageJpeg(imageSize?: { width: number, height: number }, postprocessing?: Partial<PostprocessingProps>, jpegQuality: number = 90): Promise<JpegBufferRet> {
const imageData = await this.getImageRaw(imageSize, postprocessing);
const generatedJpeg = lazyImports['jpeg-js'].encode(imageData, jpegQuality);
if (!this.externalModules['jpeg-js']) {
throw new Error("External module 'jpeg-js' was not provided. If you want to use getImageJpeg, you must import 'jpeg-js' and provide it to the HeadlessPluginContext/HeadlessScreenshotHelper constructor.");
}
const generatedJpeg = this.externalModules['jpeg-js'].encode(imageData, jpegQuality);
return generatedJpeg;
}
@@ -206,10 +210,12 @@ export const STYLIZED_POSTPROCESSING: Partial<PostprocessingProps> = {
occlusion: {
name: 'on' as const, params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
resolutionScale: 1,
color: ColorNames.black,
}
}, outline: {
name: 'on' as const, params: {

View File

@@ -119,7 +119,7 @@ class ViewportScreenshotHelper extends PluginComponent {
postprocessing: {
...c.props.postprocessing,
occlusion: aoProps.name === 'on'
? { name: 'on', params: { ...aoProps.params, samples: 128, resolutionScale: 1 } }
? { name: 'on', params: { ...aoProps.params, samples: 128, resolutionScale: c.webgl.pixelRatio } }
: aoProps
},
marking: { ...c.props.marking }
@@ -143,7 +143,7 @@ class ViewportScreenshotHelper extends PluginComponent {
postprocessing: {
...c.props.postprocessing,
occlusion: aoProps.name === 'on'
? { name: 'on', params: { ...aoProps.params, samples: 128, resolutionScale: 1 } }
? { name: 'on', params: { ...aoProps.params, samples: 128, resolutionScale: c.webgl.pixelRatio } }
: aoProps
},
marking: { ...c.props.marking }

View File

@@ -18,7 +18,7 @@ import { Loci as ModelLoci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
import { Overpaint } from '../mol-theme/overpaint';
import { Transparency } from '../mol-theme/transparency';
import { Mat4 } from '../mol-math/linear-algebra';
import { getQualityProps } from './util';
import { LocationCallback, getQualityProps } from './util';
import { BaseGeometry } from '../mol-geo/geometry/base';
import { Visual } from './visual';
import { CustomProperty } from '../mol-model-props/common/custom-property';
@@ -162,6 +162,7 @@ interface Representation<D, P extends PD.Params = PD.Params, S extends Represent
setTheme: (theme: Theme) => void
getLoci: (pickingId: PickingId) => ModelLoci
getAllLoci: () => ModelLoci[]
eachLocation: (cb: LocationCallback) => void
mark: (loci: ModelLoci, action: MarkerAction) => boolean
destroy: () => void
}
@@ -250,6 +251,7 @@ namespace Representation {
setTheme: () => {},
getLoci: () => EmptyLoci,
getAllLoci: () => [],
eachLocation: () => {},
mark: () => false,
destroy: () => {}
};
@@ -370,6 +372,14 @@ namespace Representation {
}
return loci;
},
eachLocation: (cb: LocationCallback) => {
const { visuals } = currentProps;
for (let i = 0, il = reprList.length; i < il; ++i) {
if (!visuals || visuals.includes(reprMap[i])) {
reprList[i].eachLocation(cb);
}
}
},
mark: (loci: ModelLoci, action: MarkerAction) => {
let marked = false;
for (let i = 0, il = reprList.length; i < il; ++i) {
@@ -436,6 +446,9 @@ namespace Representation {
// TODO
return [];
},
eachLocation: () => {
// TODO
},
mark: (loci: ModelLoci, action: MarkerAction) => {
// TODO
return false;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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>
*/
@@ -11,7 +11,7 @@ import { Subject } from 'rxjs';
import { getNextMaterialId, createRenderObject, GraphicsRenderObject } from '../../mol-gl/render-object';
import { Theme } from '../../mol-theme/theme';
import { LocationIterator } from '../../mol-geo/util/location-iterator';
import { VisualUpdateState } from '../util';
import { LocationCallback, VisualUpdateState } from '../util';
import { createMarkers } from '../../mol-geo/geometry/marker-data';
import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
import { ValueCell } from '../../mol-util';
@@ -223,6 +223,13 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
getAllLoci() {
return [Shape.Loci(_shape)];
},
eachLocation: (cb: LocationCallback) => {
locationIt.reset();
while (locationIt.hasNext) {
const { location, isSecondary } = locationIt.move();
cb(location, isSecondary);
}
},
mark(loci: Loci, action: MarkerAction) {
if (!MarkerActions.is(_state.markerActions, action)) return false;
if (ShapeGroup.isLoci(loci) || Shape.isLoci(loci)) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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 David Sehnal <david.sehnal@gmail.com>
@@ -22,6 +22,7 @@ import { Clipping } from '../../mol-theme/clipping';
import { Transparency } from '../../mol-theme/transparency';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Substance } from '../../mol-theme/substance';
import { LocationCallback } from '../util';
export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, structure: Structure, props: PD.Values<P>, webgl?: WebGLContext) => ComplexVisual<P>): StructureRepresentation<P> {
let version = 0;
@@ -77,7 +78,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
}
function getAllLoci() {
return [Structure.Loci(_structure.target)];
return [Structure.Loci(_structure.child ?? _structure)];
}
function eachLocation(cb: LocationCallback) {
visual?.eachLocation(cb);
}
function mark(loci: Loci, action: MarkerAction) {
@@ -162,6 +167,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
setTheme,
getLoci,
getAllLoci,
eachLocation,
mark,
destroy
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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>
*/
@@ -15,7 +15,7 @@ import { createRenderObject, GraphicsRenderObject, RenderObjectValues } from '..
import { PickingId } from '../../mol-geo/geometry/picking';
import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
import { Interval } from '../../mol-data/int';
import { VisualUpdateState } from '../util';
import { LocationCallback, VisualUpdateState } from '../util';
import { ColorTheme } from '../../mol-theme/color';
import { ValueCell, deepEqual } from '../../mol-util';
import { createSizes, SizeData } from '../../mol-geo/geometry/size-data';
@@ -266,6 +266,13 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
getLoci(pickingId: PickingId) {
return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci;
},
eachLocation(cb: LocationCallback) {
locationIt.reset();
while (locationIt.hasNext) {
const { location, isSecondary } = locationIt.move();
cb(location, isSecondary);
}
},
mark(loci: Loci, action: MarkerAction) {
return Visual.mark(renderObject, loci, action, lociApply, previousMark);
},

View File

@@ -26,6 +26,7 @@ import { Clipping } from '../../mol-theme/clipping';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { StructureGroup } from './visual/util/common';
import { Substance } from '../../mol-theme/substance';
import { LocationCallback } from '../util';
export interface UnitsVisual<P extends StructureParams> extends Visual<StructureGroup, P> { }
@@ -194,8 +195,14 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
return loci;
}
function eachLocation(cb: LocationCallback) {
visuals.forEach(({ visual }) => {
visual.eachLocation(cb);
});
}
function getAllLoci() {
return [Structure.Loci(_structure.target)];
return [Structure.Loci(_structure.child ?? _structure)];
}
function mark(loci: Loci, action: MarkerAction) {
@@ -312,6 +319,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
setTheme,
getLoci,
getAllLoci,
eachLocation,
mark,
destroy
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 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>
*/
@@ -16,7 +16,7 @@ import { createRenderObject, GraphicsRenderObject, RenderObjectValues } from '..
import { PickingId } from '../../mol-geo/geometry/picking';
import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
import { Interval } from '../../mol-data/int';
import { VisualUpdateState } from '../util';
import { LocationCallback, VisualUpdateState } from '../util';
import { ColorTheme } from '../../mol-theme/color';
import { createMarkers } from '../../mol-geo/geometry/marker-data';
import { MarkerAction } from '../../mol-util/marker-action';
@@ -337,6 +337,13 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
getLoci(pickingId: PickingId) {
return renderObject ? getLoci(pickingId, currentStructureGroup, renderObject.id) : EmptyLoci;
},
eachLocation(cb: LocationCallback) {
locationIt.reset();
while (locationIt.hasNext) {
const { location, isSecondary } = locationIt.move();
cb(location, isSecondary);
}
},
mark(loci: Loci, action: MarkerAction) {
let hasInvariantId = true;
if (StructureElement.Loci.is(loci)) {

View File

@@ -218,6 +218,7 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex
eachLocation: eachInterBond,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
state.createGeometry = (
newProps.sizeFactor !== currentProps.sizeFactor ||
newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
newProps.linkScale !== currentProps.linkScale ||
newProps.linkSpacing !== currentProps.linkSpacing ||

View File

@@ -235,6 +235,7 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
eachLocation: eachIntraBond,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitBondCylinderParams>, currentProps: PD.Values<IntraUnitBondCylinderParams>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
state.createGeometry = (
newProps.sizeFactor !== currentProps.sizeFactor ||
newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
newProps.linkScale !== currentProps.linkScale ||
newProps.linkSpacing !== currentProps.linkSpacing ||

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