mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 07:04:22 +08:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50c1b667c5 | ||
|
|
360031d37c | ||
|
|
9ec873e0db | ||
|
|
c830a720b0 | ||
|
|
1aa7d1e0f7 | ||
|
|
c5c8de8628 | ||
|
|
74c6d6f5a1 | ||
|
|
2bff0faff7 | ||
|
|
4df028aa77 | ||
|
|
47c2d153aa | ||
|
|
18be09e9d5 | ||
|
|
55e940e88c | ||
|
|
e246f4e5ca | ||
|
|
5e1bb4b106 | ||
|
|
0b2889bb99 | ||
|
|
2994caf411 | ||
|
|
e157993a0f | ||
|
|
6c7c9afc34 | ||
|
|
2d0b17d93c | ||
|
|
033c613c89 | ||
|
|
1985eb59dd | ||
|
|
1cf6cbf8a3 | ||
|
|
0b42379c34 | ||
|
|
414c349974 | ||
|
|
cf6d5f7194 | ||
|
|
949f5207b4 | ||
|
|
a1da374b32 | ||
|
|
5460322d4a | ||
|
|
8b2da0b787 | ||
|
|
3eaf4dacaf | ||
|
|
d66d9b4dd7 | ||
|
|
cc52279e01 | ||
|
|
0def474f6d | ||
|
|
e0ea9a2855 | ||
|
|
2bc381fe05 | ||
|
|
fb3cd3bf52 | ||
|
|
c4414c7cc4 | ||
|
|
e2f2ceb7a9 | ||
|
|
641e7efb11 | ||
|
|
11f2ef50ef | ||
|
|
869ecfaf71 | ||
|
|
cb8731815c | ||
|
|
a9177ad362 | ||
|
|
ad116df73b | ||
|
|
f30b3a410c | ||
|
|
c440ba2d4b | ||
|
|
a3267dafdb | ||
|
|
7a1e83733c | ||
|
|
7cb96ce983 | ||
|
|
a73633d0c3 | ||
|
|
b2f8e8dd4e | ||
|
|
291d7abb78 | ||
|
|
32873d787b | ||
|
|
e243d71abf | ||
|
|
2689d3f21a | ||
|
|
c1bc008114 | ||
|
|
254578460a | ||
|
|
f5467dd3b9 | ||
|
|
9eb8714e11 | ||
|
|
847678ea56 | ||
|
|
f08729a402 | ||
|
|
a7c91257a7 | ||
|
|
835369a91e | ||
|
|
62554b522f | ||
|
|
fd041cd4c3 | ||
|
|
cfbb68c8ef | ||
|
|
d7acec4f7d | ||
|
|
7da46bca8b | ||
|
|
c480579ca8 | ||
|
|
00ff1a1eae | ||
|
|
ae795f8ad3 | ||
|
|
9d3c071689 | ||
|
|
01cb23f566 | ||
|
|
fe8a9799ab | ||
|
|
4f18154681 | ||
|
|
2114c4a3ad | ||
|
|
2ca41b2b51 | ||
|
|
6605a2019e | ||
|
|
8b1ed5f183 | ||
|
|
f11a1b788f | ||
|
|
7928e24c54 | ||
|
|
5dbca41da6 | ||
|
|
f3fa54addf | ||
|
|
e636397f90 | ||
|
|
6d76bf120d | ||
|
|
a50e81551f | ||
|
|
86512bcea1 | ||
|
|
975f45eb01 | ||
|
|
f2399d3179 | ||
|
|
b26d62a067 | ||
|
|
926d6cbd46 | ||
|
|
7ea47d2a99 | ||
|
|
89ad8cfc15 | ||
|
|
302a309aff | ||
|
|
c3e62bc2e5 | ||
|
|
c2ab322bd2 | ||
|
|
aeab0f235c | ||
|
|
ae2285599f | ||
|
|
104ab757d2 | ||
|
|
de84a8c8c5 | ||
|
|
4fa135daf0 | ||
|
|
9870cb4082 | ||
|
|
b2924761ab | ||
|
|
509e6bc2d8 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -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
3973
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
37
src/mol-canvas3d/_spec/camera.spec.ts
Normal file
37
src/mol-canvas3d/_spec/camera.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -72,6 +72,7 @@ uniform vec3 uInteriorColor;
|
||||
bool interior;
|
||||
|
||||
uniform float uXrayEdgeFalloff;
|
||||
uniform float uExposure;
|
||||
|
||||
uniform mat4 uProjection;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ uniform vec3 uFogColor;
|
||||
uniform float uAlpha;
|
||||
uniform bool uTransparentBackground;
|
||||
uniform float uXrayEdgeFalloff;
|
||||
uniform float uExposure;
|
||||
|
||||
uniform int uRenderMask;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
74
src/mol-math/geometry/_spec/frustum3d.spec.ts
Normal file
74
src/mol-math/geometry/_spec/frustum3d.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
40
src/mol-math/geometry/_spec/plane3d.spec.ts
Normal file
40
src/mol-math/geometry/_spec/plane3d.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
21
src/mol-math/geometry/_spec/polygon.spec.ts
Normal file
21
src/mol-math/geometry/_spec/polygon.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
24
src/mol-math/geometry/polygon.ts
Normal file
24
src/mol-math/geometry/polygon.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
99
src/mol-math/geometry/primitives/frustum3d.ts
Normal file
99
src/mol-math/geometry/primitives/frustum3d.ts
Normal 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 };
|
||||
93
src/mol-math/geometry/primitives/plane3d.ts
Normal file
93
src/mol-math/geometry/primitives/plane3d.ts
Normal 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 };
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
218
src/mol-plugin-state/manager/focus-camera/orient-axes.ts
Normal file
218
src/mol-plugin-state/manager/focus-camera/orient-axes.ts
Normal 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 };
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>}
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -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: {} },
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</>;
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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>}
|
||||
</>;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
});
|
||||
@@ -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}'),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) }>(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user