mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 23:34:23 +08:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
944d370c14 | ||
|
|
74f9aa6af6 | ||
|
|
c20c9c9917 | ||
|
|
4801435d72 | ||
|
|
33fd105ef7 | ||
|
|
3ea3fb8984 | ||
|
|
b4bbc544ca | ||
|
|
5f880e920b | ||
|
|
bcce801dd7 | ||
|
|
00f9dcee4a | ||
|
|
505af2bc96 | ||
|
|
c217aab5fc | ||
|
|
5d5fd0028f | ||
|
|
c88693dfdd | ||
|
|
0a16ec1bd2 | ||
|
|
6f36a3c724 | ||
|
|
71496bd1ef | ||
|
|
c5a99a7c12 | ||
|
|
cfaf33d696 | ||
|
|
e24c76d2bf | ||
|
|
b6273205a2 | ||
|
|
b38193aa19 | ||
|
|
f9cfacae23 | ||
|
|
ac46317dc6 | ||
|
|
2b492a5a61 | ||
|
|
a2133657f0 | ||
|
|
e8de45789f | ||
|
|
3d2bd167ca | ||
|
|
504c8626dc | ||
|
|
f4b29dc7e0 | ||
|
|
b34c5c743b | ||
|
|
c57311d6c0 | ||
|
|
4d786dc697 | ||
|
|
062e3e055a | ||
|
|
1465174a45 | ||
|
|
cc00ada5a3 | ||
|
|
cbf312b62d | ||
|
|
2be3144086 | ||
|
|
da3acd9d19 | ||
|
|
34b048479b | ||
|
|
ca92931bf2 | ||
|
|
0d3daeb823 | ||
|
|
58b1d7e0eb | ||
|
|
c5997ed056 | ||
|
|
da1deee7f3 | ||
|
|
a4eaff3175 | ||
|
|
211cfc0bd3 | ||
|
|
c0f85b691d | ||
|
|
6b93d58ea6 | ||
|
|
5bce423b49 | ||
|
|
01b0dde503 | ||
|
|
ed1bc8cb7d | ||
|
|
abe559261b | ||
|
|
b0cdf22cb8 | ||
|
|
a61ba71f1e | ||
|
|
7061d57559 | ||
|
|
83a1e6c87c | ||
|
|
c18888b8de | ||
|
|
2d80935e00 | ||
|
|
4287d158b6 | ||
|
|
4e9b569178 | ||
|
|
5d626d291b | ||
|
|
afa4a01c44 | ||
|
|
3c4a23c5a3 | ||
|
|
8d92c976d9 | ||
|
|
1a9adfad29 | ||
|
|
a9e9a5974d | ||
|
|
27160aa8fe | ||
|
|
6a71af00cf | ||
|
|
6c0938db50 | ||
|
|
b76173c82f | ||
|
|
0bcf2b1ff4 | ||
|
|
d074415a26 |
72
package-lock.json
generated
72
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "0.2.12",
|
||||
"version": "0.3.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1363,9 +1363,9 @@
|
||||
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.7.tgz",
|
||||
"integrity": "sha512-4jUncNe2tj1nmrO/34PsRpZqYVnRV1svbU78cKhuQKkMntKB/AmdLyGgswcZKjFHEHGpiY8pVD8CuVI55nP54w=="
|
||||
"version": "12.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
|
||||
"integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ=="
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.5.2",
|
||||
@@ -1392,18 +1392,18 @@
|
||||
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.3.tgz",
|
||||
"integrity": "sha512-Ogb2nSn+2qQv5opoCv7Ls5yFxtyrdUYxp5G+SWTrlGk7dmFKw331GiezCgEZj9U7QeXJi1CDtws9pdXU1zUL4g==",
|
||||
"version": "16.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz",
|
||||
"integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "16.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.0.tgz",
|
||||
"integrity": "sha512-OL2lk7LYGjxn4b0efW3Pvf2KBVP0y1v3wip1Bp7nA79NkOpElH98q3WdCEdDj93b2b0zaeBG9DvriuKjIK5xDA==",
|
||||
"version": "16.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.1.tgz",
|
||||
"integrity": "sha512-1S/akvkKr63qIUWVu5IKYou2P9fHLb/P2VAwyxVV85JGaGZTcUniMiTuIqM3lXFB25ej6h+CYEQ27ERVwi6eGA==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@@ -3051,13 +3051,13 @@
|
||||
}
|
||||
},
|
||||
"concurrently": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-4.1.2.tgz",
|
||||
"integrity": "sha512-Kim9SFrNr2jd8/0yNYqDTFALzUX1tvimmwFWxmp/D4mRI+kbqIIwE2RkBDrxS2ic25O1UgQMI5AtBqdtX3ynYg==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.0.0.tgz",
|
||||
"integrity": "sha512-1yDvK8mduTIdxIxV9C60KoiOySUl/lfekpdbI+U5GXaPrgdffEavFa9QZB3vh68oWOpbCC+TuvxXV9YRPMvUrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.4.2",
|
||||
"date-fns": "^1.30.1",
|
||||
"date-fns": "^2.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"read-pkg": "^4.0.1",
|
||||
"rxjs": "^6.5.2",
|
||||
@@ -3099,9 +3099,9 @@
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
|
||||
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.4.1.tgz",
|
||||
"integrity": "sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
@@ -3520,9 +3520,9 @@
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz",
|
||||
"integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg=="
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz",
|
||||
"integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ=="
|
||||
},
|
||||
"currently-unhandled": {
|
||||
"version": "0.4.1",
|
||||
@@ -5813,9 +5813,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"graphql": {
|
||||
"version": "14.5.7",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-14.5.7.tgz",
|
||||
"integrity": "sha512-as410RMJSUFqF8RcH2QWxZ5ioqHzsH9VWnWbaU+UnDXJ/6azMDIYPrtXCBPXd8rlunEVb7W8z6fuUnNHMbFu9A==",
|
||||
"version": "14.5.8",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-14.5.8.tgz",
|
||||
"integrity": "sha512-MMwmi0zlVLQKLdGiMfWkgQD7dY/TUKt4L+zgJ/aR0Howebod3aNgP5JkgvAULiR2HPVZaP2VEElqtdidHweLkg==",
|
||||
"requires": {
|
||||
"iterall": "^1.2.2"
|
||||
}
|
||||
@@ -11341,9 +11341,9 @@
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.9.0.tgz",
|
||||
"integrity": "sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w==",
|
||||
"version": "16.10.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz",
|
||||
"integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -11351,14 +11351,14 @@
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz",
|
||||
"integrity": "sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==",
|
||||
"version": "16.10.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz",
|
||||
"integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.15.0"
|
||||
"scheduler": "^0.16.2"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
@@ -12478,9 +12478,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
|
||||
"integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==",
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz",
|
||||
"integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -13832,9 +13832,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",
|
||||
"integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==",
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
|
||||
"integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
|
||||
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "0.2.12",
|
||||
"version": "0.3.2",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -15,11 +15,11 @@
|
||||
"test": "npm run lint && jest",
|
||||
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
|
||||
"build-tsc": "tsc",
|
||||
"build-extra": "cpx \"src/**/*.{scss,woff,woff2,ttf,otf,eot,svg,html}\" lib/",
|
||||
"build-extra": "cpx \"src/**/*.{scss,woff,woff2,ttf,otf,eot,svg,html,ico}\" lib/",
|
||||
"build-webpack": "webpack --mode production",
|
||||
"watch": "concurrently --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack\"",
|
||||
"watch-tsc": "tsc -watch",
|
||||
"watch-extra": "cpx \"src/**/*.{scss,woff,woff2,ttf,otf,eot,svg,html}\" lib/ --watch",
|
||||
"watch-extra": "cpx \"src/**/*.{scss,woff,woff2,ttf,otf,eot,svg,html,ico}\" lib/ --watch",
|
||||
"watch-webpack": "webpack -w --mode development --display minimal",
|
||||
"serve": "http-server -p 1338",
|
||||
"model-server": "node lib/servers/model/server.js",
|
||||
@@ -65,7 +65,7 @@
|
||||
"devDependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"circular-dependency-plugin": "^5.2.0",
|
||||
"concurrently": "^4.1.2",
|
||||
"concurrently": "^5.0.0",
|
||||
"cpx": "^1.5.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
@@ -86,7 +86,7 @@
|
||||
"style-loader": "^1.0.0",
|
||||
"ts-jest": "^24.1.0",
|
||||
"tslint": "^5.20.0",
|
||||
"typescript": "^3.6.3",
|
||||
"typescript": "^3.6.4",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-cli": "^3.3.9"
|
||||
},
|
||||
@@ -96,20 +96,20 @@
|
||||
"@types/compression": "1.0.1",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/node": "^12.7.7",
|
||||
"@types/node": "^12.7.12",
|
||||
"@types/node-fetch": "^2.5.2",
|
||||
"@types/react": "^16.9.3",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react": "^16.9.5",
|
||||
"@types/react-dom": "^16.9.1",
|
||||
"@types/swagger-ui-dist": "3.0.3",
|
||||
"@types/webgl2": "0.0.5",
|
||||
"argparse": "^1.0.10",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^14.5.7",
|
||||
"graphql": "^14.5.8",
|
||||
"immutable": "^3.8.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react": "^16.10.2",
|
||||
"react-dom": "^16.10.2",
|
||||
"rxjs": "^6.5.3",
|
||||
"swagger-ui-dist": "^3.23.11",
|
||||
"util.promisify": "^1.0.0",
|
||||
|
||||
@@ -19,4 +19,12 @@ export class BasicWrapperControls extends PluginUIComponent {
|
||||
<TransformUpdaterControl nodeRef='ihm-visual' header={{ name: 'I/HM Visual' }} initiallyCollapsed={true} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomToastMessage extends PluginUIComponent {
|
||||
render() {
|
||||
return <>
|
||||
Custom <i>Toast</i> content. No timeout.
|
||||
</>;
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,9 @@
|
||||
addControl('Static Superposition', () => BasicMolStarWrapper.tests.staticSuperposition());
|
||||
addControl('Dynamic Superposition', () => BasicMolStarWrapper.tests.dynamicSuperposition());
|
||||
addControl('Validation Tooltip', () => BasicMolStarWrapper.tests.toggleValidationTooltip());
|
||||
|
||||
addControl('Show Toasts', () => BasicMolStarWrapper.tests.showToasts());
|
||||
addControl('Hide Toasts', () => BasicMolStarWrapper.tests.hideToasts());
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { StripedResidues } from './coloring';
|
||||
// import { BasicWrapperControls } from './controls';
|
||||
import { StaticSuperpositionTestData, buildStaticSuperposition, dynamicSuperpositionTest } from './superposition';
|
||||
import { PDBeStructureQualityReport } from '../../mol-plugin/behavior/dynamic/custom-props';
|
||||
import { CustomToastMessage } from './controls';
|
||||
require('mol-plugin/skin/light.scss')
|
||||
|
||||
type SupportedFormats = 'cif' | 'pdb'
|
||||
@@ -158,6 +159,23 @@ class BasicWrapper {
|
||||
const state = this.plugin.state.behaviorState;
|
||||
const tree = state.build().to(PDBeStructureQualityReport.id).update(PDBeStructureQualityReport, p => ({ ...p, showTooltip: !p.showTooltip }));
|
||||
await PluginCommands.State.Update.dispatch(this.plugin, { state, tree });
|
||||
},
|
||||
showToasts: () => {
|
||||
PluginCommands.Toast.Show.dispatch(this.plugin, {
|
||||
title: 'Toast 1',
|
||||
message: 'This is an example text, timeout 3s',
|
||||
key: 'toast-1',
|
||||
timeoutMs: 3000
|
||||
});
|
||||
PluginCommands.Toast.Show.dispatch(this.plugin, {
|
||||
title: 'Toast 2',
|
||||
message: CustomToastMessage,
|
||||
key: 'toast-2'
|
||||
});
|
||||
},
|
||||
hideToasts: () => {
|
||||
PluginCommands.Toast.Hide.dispatch(this.plugin, { key: 'toast-1' });
|
||||
PluginCommands.Toast.Hide.dispatch(this.plugin, { key: 'toast-2' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/apps/viewer/favicon.ico
Executable file
BIN
src/apps/viewer/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Mol* Viewer</title>
|
||||
<style>
|
||||
* {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import './index.html'
|
||||
import './favicon.ico'
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginCommands } from '../../mol-plugin/command';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
|
||||
@@ -269,6 +269,9 @@ function updateClip(camera: Camera) {
|
||||
// set at least to 5 to avoid slow sphere impostor rendering
|
||||
near = Math.max(5, near)
|
||||
far = Math.max(5, far)
|
||||
} else {
|
||||
near = Math.max(0, near)
|
||||
far = Math.max(0, far)
|
||||
}
|
||||
|
||||
camera.near = near;
|
||||
|
||||
@@ -32,6 +32,7 @@ import { readTexture } from '../mol-gl/compute/util';
|
||||
import { DrawPass } from './passes/draw';
|
||||
import { PickPass } from './passes/pick';
|
||||
import { Task } from '../mol-task';
|
||||
import { ImagePass, ImageProps } from './passes/image';
|
||||
|
||||
export const Canvas3DParams = {
|
||||
cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
|
||||
@@ -74,6 +75,7 @@ interface Canvas3D {
|
||||
downloadScreenshot: () => void
|
||||
getPixelData: (variant: GraphicsRenderVariant) => PixelData
|
||||
setProps: (props: Partial<Canvas3DProps>) => void
|
||||
getImagePass: () => ImagePass
|
||||
|
||||
/** Returns a copy of the current Canvas3D instance props */
|
||||
readonly props: Readonly<Canvas3DProps>
|
||||
@@ -93,10 +95,11 @@ namespace Canvas3D {
|
||||
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) {
|
||||
const gl = getGLContext(canvas, {
|
||||
alpha: false,
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
depth: true,
|
||||
preserveDrawingBuffer: true
|
||||
preserveDrawingBuffer: true,
|
||||
premultipliedAlpha: false,
|
||||
})
|
||||
if (gl === null) throw new Error('Could not create a WebGL rendering context')
|
||||
const input = InputObserver.fromElement(canvas)
|
||||
@@ -127,12 +130,12 @@ namespace Canvas3D {
|
||||
})
|
||||
|
||||
const controls = TrackballControls.create(input, camera, p.trackball)
|
||||
const renderer = Renderer.create(webgl, camera, p.renderer)
|
||||
const renderer = Renderer.create(webgl, p.renderer)
|
||||
const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
|
||||
|
||||
const drawPass = new DrawPass(webgl, renderer, scene, debugHelper)
|
||||
const pickPass = new PickPass(webgl, renderer, scene, 0.5)
|
||||
const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper)
|
||||
const pickPass = new PickPass(webgl, renderer, scene, camera, 0.5)
|
||||
const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
|
||||
const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
|
||||
|
||||
@@ -176,6 +179,7 @@ namespace Canvas3D {
|
||||
|
||||
let didRender = false
|
||||
controls.update(currentTime)
|
||||
Viewport.set(camera.viewport, 0, 0, width, height)
|
||||
const cameraChanged = camera.update()
|
||||
multiSample.update(force || cameraChanged, currentTime)
|
||||
|
||||
@@ -185,9 +189,9 @@ namespace Canvas3D {
|
||||
pickPass.render()
|
||||
break;
|
||||
case 'draw':
|
||||
renderer.setViewport(0, 0, width, height);
|
||||
renderer.setViewport(0, 0, width, height)
|
||||
if (multiSample.enabled) {
|
||||
multiSample.render()
|
||||
multiSample.render(true)
|
||||
} else {
|
||||
drawPass.render(!postprocessing.enabled)
|
||||
if (postprocessing.enabled) postprocessing.render(true)
|
||||
@@ -308,7 +312,7 @@ namespace Canvas3D {
|
||||
getLoci,
|
||||
|
||||
handleResize,
|
||||
resetCamera: (/*dir?: Vec3*/) => {
|
||||
resetCamera: () => {
|
||||
if (scene.isCommiting) {
|
||||
cameraResetRequested = true
|
||||
} else {
|
||||
@@ -347,6 +351,9 @@ namespace Canvas3D {
|
||||
if (props.debug) debugHelper.setProps(props.debug)
|
||||
requestDraw(true)
|
||||
},
|
||||
getImagePass: (props: Partial<ImageProps> = {}) => {
|
||||
return new ImagePass(webgl, renderer, scene, camera, debugHelper, props)
|
||||
},
|
||||
|
||||
get props() {
|
||||
return {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DefaultTrackballBindings = {
|
||||
dragFocusZoom: Binding(Trigger(B.Flag.Auxilary, M.create()), 'Focus and zoom the 3D scene by dragging using ${trigger}'),
|
||||
|
||||
scrollZoom: Binding(Trigger(B.Flag.Auxilary, M.create()), 'Zoom the 3D scene by scrolling using ${trigger}'),
|
||||
scrollFocus: Binding(Trigger(B.Flag.Auxilary, M.create({ shift: true })), 'Focus the 3D scene by dragging using ${trigger}'),
|
||||
scrollFocus: Binding(Trigger(B.Flag.Auxilary, M.create({ shift: true })), 'Focus the 3D scene by scrolling using ${trigger}'),
|
||||
scrollFocusZoom: Binding.Empty,
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const TrackballControlsParams = {
|
||||
zoomSpeed: PD.Numeric(6.0, { min: 0.1, max: 10, step: 0.1 }),
|
||||
panSpeed: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
|
||||
|
||||
spin: PD.Boolean(false),
|
||||
spin: PD.Boolean(false, { description: 'Spin the 3D scene around the x-axis in view space' }),
|
||||
spinSpeed: PD.Numeric(1, { min: -100, max: 100, step: 1 }),
|
||||
|
||||
staticMoving: PD.Boolean(true, { isHidden: true }),
|
||||
|
||||
@@ -10,6 +10,7 @@ import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
|
||||
import { createTexture, Texture } from '../../mol-gl/webgl/texture';
|
||||
import { Camera } from '../camera';
|
||||
|
||||
export class DrawPass {
|
||||
colorTarget: RenderTarget
|
||||
@@ -18,11 +19,11 @@ export class DrawPass {
|
||||
|
||||
private depthTarget: RenderTarget | null
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private debugHelper: BoundingSphereHelper) {
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper) {
|
||||
const { gl, extensions } = webgl
|
||||
const width = gl.drawingBufferWidth
|
||||
const height = gl.drawingBufferHeight
|
||||
this.colorTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.colorTarget = createRenderTarget(webgl, width, height)
|
||||
this.packedDepth = !extensions.depthTexture
|
||||
this.depthTarget = this.packedDepth ? createRenderTarget(webgl, width, height) : null
|
||||
this.depthTexture = this.depthTarget ? this.depthTarget.texture : createTexture(webgl, 'image-depth', 'depth', 'ushort', 'nearest')
|
||||
@@ -42,28 +43,28 @@ export class DrawPass {
|
||||
}
|
||||
|
||||
render(toDrawingBuffer: boolean) {
|
||||
const { webgl, renderer, scene, debugHelper, colorTarget, depthTarget } = this
|
||||
const { gl } = webgl
|
||||
const { webgl, renderer, scene, camera, debugHelper, colorTarget, depthTarget } = this
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
colorTarget.bind()
|
||||
}
|
||||
renderer.setViewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
renderer.render(scene, 'color', true)
|
||||
|
||||
renderer.setViewport(0, 0, colorTarget.width, colorTarget.height)
|
||||
renderer.render(scene, camera, 'color', true)
|
||||
if (debugHelper.isEnabled) {
|
||||
debugHelper.syncVisibility()
|
||||
renderer.render(debugHelper.scene, 'color', false)
|
||||
renderer.render(debugHelper.scene, camera, 'color', false)
|
||||
}
|
||||
|
||||
// do a depth pass if not rendering to drawing buffer and
|
||||
// extensions.depthTexture is unsupported (i.e. depthTarget is set)
|
||||
if (!toDrawingBuffer && depthTarget) {
|
||||
depthTarget.bind()
|
||||
renderer.render(scene, 'depth', true)
|
||||
renderer.render(scene, camera, 'depth', true)
|
||||
if (debugHelper.isEnabled) {
|
||||
debugHelper.syncVisibility()
|
||||
renderer.render(debugHelper.scene, 'depth', false)
|
||||
renderer.render(debugHelper.scene, camera, 'depth', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
src/mol-canvas3d/passes/image.ts
Normal file
91
src/mol-canvas3d/passes/image.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { DrawPass } from './draw'
|
||||
import { PostprocessingPass, PostprocessingParams } from './postprocessing'
|
||||
import { MultiSamplePass, MultiSampleParams } from './multi-sample'
|
||||
import { Camera } from '../camera';
|
||||
import { Viewport } from '../camera/util';
|
||||
|
||||
export const ImageParams = {
|
||||
multiSample: PD.Group(MultiSampleParams),
|
||||
postprocessing: PD.Group(PostprocessingParams),
|
||||
}
|
||||
export type ImageProps = PD.Values<typeof ImageParams>
|
||||
|
||||
export class ImagePass {
|
||||
private _width = 1024
|
||||
private _height = 768
|
||||
private _camera = new Camera()
|
||||
|
||||
private _colorTarget: RenderTarget
|
||||
get colorTarget() { return this._colorTarget }
|
||||
|
||||
readonly drawPass: DrawPass
|
||||
private readonly postprocessing: PostprocessingPass
|
||||
private readonly multiSample: MultiSamplePass
|
||||
|
||||
get width() { return this._width }
|
||||
get height() { return this._height }
|
||||
|
||||
constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, props: Partial<ImageProps>) {
|
||||
const p = { ...PD.getDefaultValues(ImageParams), ...props }
|
||||
|
||||
this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper)
|
||||
this.postprocessing = new PostprocessingPass(webgl, this._camera, this.drawPass, p.postprocessing)
|
||||
this.multiSample = new MultiSamplePass(webgl, this._camera, this.drawPass, this.postprocessing, p.multiSample)
|
||||
|
||||
this.setSize(this._width, this._height)
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this._width = width
|
||||
this._height = height
|
||||
|
||||
this.drawPass.setSize(width, height)
|
||||
this.postprocessing.setSize(width, height)
|
||||
this.multiSample.setSize(width, height)
|
||||
}
|
||||
|
||||
setProps(props: Partial<ImageProps> = {}) {
|
||||
if (props.postprocessing) this.postprocessing.setProps(props.postprocessing)
|
||||
if (props.multiSample) this.multiSample.setProps(props.multiSample)
|
||||
}
|
||||
|
||||
render() {
|
||||
Camera.copySnapshot(this._camera.state, this.camera.state)
|
||||
Viewport.set(this._camera.viewport, 0, 0, this._width, this._height)
|
||||
this._camera.update()
|
||||
|
||||
this.renderer.setViewport(0, 0, this._width, this._height);
|
||||
|
||||
if (this.multiSample.enabled) {
|
||||
this.multiSample.render(false)
|
||||
this._colorTarget = this.multiSample.colorTarget
|
||||
} else {
|
||||
this.drawPass.render(false)
|
||||
if (this.postprocessing.enabled) {
|
||||
this.postprocessing.render(false)
|
||||
this._colorTarget = this.postprocessing.target
|
||||
} else {
|
||||
this._colorTarget = this.drawPass.colorTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getImageData(width: number, height: number) {
|
||||
this.setSize(width, height)
|
||||
this.render()
|
||||
const pd = this.colorTarget.getPixelData()
|
||||
return new ImageData(new Uint8ClampedArray(pd.array), pd.width, pd.height)
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
|
||||
|
||||
export class MultiSamplePass {
|
||||
props: MultiSampleProps
|
||||
colorTarget: RenderTarget
|
||||
|
||||
private composeTarget: RenderTarget
|
||||
private holdTarget: RenderTarget
|
||||
@@ -65,6 +66,7 @@ export class MultiSamplePass {
|
||||
|
||||
constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) {
|
||||
const { gl } = webgl
|
||||
this.colorTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.composeTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.holdTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture)
|
||||
@@ -92,6 +94,7 @@ export class MultiSamplePass {
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.colorTarget.setSize(width, height)
|
||||
this.composeTarget.setSize(width, height)
|
||||
this.holdTarget.setSize(width, height)
|
||||
ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height))
|
||||
@@ -102,15 +105,15 @@ export class MultiSamplePass {
|
||||
if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel
|
||||
}
|
||||
|
||||
render() {
|
||||
render(toDrawingBuffer: boolean) {
|
||||
if (this.props.mode === 'temporal') {
|
||||
this.renderTemporalMultiSample()
|
||||
this.renderTemporalMultiSample(toDrawingBuffer)
|
||||
} else {
|
||||
this.renderMultiSample()
|
||||
this.renderMultiSample(toDrawingBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
private renderMultiSample() {
|
||||
private renderMultiSample(toDrawingBuffer: boolean) {
|
||||
const { camera, compose, composeTarget, drawPass, postprocessing, webgl } = this
|
||||
const { gl, state } = webgl
|
||||
|
||||
@@ -168,7 +171,11 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.tColor, composeTarget.texture)
|
||||
compose.update()
|
||||
|
||||
webgl.unbindFramebuffer()
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
this.colorTarget.bind()
|
||||
}
|
||||
gl.viewport(0, 0, width, height)
|
||||
state.disable(gl.BLEND)
|
||||
compose.render()
|
||||
@@ -177,7 +184,7 @@ export class MultiSamplePass {
|
||||
camera.update()
|
||||
}
|
||||
|
||||
private renderTemporalMultiSample() {
|
||||
private renderTemporalMultiSample(toDrawingBuffer: boolean) {
|
||||
const { camera, compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this
|
||||
const { gl, state } = webgl
|
||||
|
||||
@@ -252,7 +259,11 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.uWeight, 1.0)
|
||||
ValueCell.update(compose.values.tColor, composeTarget.texture)
|
||||
compose.update()
|
||||
webgl.unbindFramebuffer()
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
this.colorTarget.bind()
|
||||
}
|
||||
gl.viewport(0, 0, width, height)
|
||||
state.disable(gl.BLEND)
|
||||
compose.render()
|
||||
@@ -261,7 +272,11 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight)
|
||||
ValueCell.update(compose.values.tColor, holdTarget.texture)
|
||||
compose.update()
|
||||
webgl.unbindFramebuffer()
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
this.colorTarget.bind()
|
||||
}
|
||||
gl.viewport(0, 0, width, height)
|
||||
if (accumulationWeight === 0) state.disable(gl.BLEND)
|
||||
else state.enable(gl.BLEND)
|
||||
|
||||
@@ -10,6 +10,7 @@ import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { decodeFloatRGB } from '../../mol-util/float-packing';
|
||||
import { Camera } from '../camera';
|
||||
|
||||
export class PickPass {
|
||||
pickDirty = true
|
||||
@@ -26,7 +27,7 @@ export class PickPass {
|
||||
private pickWidth: number
|
||||
private pickHeight: number
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private pickBaseScale: number) {
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private pickBaseScale: number) {
|
||||
const { gl } = webgl
|
||||
const width = gl.drawingBufferWidth
|
||||
const height = gl.drawingBufferHeight
|
||||
@@ -64,14 +65,14 @@ export class PickPass {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { renderer, scene } = this
|
||||
const { renderer, scene, camera } = this
|
||||
renderer.setViewport(0, 0, this.pickWidth, this.pickHeight);
|
||||
this.objectPickTarget.bind();
|
||||
renderer.render(scene, 'pickObject', true);
|
||||
renderer.render(scene, camera, 'pickObject', true);
|
||||
this.instancePickTarget.bind();
|
||||
renderer.render(scene, 'pickInstance', true);
|
||||
renderer.render(scene, camera, 'pickInstance', true);
|
||||
this.groupPickTarget.bind();
|
||||
renderer.render(scene, 'pickGroup', true);
|
||||
renderer.render(scene, camera, 'pickGroup', true);
|
||||
|
||||
this.pickDirty = false
|
||||
}
|
||||
|
||||
@@ -4,16 +4,57 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
/** resize canvas to container element */
|
||||
/** Set canvas size taking `devicePixelRatio` into account */
|
||||
export function setCanvasSize(canvas: HTMLCanvasElement, width: number, height: number) {
|
||||
canvas.width = window.devicePixelRatio * width
|
||||
canvas.height = window.devicePixelRatio * height
|
||||
Object.assign(canvas.style, { width: `${width}px`, height: `${height}px` })
|
||||
}
|
||||
|
||||
/** Resize canvas to container element taking `devicePixelRatio` into account */
|
||||
export function resizeCanvas (canvas: HTMLCanvasElement, container: Element) {
|
||||
let w = window.innerWidth
|
||||
let h = window.innerHeight
|
||||
let width = window.innerWidth
|
||||
let height = window.innerHeight
|
||||
if (container !== document.body) {
|
||||
let bounds = container.getBoundingClientRect()
|
||||
w = bounds.right - bounds.left
|
||||
h = bounds.bottom - bounds.top
|
||||
width = bounds.right - bounds.left
|
||||
height = bounds.bottom - bounds.top
|
||||
}
|
||||
canvas.width = window.devicePixelRatio * w
|
||||
canvas.height = window.devicePixelRatio * h
|
||||
Object.assign(canvas.style, { width: `${w}px`, height: `${h}px` })
|
||||
setCanvasSize(canvas, width, height)
|
||||
}
|
||||
|
||||
function _canvasToBlob(canvas: HTMLCanvasElement, callback: BlobCallback, type?: string, quality?: any) {
|
||||
const bin = atob(canvas.toDataURL(type, quality).split(',')[1])
|
||||
const len = bin.length
|
||||
const len32 = len >> 2
|
||||
const a8 = new Uint8Array(len)
|
||||
const a32 = new Uint32Array( a8.buffer, 0, len32 )
|
||||
|
||||
let j = 0
|
||||
for (let i = 0; i < len32; ++i) {
|
||||
a32[i] = bin.charCodeAt(j++) |
|
||||
bin.charCodeAt(j++) << 8 |
|
||||
bin.charCodeAt(j++) << 16 |
|
||||
bin.charCodeAt(j++) << 24
|
||||
}
|
||||
|
||||
let tailLength = len & 3;
|
||||
while (tailLength--) a8[j] = bin.charCodeAt(j++)
|
||||
|
||||
callback(new Blob([a8], { type: type || 'image/png' }));
|
||||
}
|
||||
|
||||
export async function canvasToBlob(canvas: HTMLCanvasElement, type?: string, quality?: any): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (blob: Blob | null) => {
|
||||
if (blob) resolve(blob)
|
||||
else reject('no blob returned')
|
||||
}
|
||||
|
||||
if (!HTMLCanvasElement.prototype.toBlob) {
|
||||
_canvasToBlob(canvas, callback, type, quality)
|
||||
} else {
|
||||
canvas.toBlob(callback, type, quality)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -101,9 +101,9 @@ export namespace MeshBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
export function addCage(state: State, t: Mat4, cage: Cage, radius: number, detail: number) {
|
||||
export function addCage(state: State, t: Mat4, cage: Cage, radius: number, detail: number, radialSegments: number) {
|
||||
const { vertices: va, edges: ea } = cage
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius }
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments }
|
||||
for (let i = 0, il = ea.length; i < il; i += 2) {
|
||||
Vec3.fromArray(tmpVecA, va, ea[i] * 3)
|
||||
Vec3.fromArray(tmpVecB, va, ea[i + 1] * 3)
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Mat4, Vec3 } from '../../mol-math/linear-algebra'
|
||||
import { NumberArray } from '../../mol-util/type-helpers'
|
||||
|
||||
export interface Cage {
|
||||
readonly vertices: ArrayLike<number>
|
||||
readonly edges: ArrayLike<number>
|
||||
@@ -11,4 +14,24 @@ export interface Cage {
|
||||
|
||||
export function createCage(vertices: ArrayLike<number>, edges: ArrayLike<number>): Cage {
|
||||
return { vertices, edges }
|
||||
}
|
||||
|
||||
export function copyCage(cage: Cage): Cage {
|
||||
return {
|
||||
vertices: new Float32Array(cage.vertices),
|
||||
edges: new Uint32Array(cage.edges)
|
||||
}
|
||||
}
|
||||
|
||||
const tmpV = Vec3.zero()
|
||||
|
||||
/** Transform primitive in-place */
|
||||
export function transformCage(cage: Cage, t: Mat4) {
|
||||
const { vertices } = cage
|
||||
for (let i = 0, il = vertices.length; i < il; i += 3) {
|
||||
// position
|
||||
Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, vertices, i), t)
|
||||
Vec3.toArray(tmpV, vertices as NumberArray, i)
|
||||
}
|
||||
return cage
|
||||
}
|
||||
@@ -30,6 +30,14 @@ export function createPrimitive(vertices: ArrayLike<number>, indices: ArrayLike<
|
||||
return builder.getPrimitive()
|
||||
}
|
||||
|
||||
export function copyPrimitive(primitive: Primitive): Primitive {
|
||||
return {
|
||||
vertices: new Float32Array(primitive.vertices),
|
||||
normals: new Float32Array(primitive.normals),
|
||||
indices: new Uint32Array(primitive.indices)
|
||||
}
|
||||
}
|
||||
|
||||
export interface PrimitiveBuilder {
|
||||
add(a: Vec3, b: Vec3, c: Vec3): void
|
||||
getPrimitive(): Primitive
|
||||
|
||||
@@ -78,10 +78,22 @@ export function PentagonalPrism() {
|
||||
|
||||
let hexagonalPrism: Primitive
|
||||
export function HexagonalPrism() {
|
||||
if (!hexagonalPrism) hexagonalPrism = Prism(polygon(6, true))
|
||||
if (!hexagonalPrism) hexagonalPrism = Prism(polygon(6, false))
|
||||
return hexagonalPrism
|
||||
}
|
||||
|
||||
let shiftedHexagonalPrism: Primitive
|
||||
export function ShiftedHexagonalPrism() {
|
||||
if (!shiftedHexagonalPrism) shiftedHexagonalPrism = Prism(polygon(6, true))
|
||||
return shiftedHexagonalPrism
|
||||
}
|
||||
|
||||
let heptagonalPrism: Primitive
|
||||
export function HeptagonalPrism() {
|
||||
if (!heptagonalPrism) heptagonalPrism = Prism(polygon(7, false))
|
||||
return heptagonalPrism
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
@@ -133,6 +145,6 @@ export function PentagonalPrismCage() {
|
||||
|
||||
let hexagonalPrismCage: Cage
|
||||
export function HexagonalPrismCage() {
|
||||
if (!hexagonalPrismCage) hexagonalPrismCage = PrismCage(polygon(6, true))
|
||||
if (!hexagonalPrismCage) hexagonalPrismCage = PrismCage(polygon(6, false))
|
||||
return hexagonalPrismCage
|
||||
}
|
||||
@@ -30,7 +30,7 @@ function createRenderer(gl: WebGLRenderingContext) {
|
||||
const camera = new Camera({
|
||||
position: Vec3.create(0, 0, 50)
|
||||
})
|
||||
const renderer = Renderer.create(ctx, camera)
|
||||
const renderer = Renderer.create(ctx)
|
||||
return { ctx, camera, renderer }
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ export const GlobalUniformSchema = {
|
||||
uFogFar: UniformSpec('f'),
|
||||
uFogColor: UniformSpec('v3'),
|
||||
|
||||
uTransparentBackground: UniformSpec('i'),
|
||||
uPickingAlphaThreshold: UniformSpec('f'),
|
||||
uInteriorDarkening: UniformSpec('f'),
|
||||
}
|
||||
|
||||
@@ -38,14 +38,15 @@ interface Renderer {
|
||||
readonly props: Readonly<RendererProps>
|
||||
|
||||
clear: () => void
|
||||
render: (scene: Scene, variant: GraphicsRenderVariant, clear: boolean) => void
|
||||
render: (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean) => void
|
||||
setProps: (props: Partial<RendererProps>) => void
|
||||
setViewport: (x: number, y: number, width: number, height: number) => void
|
||||
dispose: () => void
|
||||
}
|
||||
|
||||
export const RendererParams = {
|
||||
backgroundColor: PD.Color(Color(0x000000)),
|
||||
backgroundColor: PD.Color(Color(0x000000), { description: 'Background color of the 3D canvas' }),
|
||||
transparentBackground: PD.Boolean(false, { description: 'Background opacity of the 3D canvas' }),
|
||||
pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
|
||||
interiorDarkening: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
|
||||
|
||||
@@ -59,39 +60,40 @@ export const RendererParams = {
|
||||
export type RendererProps = PD.Values<typeof RendererParams>
|
||||
|
||||
namespace Renderer {
|
||||
export function create(ctx: WebGLContext, camera: Camera, props: Partial<RendererProps> = {}): Renderer {
|
||||
export function create(ctx: WebGLContext, props: Partial<RendererProps> = {}): Renderer {
|
||||
const { gl, state, stats } = ctx
|
||||
const p = deepClone({ ...PD.getDefaultValues(RendererParams), ...props })
|
||||
|
||||
const viewport = Viewport()
|
||||
const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor)
|
||||
|
||||
const view = Mat4.clone(camera.view)
|
||||
const invView = Mat4.invert(Mat4.identity(), view)
|
||||
const modelView = Mat4.clone(camera.view)
|
||||
const invModelView = Mat4.invert(Mat4.identity(), modelView)
|
||||
const invProjection = Mat4.invert(Mat4.identity(), camera.projection)
|
||||
const modelViewProjection = Mat4.mul(Mat4.identity(), modelView, camera.projection)
|
||||
const invModelViewProjection = Mat4.invert(Mat4.identity(), modelViewProjection)
|
||||
const view = Mat4()
|
||||
const invView = Mat4()
|
||||
const modelView = Mat4()
|
||||
const invModelView = Mat4()
|
||||
const invProjection = Mat4()
|
||||
const modelViewProjection = Mat4()
|
||||
const invModelViewProjection = Mat4()
|
||||
|
||||
const viewOffset = camera.viewOffset.enabled ? Vec2.create(camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2()
|
||||
const viewOffset = Vec2()
|
||||
|
||||
const globalUniforms: GlobalUniformValues = {
|
||||
uModel: ValueCell.create(Mat4.identity()),
|
||||
uView: ValueCell.create(camera.view),
|
||||
uView: ValueCell.create(view),
|
||||
uInvView: ValueCell.create(invView),
|
||||
uModelView: ValueCell.create(modelView),
|
||||
uInvModelView: ValueCell.create(invModelView),
|
||||
uInvProjection: ValueCell.create(invProjection),
|
||||
uProjection: ValueCell.create(Mat4.clone(camera.projection)),
|
||||
uProjection: ValueCell.create(Mat4()),
|
||||
uModelViewProjection: ValueCell.create(modelViewProjection),
|
||||
uInvModelViewProjection: ValueCell.create(invModelViewProjection),
|
||||
|
||||
uIsOrtho: ValueCell.create(camera.state.mode === 'orthographic' ? 1 : 0),
|
||||
uIsOrtho: ValueCell.create(1),
|
||||
uViewOffset: ValueCell.create(viewOffset),
|
||||
|
||||
uPixelRatio: ValueCell.create(ctx.pixelRatio),
|
||||
uViewportHeight: ValueCell.create(viewport.height),
|
||||
uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
|
||||
uViewOffset: ValueCell.create(viewOffset),
|
||||
|
||||
uLightIntensity: ValueCell.create(p.lightIntensity),
|
||||
uAmbientIntensity: ValueCell.create(p.ambientIntensity),
|
||||
@@ -100,13 +102,14 @@ namespace Renderer {
|
||||
uRoughness: ValueCell.create(p.roughness),
|
||||
uReflectivity: ValueCell.create(p.reflectivity),
|
||||
|
||||
uCameraPosition: ValueCell.create(Vec3.clone(camera.state.position)),
|
||||
uNear: ValueCell.create(camera.near),
|
||||
uFar: ValueCell.create(camera.far),
|
||||
uFogNear: ValueCell.create(camera.fogNear),
|
||||
uFogFar: ValueCell.create(camera.fogFar),
|
||||
uCameraPosition: ValueCell.create(Vec3()),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uFogNear: ValueCell.create(1),
|
||||
uFogFar: ValueCell.create(10000),
|
||||
uFogColor: ValueCell.create(bgColor),
|
||||
|
||||
uTransparentBackground: ValueCell.create(p.transparentBackground ? 1 : 0),
|
||||
uPickingAlphaThreshold: ValueCell.create(p.pickingAlphaThreshold),
|
||||
uInteriorDarkening: ValueCell.create(p.interiorDarkening),
|
||||
}
|
||||
@@ -158,7 +161,7 @@ namespace Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
const render = (scene: Scene, variant: GraphicsRenderVariant, clear: boolean) => {
|
||||
const render = (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean) => {
|
||||
ValueCell.update(globalUniforms.uModel, scene.view)
|
||||
ValueCell.update(globalUniforms.uView, camera.view)
|
||||
ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view))
|
||||
@@ -191,7 +194,7 @@ namespace Renderer {
|
||||
|
||||
if (clear) {
|
||||
if (variant === 'color') {
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0)
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], p.transparentBackground ? 0 : 1)
|
||||
} else {
|
||||
state.clearColor(1, 1, 1, 1)
|
||||
}
|
||||
@@ -204,7 +207,7 @@ namespace Renderer {
|
||||
if (r.state.opaque) renderObject(r, variant)
|
||||
}
|
||||
|
||||
state.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE)
|
||||
state.enable(gl.BLEND)
|
||||
for (let i = 0, il = renderables.length; i < il; ++i) {
|
||||
const r = renderables[i]
|
||||
@@ -224,7 +227,7 @@ namespace Renderer {
|
||||
clear: () => {
|
||||
state.depthMask(true)
|
||||
state.colorMask(true, true, true, true)
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0)
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], p.transparentBackground ? 0 : 1)
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||
},
|
||||
render,
|
||||
@@ -243,6 +246,10 @@ namespace Renderer {
|
||||
Color.toVec3Normalized(bgColor, p.backgroundColor)
|
||||
ValueCell.update(globalUniforms.uFogColor, Vec3.copy(globalUniforms.uFogColor.ref.value, bgColor))
|
||||
}
|
||||
if (props.transparentBackground !== undefined && props.transparentBackground !== p.transparentBackground) {
|
||||
p.transparentBackground = props.transparentBackground
|
||||
ValueCell.update(globalUniforms.uTransparentBackground, p.transparentBackground ? 1 : 0)
|
||||
}
|
||||
if (props.lightIntensity !== undefined && props.lightIntensity !== p.lightIntensity) {
|
||||
p.lightIntensity = props.lightIntensity
|
||||
ValueCell.update(globalUniforms.uLightIntensity, p.lightIntensity)
|
||||
|
||||
@@ -2,10 +2,11 @@ export default `
|
||||
#ifdef dUseFog
|
||||
float depth = length(vViewPosition);
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, depth);
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
|
||||
float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
|
||||
if (fogAlpha < 0.01)
|
||||
discard;
|
||||
gl_FragColor = vec4(gl_FragColor.rgb, fogAlpha);
|
||||
if (uTransparentBackground == 0) {
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
|
||||
} else {
|
||||
float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
|
||||
gl_FragColor.a = fogAlpha;
|
||||
}
|
||||
#endif
|
||||
`
|
||||
@@ -36,7 +36,7 @@ geometry.normal = normal;
|
||||
geometry.viewDir = normalize(vViewPosition);
|
||||
|
||||
IncidentLight directLight;
|
||||
directLight.direction = geometry.viewDir;
|
||||
directLight.direction = vec3(0.0, 0.0, -1.0);
|
||||
directLight.color = vec3(uLightIntensity);
|
||||
|
||||
RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
|
||||
|
||||
@@ -5,7 +5,11 @@ uniform int uGroupCount;
|
||||
|
||||
uniform vec3 uHighlightColor;
|
||||
uniform vec3 uSelectColor;
|
||||
varying float vMarker;
|
||||
#if __VERSION__ != 300
|
||||
varying float vMarker;
|
||||
#else
|
||||
flat in float vMarker;
|
||||
#endif
|
||||
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
@@ -18,6 +22,7 @@ uniform vec3 uFogColor;
|
||||
uniform float uAlpha;
|
||||
uniform float uPickingAlphaThreshold;
|
||||
uniform int uPickable;
|
||||
uniform int uTransparentBackground;
|
||||
|
||||
uniform float uInteriorDarkening;
|
||||
`
|
||||
@@ -8,7 +8,11 @@ uniform int uGroupCount;
|
||||
|
||||
uniform vec2 uMarkerTexDim;
|
||||
uniform sampler2D tMarker;
|
||||
varying float vMarker;
|
||||
#if __VERSION__ != 300
|
||||
varying float vMarker;
|
||||
#else
|
||||
flat out float vMarker;
|
||||
#endif
|
||||
|
||||
varying vec3 vViewPosition;
|
||||
`
|
||||
@@ -124,8 +124,8 @@ void main(void){
|
||||
#elif defined(dColorType_depth)
|
||||
gl_FragColor = material;
|
||||
#else
|
||||
vec3 normal = cameraNormal;
|
||||
vec3 vViewPosition = -cameraPos;
|
||||
vec3 normal = -cameraNormal;
|
||||
vec3 vViewPosition = cameraPos;
|
||||
#include apply_light_color
|
||||
|
||||
if (interior) {
|
||||
|
||||
@@ -190,6 +190,7 @@ export interface WebGLContext {
|
||||
readonly framebufferCache: FramebufferCache
|
||||
|
||||
readonly maxTextureSize: number
|
||||
readonly maxRenderbufferSize: number
|
||||
readonly maxDrawBuffers: number
|
||||
|
||||
unbindFramebuffer: () => void
|
||||
@@ -212,6 +213,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
|
||||
|
||||
const parameters = {
|
||||
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
|
||||
maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
|
||||
maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) as number : 0,
|
||||
maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number,
|
||||
}
|
||||
@@ -274,6 +276,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
|
||||
framebufferCache,
|
||||
|
||||
get maxTextureSize () { return parameters.maxTextureSize },
|
||||
get maxRenderbufferSize () { return parameters.maxRenderbufferSize },
|
||||
get maxDrawBuffers () { return parameters.maxDrawBuffers },
|
||||
|
||||
unbindFramebuffer: () => unbindFramebuffer(gl),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Vec3, Mat4 } from '../../linear-algebra'
|
||||
@@ -9,9 +10,10 @@ import { SpacegroupName, TransformData, GroupData, getSpacegroupIndex, OperatorD
|
||||
import { SymmetryOperator } from '../../geometry';
|
||||
|
||||
interface SpacegroupCell {
|
||||
// zero based spacegroup number
|
||||
/** Zero based spacegroup number */
|
||||
readonly index: number,
|
||||
readonly size: Vec3,
|
||||
readonly volume: number,
|
||||
readonly anglesInRadians: Vec3,
|
||||
/** Transfrom cartesian -> fractional coordinates within the cell */
|
||||
readonly toFractional: Mat4,
|
||||
@@ -34,7 +36,7 @@ namespace SpacegroupCell {
|
||||
return cell.index === 0 && cell.size[0] === 1 && cell.size[1] === 1 && cell.size[1] === 1;
|
||||
}
|
||||
|
||||
// returns Zero cell if the spacegroup does not exist
|
||||
/** Returns Zero cell if the spacegroup does not exist */
|
||||
export function create(nameOrNumber: number | string | SpacegroupName, size: Vec3, anglesInRadians: Vec3): SpacegroupCell {
|
||||
const index = getSpacegroupIndex(nameOrNumber);
|
||||
if (index < 0) {
|
||||
@@ -42,6 +44,8 @@ namespace SpacegroupCell {
|
||||
return Zero;
|
||||
}
|
||||
|
||||
const volume = size[0] * size[1] * size[2]
|
||||
|
||||
const alpha = anglesInRadians[0];
|
||||
const beta = anglesInRadians[1];
|
||||
const gamma = anglesInRadians[2];
|
||||
@@ -64,13 +68,13 @@ namespace SpacegroupCell {
|
||||
]);
|
||||
const toFractional = Mat4.invert(Mat4.zero(), fromFractional)!;
|
||||
|
||||
return { index, size, anglesInRadians, toFractional, fromFractional };
|
||||
return { index, size, volume, anglesInRadians, toFractional, fromFractional };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace Spacegroup {
|
||||
// P1 with [1, 1, 1] cell.
|
||||
/** P1 with [1, 1, 1] cell */
|
||||
export const ZeroP1 = create(SpacegroupCell.Zero);
|
||||
|
||||
export function create(cell: SpacegroupCell): Spacegroup {
|
||||
@@ -78,18 +82,56 @@ namespace Spacegroup {
|
||||
return { name: SpacegroupNames[cell.index], cell, operators };
|
||||
}
|
||||
|
||||
const _tempVec = Vec3.zero(), _tempMat = Mat4.zero();
|
||||
export function updateOperatorMatrix(spacegroup: Spacegroup, index: number, i: number, j: number, k: number, target: Mat4) {
|
||||
_tempVec[0] = i;
|
||||
_tempVec[1] = j;
|
||||
_tempVec[2] = k;
|
||||
const _ijkVec = Vec3();
|
||||
const _tempMat = Mat4();
|
||||
export function setOperatorMatrix(spacegroup: Spacegroup, index: number, i: number, j: number, k: number, target: Mat4) {
|
||||
Vec3.set(_ijkVec, i, j, k);
|
||||
|
||||
Mat4.fromTranslation(_tempMat, _tempVec);
|
||||
return Mat4.mul(target, Mat4.mul(target, Mat4.mul(target, spacegroup.cell.fromFractional, _tempMat), spacegroup.operators[index]), spacegroup.cell.toFractional);
|
||||
Mat4.fromTranslation(_tempMat, _ijkVec);
|
||||
return Mat4.mul(
|
||||
target,
|
||||
Mat4.mul(
|
||||
target,
|
||||
Mat4.mul(target, spacegroup.cell.fromFractional, _tempMat),
|
||||
spacegroup.operators[index]
|
||||
),
|
||||
spacegroup.cell.toFractional
|
||||
);
|
||||
}
|
||||
|
||||
export function getSymmetryOperator(spacegroup: Spacegroup, index: number, i: number, j: number, k: number): SymmetryOperator {
|
||||
const operator = updateOperatorMatrix(spacegroup, index, i, j, k, Mat4.zero());
|
||||
const operator = setOperatorMatrix(spacegroup, index, i, j, k, Mat4.zero());
|
||||
return SymmetryOperator.create(`${index + 1}_${5 + i}${5 + j}${5 + k}`, operator, { id: '', operList: [] }, '', Vec3.create(i, j, k), index);
|
||||
}
|
||||
|
||||
const _translationRef = Vec3()
|
||||
const _translationRefSymop = Vec3()
|
||||
const _translationSymop = Vec3()
|
||||
export function setOperatorMatrixRef(spacegroup: Spacegroup, index: number, i: number, j: number, k: number, ref: Vec3, target: Mat4) {
|
||||
Vec3.set(_ijkVec, i, j, k);
|
||||
Vec3.floor(_translationRef, ref)
|
||||
|
||||
Mat4.copy(target, spacegroup.operators[index])
|
||||
|
||||
Vec3.floor(_translationRefSymop, Vec3.transformMat4(_translationRefSymop, ref, target))
|
||||
|
||||
Mat4.getTranslation(_translationSymop, target)
|
||||
Vec3.sub(_translationSymop, _translationSymop, _translationRefSymop)
|
||||
Vec3.add(_translationSymop, _translationSymop, _translationRef)
|
||||
Vec3.add(_translationSymop, _translationSymop, _ijkVec)
|
||||
|
||||
Mat4.setTranslation(target, _translationSymop)
|
||||
Mat4.mul(target, spacegroup.cell.fromFractional, target)
|
||||
Mat4.mul(target, target, spacegroup.cell.toFractional)
|
||||
return target
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Symmetry operator for transformation around the given
|
||||
* reference point `ref` in fractional coordinates
|
||||
*/
|
||||
export function getSymmetryOperatorRef(spacegroup: Spacegroup, index: number, i: number, j: number, k: number, ref: Vec3) {
|
||||
const operator = setOperatorMatrixRef(spacegroup, index, i, j, k, ref, Mat4.zero());
|
||||
return SymmetryOperator.create(`${index + 1}_${5 + i}${5 + j}${5 + k}`, operator, { id: '', operList: [] }, '', Vec3.create(i, j, k), index);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*/
|
||||
|
||||
import { Mat4, Vec3 } from '../3d'
|
||||
import { Mat4, Vec3, EPSILON } from '../3d'
|
||||
import { NumberArray } from '../../../mol-util/type-helpers';
|
||||
|
||||
interface Mat3 extends Array<number> { [d: number]: number, '@type': 'mat3', length: 9 }
|
||||
@@ -118,6 +118,11 @@ namespace Mat3 {
|
||||
return out;
|
||||
}
|
||||
|
||||
const _id = identity();
|
||||
export function isIdentity(m: Mat3, eps?: number) {
|
||||
return areEqual(m, _id, typeof eps === 'undefined' ? EPSILON : eps);
|
||||
}
|
||||
|
||||
export function hasNaN(m: Mat3) {
|
||||
for (let i = 0; i < 9; i++) if (isNaN(m[i])) return true
|
||||
return false
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getSecondaryStructure } from './secondary-structure';
|
||||
import { getSequence } from './sequence';
|
||||
import { sortAtomSite } from './sort';
|
||||
import { StructConn } from './bonds/struct_conn';
|
||||
import { getMoleculeType, MoleculeType, getEntityType, getEntitySubtype } from '../../../mol-model/structure/model/types';
|
||||
import { getMoleculeType, MoleculeType, getEntityType, getEntitySubtype, getDefaultChemicalComponent } from '../../../mol-model/structure/model/types';
|
||||
import { ModelFormat } from '../format';
|
||||
import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from '../../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import mmCIF_Format = ModelFormat.mmCIF
|
||||
@@ -76,6 +76,8 @@ function getNcsOperators(format: mmCIF_Format) {
|
||||
const m = Tensor.toMat3(Mat3(), matrixSpace, matrix.value(i));
|
||||
const v = Tensor.toVec3(Vec3(), vectorSpace, vector.value(i));
|
||||
if (!SymmetryOperator.checkIfRotationAndTranslation(m, v)) continue;
|
||||
// ignore non-identity 'given' NCS operators
|
||||
if (struct_ncs_oper.code.value(i) === 'given' && !Mat3.isIdentity(m) && !Vec3.isZero(v)) continue;
|
||||
const ncsId = id.value(i)
|
||||
opers[opers.length] = SymmetryOperator.ofRotationAndOffset(`ncs_${ncsId}`, m, v, ncsId);
|
||||
}
|
||||
@@ -125,19 +127,30 @@ function getMissingResidues(format: mmCIF_Format): Model['properties']['missingR
|
||||
function getChemicalComponentMap(format: mmCIF_Format): Model['properties']['chemicalComponentMap'] {
|
||||
const map = new Map<string, ChemicalComponent>();
|
||||
const { chem_comp } = format.data
|
||||
const { id } = chem_comp
|
||||
for (let i = 0, il = id.rowCount; i < il; ++i) {
|
||||
map.set(id.value(i), Table.getRow(chem_comp, i))
|
||||
|
||||
if (chem_comp._rowCount > 0) {
|
||||
const { id } = chem_comp
|
||||
for (let i = 0, il = id.rowCount; i < il; ++i) {
|
||||
map.set(id.value(i), Table.getRow(chem_comp, i))
|
||||
}
|
||||
} else {
|
||||
const uniqueNames = getUniqueComponentNames(format);
|
||||
uniqueNames.forEach(n => {
|
||||
map.set(n, getDefaultChemicalComponent(n));
|
||||
});
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
function getSaccharideComponentMap(format: mmCIF_Format): SaccharideComponentMap {
|
||||
const map = new Map<string, SaccharideComponent>();
|
||||
const { pdbx_chem_comp_identifier } = format.data
|
||||
if (pdbx_chem_comp_identifier._rowCount > 0) {
|
||||
const { comp_id, type, identifier } = pdbx_chem_comp_identifier
|
||||
for (let i = 0, il = pdbx_chem_comp_identifier._rowCount; i < il; ++i) {
|
||||
|
||||
if (format.data.pdbx_chem_comp_identifier._rowCount > 0) {
|
||||
// note that `pdbx_chem_comp_identifier` does not contain
|
||||
// a 'SNFG CARB SYMBOL' entry for 'Unknown' saccharide components
|
||||
// so we always need to check `chem_comp` for those
|
||||
const { comp_id, type, identifier } = format.data.pdbx_chem_comp_identifier
|
||||
for (let i = 0, il = comp_id.rowCount; i < il; ++i) {
|
||||
if (type.value(i) === 'SNFG CARB SYMBOL') {
|
||||
const snfgName = identifier.value(i)
|
||||
const saccharideComp = SaccharidesSnfgMap.get(snfgName)
|
||||
@@ -148,21 +161,24 @@ function getSaccharideComponentMap(format: mmCIF_Format): SaccharideComponentMap
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (format.data.chem_comp._rowCount > 0) {
|
||||
}
|
||||
|
||||
if (format.data.chem_comp._rowCount > 0) {
|
||||
const { id, type } = format.data.chem_comp
|
||||
for (let i = 0, il = id.rowCount; i < il; ++i) {
|
||||
const _id = id.value(i)
|
||||
if (map.has(_id)) continue
|
||||
const _type = type.value(i)
|
||||
if (SaccharideCompIdMap.has(_id)) {
|
||||
map.set(_id, SaccharideCompIdMap.get(_id)!)
|
||||
} else if (!map.has(_id) && getMoleculeType(_type, _id) === MoleculeType.saccharide) {
|
||||
} else if (getMoleculeType(_type, _id) === MoleculeType.saccharide) {
|
||||
map.set(_id, UnknownSaccharideComponent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const uniqueNames = getUniqueComponentNames(format)
|
||||
SaccharideCompIdMap.forEach((v, k) => {
|
||||
if (uniqueNames.has(k)) map.set(k, v)
|
||||
if (!map.has(k) && uniqueNames.has(k)) map.set(k, v)
|
||||
})
|
||||
}
|
||||
return map
|
||||
@@ -372,11 +388,19 @@ function getEntities(format: mmCIF_Format): Entities {
|
||||
}
|
||||
|
||||
if (assignSubtype) {
|
||||
const chemCompType = new Map<string, string>()
|
||||
const { id, type } = format.data.chem_comp;
|
||||
for (let i = 0, il = format.data.chem_comp._rowCount; i < il; i++) {
|
||||
chemCompType.set(id.value(i), type.value(i))
|
||||
}
|
||||
|
||||
const { label_entity_id, label_comp_id } = format.data.atom_site;
|
||||
for (let i = 0 as ElementIndex, il = format.data.atom_site._rowCount; i < il; i++) {
|
||||
const entityId = label_entity_id.value(i);
|
||||
if (!entityIds.has(entityId)) {
|
||||
subtypes[getEntityIndex(entityId)] = getEntitySubtype(label_comp_id.value(i))
|
||||
const compId = label_comp_id.value(i)
|
||||
const compType = chemCompType.get(compId) || ''
|
||||
subtypes[getEntityIndex(entityId)] = getEntitySubtype(compId, compType)
|
||||
entityIds.add(entityId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Loci {
|
||||
export function getBoundingSphere(loci: Loci, boundingSphere?: Sphere3D): Sphere3D | undefined {
|
||||
if (loci.kind === 'every-loci' || loci.kind === 'empty-loci') return void 0;
|
||||
|
||||
if (!boundingSphere) boundingSphere = Sphere3D.zero()
|
||||
if (!boundingSphere) boundingSphere = Sphere3D()
|
||||
sphereHelper.reset();
|
||||
|
||||
if (loci.kind === 'structure-loci') {
|
||||
|
||||
@@ -54,30 +54,37 @@ namespace Sequence {
|
||||
function determineKind(names: Column<string>) {
|
||||
for (let i = 0, _i = Math.min(names.rowCount, 10); i < _i; i++) {
|
||||
const name = names.value(i) || '';
|
||||
if (getProteinOneLetterCode(name) !== 'X') return { kind: Kind.Protein, code: getProteinOneLetterCode };
|
||||
if (getRnaOneLetterCode(name) !== 'X') return { kind: Kind.RNA, code: getRnaOneLetterCode };
|
||||
if (getDnaOneLetterCode(name) !== 'X') return { kind: Kind.DNA, code: getDnaOneLetterCode };
|
||||
if (getProteinOneLetterCode(name) !== 'X') return Kind.Protein;
|
||||
if (getRnaOneLetterCode(name) !== 'X') return Kind.RNA;
|
||||
if (getDnaOneLetterCode(name) !== 'X') return Kind.DNA;
|
||||
}
|
||||
return { kind: Kind.Generic, code: (v: string) => 'X' };
|
||||
return Kind.Generic;
|
||||
}
|
||||
|
||||
function modCode(code: (name: string) => string, map: ReadonlyMap<string, string>): (name: string) => string {
|
||||
return n => {
|
||||
const ret = code(n);
|
||||
if (ret !== 'X' || !map.has(n)) return ret;
|
||||
return code(map.get(n)!);
|
||||
function codeProvider(kind: Kind, map?: ReadonlyMap<string, string>) {
|
||||
let code: (name: string) => string
|
||||
switch (kind) {
|
||||
case Kind.Protein: code = getProteinOneLetterCode; break;
|
||||
case Kind.DNA: code = getDnaOneLetterCode; break;
|
||||
case Kind.RNA: code = getRnaOneLetterCode; break;
|
||||
case Kind.Generic: code = () => 'X'; break;
|
||||
default: throw new Error(`unknown kind '${kind}'`)
|
||||
}
|
||||
if (map && map.size > 0) {
|
||||
return (name: string) => {
|
||||
const ret = code(name);
|
||||
if (ret !== 'X' || !map.has(name)) return ret;
|
||||
return code(map.get(name)!);
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
export function ofResidueNames(compId: Column<string>, seqId: Column<number>, modifiedMap?: ReadonlyMap<string, string>): Sequence {
|
||||
if (seqId.rowCount === 0) throw new Error('cannot be empty');
|
||||
|
||||
const { kind, code } = determineKind(compId);
|
||||
|
||||
if (!modifiedMap || modifiedMap.size === 0) {
|
||||
return new ResidueNamesImpl(kind, compId, seqId, code) as Sequence;
|
||||
}
|
||||
return new ResidueNamesImpl(kind, compId, seqId, modCode(code, modifiedMap)) as Sequence;
|
||||
const kind = determineKind(compId);
|
||||
return new ResidueNamesImpl(kind, compId, seqId, modifiedMap) as Sequence;
|
||||
}
|
||||
|
||||
class ResidueNamesImpl<K extends Kind, Alphabet extends string> implements Base<K, Alphabet> {
|
||||
@@ -87,6 +94,8 @@ namespace Sequence {
|
||||
private _code: Column<Alphabet> | undefined = undefined
|
||||
private _label: Column<string> | undefined = undefined
|
||||
|
||||
private codeFromName: (name: string) => string
|
||||
|
||||
get code(): Column<Alphabet> {
|
||||
if (this._code !== void 0) return this._code;
|
||||
this.create();
|
||||
@@ -142,10 +151,14 @@ namespace Sequence {
|
||||
const seqId = this.seqId.value(i)
|
||||
const idx = seqId - minSeqId;
|
||||
const name = this.compId.value(i);
|
||||
const code = this.getCode(name);
|
||||
const code = this.codeFromName(name);
|
||||
// in case of MICROHETEROGENEITY `sequenceArray[idx]` may already be set
|
||||
if (!sequenceArray[idx] || sequenceArray[idx] === '-') {
|
||||
sequenceArray[idx] = code;
|
||||
if (code === 'X' && this.modifiedMap && this.modifiedMap.has(name)) {
|
||||
sequenceArray[idx] = this.modifiedMap.get(name)!
|
||||
} else {
|
||||
sequenceArray[idx] = code;
|
||||
}
|
||||
}
|
||||
labels[idx].push(code === 'X' ? name : code);
|
||||
compIds[seqId].push(name);
|
||||
@@ -170,8 +183,9 @@ namespace Sequence {
|
||||
this._length = count
|
||||
}
|
||||
|
||||
constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>, private getCode: (name: string) => string) {
|
||||
constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>, private modifiedMap?: ReadonlyMap<string, string>) {
|
||||
|
||||
this.codeFromName = codeProvider(kind)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import { CustomProperties } from '../common/custom-property';
|
||||
import { SecondaryStructure } from './properties/seconday-structure';
|
||||
import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
|
||||
import { ModelFormat } from '../../../mol-model-formats/structure/format';
|
||||
import { calcModelCenter } from './util';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
/**
|
||||
* Interface to the "source data" of the molecule.
|
||||
@@ -75,4 +77,12 @@ export interface Model extends Readonly<{
|
||||
export namespace Model {
|
||||
// TODO: is this enough?
|
||||
export type Trajectory = ReadonlyArray<Model>
|
||||
|
||||
const CenterProp = '__Center__'
|
||||
export function getCenter(model: Model): Vec3 {
|
||||
if (model._dynamicPropertyData[CenterProp]) return model._dynamicPropertyData[CenterProp]
|
||||
const center = calcModelCenter(model.atomicConformation, model.coarseConformation)
|
||||
model._dynamicPropertyData[CenterProp] = center
|
||||
return center
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -9,6 +9,7 @@ import { arrayFind } from '../../../../mol-data/util'
|
||||
import { StructureQuery } from '../../query'
|
||||
import { Model } from '../../model'
|
||||
import { Spacegroup } from '../../../../mol-math/geometry';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra';
|
||||
|
||||
/** Determine an atom set and a list of operators that should be applied to that set */
|
||||
export interface OperatorGroup {
|
||||
@@ -47,8 +48,14 @@ interface ModelSymmetry {
|
||||
readonly isNonStandardCrytalFrame: boolean,
|
||||
readonly ncsOperators?: ReadonlyArray<SymmetryOperator>,
|
||||
|
||||
// optionally cached operators from [-3, -3, -3] to [3, 3, 3]
|
||||
_operators_333?: SymmetryOperator[]
|
||||
/**
|
||||
* optionally cached operators from [-3, -3, -3] to [3, 3, 3]
|
||||
* around reference point `ref` in fractional coordinates
|
||||
*/
|
||||
_operators_333?: {
|
||||
ref: Vec3,
|
||||
operators: SymmetryOperator[]
|
||||
}
|
||||
}
|
||||
|
||||
namespace ModelSymmetry {
|
||||
|
||||
@@ -9,7 +9,7 @@ import BitFlags from '../../../mol-util/bit-flags'
|
||||
import { SaccharideCompIdMap } from '../structure/carbohydrates/constants';
|
||||
import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
|
||||
import { SetUtils } from '../../../mol-util/set';
|
||||
import { EntitySubtype } from './properties/common';
|
||||
import { EntitySubtype, ChemicalComponent } from './properties/common';
|
||||
|
||||
const _esCache = (function () {
|
||||
const cache = Object.create(null);
|
||||
@@ -113,19 +113,34 @@ export const NucleicBackboneAtoms = new Set([
|
||||
'O2*', 'O3*', 'O4*', 'O5*', 'C1*', 'C2*', 'C3*', 'C4*', 'C5*'
|
||||
])
|
||||
|
||||
/** Chemical component type names for protein */
|
||||
export const ProteinComponentTypeNames = new Set([
|
||||
'D-PEPTIDE LINKING', 'L-PEPTIDE LINKING', 'D-PEPTIDE NH3 AMINO TERMINUS',
|
||||
'L-PEPTIDE NH3 AMINO TERMINUS', 'D-PEPTIDE COOH CARBOXY TERMINUS',
|
||||
'L-PEPTIDE COOH CARBOXY TERMINUS', 'PEPTIDE LINKING', 'PEPTIDE-LIKE',
|
||||
'L-GAMMA-PEPTIDE, C-DELTA LINKING', 'D-GAMMA-PEPTIDE, C-DELTA LINKING',
|
||||
'L-BETA-PEPTIDE, C-GAMMA LINKING', 'D-BETA-PEPTIDE, C-GAMMA LINKING',
|
||||
/** Chemical component type names for D-linked protein */
|
||||
export const DProteinComponentTypeNames = new Set([
|
||||
'D-PEPTIDE LINKING', 'D-PEPTIDE NH3 AMINO TERMINUS',
|
||||
'D-PEPTIDE COOH CARBOXY TERMINUS', 'D-GAMMA-PEPTIDE, C-DELTA LINKING',
|
||||
'D-BETA-PEPTIDE, C-GAMMA LINKING'
|
||||
])
|
||||
|
||||
/** Chemical component type names for L-linked protein */
|
||||
export const LProteinComponentTypeNames = new Set([
|
||||
'L-PEPTIDE LINKING', 'L-PEPTIDE NH3 AMINO TERMINUS',
|
||||
'L-PEPTIDE COOH CARBOXY TERMINUS', 'L-GAMMA-PEPTIDE, C-DELTA LINKING',
|
||||
'L-BETA-PEPTIDE, C-GAMMA LINKING'
|
||||
])
|
||||
|
||||
/** Chemical component type names for pepdite-like protein */
|
||||
export const OtherProteinComponentTypeNames = new Set([
|
||||
'PEPTIDE LINKING', 'PEPTIDE-LIKE',
|
||||
])
|
||||
|
||||
/** Chemical component type names for protein */
|
||||
export const ProteinComponentTypeNames = SetUtils.unionMany(
|
||||
DProteinComponentTypeNames, LProteinComponentTypeNames, OtherProteinComponentTypeNames
|
||||
)
|
||||
|
||||
/** Chemical component type names for DNA */
|
||||
export const DNAComponentTypeNames = [
|
||||
export const DNAComponentTypeNames = new Set([
|
||||
'DNA LINKING', 'L-DNA LINKING', 'DNA OH 5 PRIME TERMINUS', 'DNA OH 3 PRIME TERMINUS',
|
||||
]
|
||||
])
|
||||
|
||||
/** Chemical component type names for RNA */
|
||||
export const RNAComponentTypeNames = new Set([
|
||||
@@ -200,7 +215,7 @@ export function getMoleculeType(compType: string, compId: string) {
|
||||
return MoleculeType.protein
|
||||
} else if (RNAComponentTypeNames.has(compType)) {
|
||||
return MoleculeType.RNA
|
||||
} else if (DNAComponentTypeNames.includes(compType)) {
|
||||
} else if (DNAComponentTypeNames.has(compType)) {
|
||||
return MoleculeType.DNA
|
||||
} else if (SaccharideComponentTypeNames.has(compType)) {
|
||||
return MoleculeType.saccharide
|
||||
@@ -209,7 +224,12 @@ export function getMoleculeType(compType: string, compId: string) {
|
||||
} else if (IonNames.has(compId)) {
|
||||
return MoleculeType.ion
|
||||
} else if (OtherComponentTypeNames.has(compType)) {
|
||||
return MoleculeType.other
|
||||
if (SaccharideCompIdMap.has(compId)) {
|
||||
// trust our saccharide table more than given 'non-polymer' or 'other' component type
|
||||
return MoleculeType.saccharide
|
||||
} else {
|
||||
return MoleculeType.other
|
||||
}
|
||||
} else {
|
||||
return MoleculeType.unknown
|
||||
}
|
||||
@@ -230,6 +250,20 @@ export function getComponentType(compId: string): mmCIF_Schema['chem_comp']['typ
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultChemicalComponent(compId: string): ChemicalComponent {
|
||||
// TODO: this is to make the chem_comp_type property work if chem_comp category is not present.
|
||||
// should we try to set the formula etc better?
|
||||
return {
|
||||
formula: '',
|
||||
formula_weight: 0,
|
||||
id: compId,
|
||||
name: compId,
|
||||
mon_nstd_flag: 'n',
|
||||
pdbx_synonyms: [],
|
||||
type: getComponentType(compId)
|
||||
};
|
||||
}
|
||||
|
||||
export function getEntityType(compId: string): mmCIF_Schema['entity']['type']['T'] {
|
||||
compId = compId.toUpperCase()
|
||||
if (WaterNames.has(compId)) {
|
||||
@@ -243,9 +277,20 @@ export function getEntityType(compId: string): mmCIF_Schema['entity']['type']['T
|
||||
}
|
||||
}
|
||||
|
||||
export function getEntitySubtype(compId: string): EntitySubtype {
|
||||
export function getEntitySubtype(compId: string, compType: string): EntitySubtype {
|
||||
compId = compId.toUpperCase()
|
||||
if (SaccharideCompIdMap.has(compId)) {
|
||||
compType = compType.toUpperCase()
|
||||
if (LProteinComponentTypeNames.has(compType)) {
|
||||
return 'polypeptide(L)'
|
||||
} else if (DProteinComponentTypeNames.has(compType)) {
|
||||
return 'polypeptide(D)'
|
||||
} else if (RNAComponentTypeNames.has(compType)) {
|
||||
return 'polyribonucleotide'
|
||||
} else if (DNAComponentTypeNames.has(compType)) {
|
||||
return 'polydeoxyribonucleotide'
|
||||
} else if (SaccharideComponentTypeNames.has(compType)) {
|
||||
return 'oligosaccharide'
|
||||
} else if (SaccharideCompIdMap.has(compId)) {
|
||||
return 'oligosaccharide'
|
||||
} else if (PeptideBaseNames.has(compId)) {
|
||||
return 'peptide nucleic acid'
|
||||
@@ -639,5 +684,5 @@ export const ResidueHydrophobicity = {
|
||||
'TRP': [ -1.85, -2.09, -0.24 ],
|
||||
'TYR': [ -0.94, -0.71, 0.23 ],
|
||||
'VAL': [ 0.07, -0.46, -0.53 ]
|
||||
}
|
||||
export const DefaultResidueHydrophobicity = [ 0.00, 0.00, 0.00 ]
|
||||
}
|
||||
export const DefaultResidueHydrophobicity = [ 0.00, 0.00, 0.00 ]
|
||||
45
src/mol-model/structure/model/util.ts
Normal file
45
src/mol-model/structure/model/util.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { AtomicConformation } from './properties/atomic';
|
||||
import { CoarseConformation } from './properties/coarse';
|
||||
import { arrayMinMax } from '../../../mol-util/array';
|
||||
|
||||
export function calcModelCenter(atomicConformation: AtomicConformation, coarseConformation?: CoarseConformation) {
|
||||
let rangesX: number[] = []
|
||||
let rangesY: number[] = []
|
||||
let rangesZ: number[] = []
|
||||
|
||||
if (atomicConformation.x.length) {
|
||||
rangesX.push(...arrayMinMax(atomicConformation.x))
|
||||
rangesY.push(...arrayMinMax(atomicConformation.y))
|
||||
rangesZ.push(...arrayMinMax(atomicConformation.z))
|
||||
}
|
||||
|
||||
if (coarseConformation) {
|
||||
if (coarseConformation.spheres.x.length) {
|
||||
rangesX.push(...arrayMinMax(coarseConformation.spheres.x))
|
||||
rangesY.push(...arrayMinMax(coarseConformation.spheres.y))
|
||||
rangesZ.push(...arrayMinMax(coarseConformation.spheres.z))
|
||||
}
|
||||
if (coarseConformation.gaussians.x.length) {
|
||||
rangesX.push(...arrayMinMax(coarseConformation.gaussians.x))
|
||||
rangesY.push(...arrayMinMax(coarseConformation.gaussians.y))
|
||||
rangesZ.push(...arrayMinMax(coarseConformation.gaussians.z))
|
||||
}
|
||||
}
|
||||
|
||||
const [minX, maxX] = arrayMinMax(rangesX)
|
||||
const [minY, maxY] = arrayMinMax(rangesY)
|
||||
const [minZ, maxZ] = arrayMinMax(rangesZ)
|
||||
|
||||
const x = minX + (maxX - minX) / 2
|
||||
const y = minY + (maxY - minY) / 2
|
||||
const z = minZ + (maxZ - minZ) / 2
|
||||
|
||||
return Vec3.create(x, y, z)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -22,7 +22,12 @@ import { UnitRings, UnitRing } from '../unit/rings';
|
||||
import { ElementIndex } from '../../model/indexing';
|
||||
|
||||
const C = ElementSymbol('C'), O = ElementSymbol('O');
|
||||
const SugarRingFps = [UnitRing.elementFingerprint([C, C, C, C, C, O]), UnitRing.elementFingerprint([C, C, C, C, O])]
|
||||
const SugarRingFps = [
|
||||
UnitRing.elementFingerprint([C, C, C, O]),
|
||||
UnitRing.elementFingerprint([C, C, C, C, O]),
|
||||
UnitRing.elementFingerprint([C, C, C, C, C, O]),
|
||||
UnitRing.elementFingerprint([C, C, C, C, C, C, O]),
|
||||
]
|
||||
|
||||
function getAnomericCarbon(unit: Unit.Atomic, ringAtoms: ArrayLike<StructureElement.UnitIndex>): ElementIndex {
|
||||
let indexHasTwoOxygen = -1, indexHasOxygenAndCarbon = -1, indexHasC1Name = -1, indexIsCarbon = -1
|
||||
@@ -203,7 +208,8 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
|
||||
elements.push({
|
||||
geometry: { center, normal, direction },
|
||||
component: saccharideComp,
|
||||
unit, residueIndex, anomericCarbon, ringAltId
|
||||
unit, residueIndex, anomericCarbon, ringAltId,
|
||||
ringMemberCount: ringAtoms.length
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -9,9 +9,13 @@ import { Color, ColorMap } from '../../../../mol-util/color';
|
||||
|
||||
// follows community standard from https://www.ncbi.nlm.nih.gov/glycans/snfg.html
|
||||
|
||||
export const enum SaccharideShapes {
|
||||
export const enum SaccharideShape {
|
||||
// standard shapes
|
||||
FilledSphere, FilledCube, CrossedCube, DividedDiamond, FilledCone, DevidedCone,
|
||||
FlatBox, FilledStar, FilledDiamond, FlatDiamond, FlatHexagon, Pentagon
|
||||
FlatBox, FilledStar, FilledDiamond, FlatDiamond, FlatHexagon, Pentagon,
|
||||
|
||||
// generic shapes for rings with 4, 5, 6, or 7 members
|
||||
DiamondPrism, PentagonalPrism, HexagonalPrism, HeptagonalPrism
|
||||
}
|
||||
|
||||
export const SaccharideColors = ColorMap({
|
||||
@@ -53,22 +57,30 @@ export function getSaccharideName(type: SaccharideType) {
|
||||
}
|
||||
|
||||
const SaccharideTypeShapeMap = {
|
||||
[SaccharideType.Hexose]: SaccharideShapes.FilledSphere,
|
||||
[SaccharideType.HexNAc]: SaccharideShapes.FilledCube,
|
||||
[SaccharideType.Hexosamine]: SaccharideShapes.CrossedCube,
|
||||
[SaccharideType.Hexuronate]: SaccharideShapes.DividedDiamond,
|
||||
[SaccharideType.Deoxyhexose]: SaccharideShapes.FilledCone,
|
||||
[SaccharideType.DeoxyhexNAc]: SaccharideShapes.DevidedCone,
|
||||
[SaccharideType.DiDeoxyhexose]: SaccharideShapes.FlatBox,
|
||||
[SaccharideType.Pentose]: SaccharideShapes.FilledStar,
|
||||
[SaccharideType.Deoxynonulosonate]: SaccharideShapes.FilledDiamond,
|
||||
[SaccharideType.DiDeoxynonulosonate]: SaccharideShapes.FlatDiamond,
|
||||
[SaccharideType.Unknown]: SaccharideShapes.FlatHexagon,
|
||||
[SaccharideType.Assigned]: SaccharideShapes.Pentagon,
|
||||
[SaccharideType.Hexose]: SaccharideShape.FilledSphere,
|
||||
[SaccharideType.HexNAc]: SaccharideShape.FilledCube,
|
||||
[SaccharideType.Hexosamine]: SaccharideShape.CrossedCube,
|
||||
[SaccharideType.Hexuronate]: SaccharideShape.DividedDiamond,
|
||||
[SaccharideType.Deoxyhexose]: SaccharideShape.FilledCone,
|
||||
[SaccharideType.DeoxyhexNAc]: SaccharideShape.DevidedCone,
|
||||
[SaccharideType.DiDeoxyhexose]: SaccharideShape.FlatBox,
|
||||
[SaccharideType.Pentose]: SaccharideShape.FilledStar,
|
||||
[SaccharideType.Deoxynonulosonate]: SaccharideShape.FilledDiamond,
|
||||
[SaccharideType.DiDeoxynonulosonate]: SaccharideShape.FlatDiamond,
|
||||
[SaccharideType.Unknown]: SaccharideShape.FlatHexagon,
|
||||
[SaccharideType.Assigned]: SaccharideShape.Pentagon,
|
||||
}
|
||||
|
||||
export function getSaccharideShape(type: SaccharideType) {
|
||||
return SaccharideTypeShapeMap[type]
|
||||
export function getSaccharideShape(type: SaccharideType, ringMemberCount: number): SaccharideShape {
|
||||
if (type === SaccharideType.Unknown) {
|
||||
if (ringMemberCount === 4) return SaccharideShape.DiamondPrism
|
||||
else if (ringMemberCount === 5) return SaccharideShape.PentagonalPrism
|
||||
else if (ringMemberCount === 6) return SaccharideShape.HexagonalPrism
|
||||
else if (ringMemberCount === 7) return SaccharideShape.HeptagonalPrism
|
||||
else return SaccharideShape.FlatHexagon
|
||||
} else {
|
||||
return SaccharideTypeShapeMap[type]
|
||||
}
|
||||
}
|
||||
|
||||
export type SaccharideComponent = {
|
||||
@@ -200,33 +212,27 @@ const CommonSaccharideNames: { [k: string]: string[] } = {
|
||||
// Hexose
|
||||
Glc: [
|
||||
'GLC', 'BGC',
|
||||
'BOG', // via GlyFinder
|
||||
'TRE', // via GlyFinder, di-saccharide but homomer
|
||||
'MLR', // via GlyFinder, tri-saccharide but homomer
|
||||
'TRE', // di-saccharide but homomer
|
||||
'MLR', // tri-saccharide but homomer
|
||||
],
|
||||
Man: ['MAN', 'BMA'],
|
||||
Gal: [
|
||||
'GAL', 'GLA',
|
||||
'GXL' // via PubChem
|
||||
],
|
||||
Gul: ['GUP', 'GL0'],
|
||||
Alt: ['ALT'],
|
||||
All: ['ALL', 'AFD'],
|
||||
Tal: ['TAL'],
|
||||
Ido: ['4N2'],
|
||||
Gal: ['GLA', 'GAL', 'GZL'],
|
||||
Gul: ['4GL', 'GL0'],
|
||||
Alt: ['Z6H', '3MK'],
|
||||
All: ['AFD', 'ALL'],
|
||||
Tal: [],
|
||||
Ido: ['Z0F', '4N2'],
|
||||
// HexNAc
|
||||
GlcNAc: ['NAG', 'NDG'],
|
||||
ManNAc: ['NGA', 'A2G'],
|
||||
GulNAc: [],
|
||||
GlcNAc: ['NDG', 'NAG'],
|
||||
ManNAc: ['BM3', 'BM7'],
|
||||
GalNAc: ['A2G', 'NGA'],
|
||||
GulNAc: ['LXB'],
|
||||
AltNAc: [],
|
||||
AllNAc: ['NAA'],
|
||||
TalNAc: [],
|
||||
IdoNAc: ['HSQ'],
|
||||
IdoNAc: [],
|
||||
// Hexosamine
|
||||
GlcN: [
|
||||
'GCS', 'PA1',
|
||||
'IDU', 'SGN', 'SUS', // via GlyFinder
|
||||
],
|
||||
GlcN: ['PA1', 'GCS'],
|
||||
ManN: ['95Z'],
|
||||
GalN: ['X6X', '1GN'],
|
||||
GulN: [],
|
||||
@@ -237,71 +243,70 @@ const CommonSaccharideNames: { [k: string]: string[] } = {
|
||||
// Hexuronate
|
||||
GlcA: ['GCU', 'BDP'],
|
||||
ManA: ['MAV', 'BEM'],
|
||||
GalA: ['ADA', 'GTR'],
|
||||
GulA: ['LGU'],
|
||||
GalA: ['ADA', 'GTR', 'GTK'],
|
||||
GulA: [],
|
||||
AltA: [],
|
||||
AllA: [],
|
||||
TalA: ['X0X', 'X1X'],
|
||||
IdoA: [
|
||||
'IDR',
|
||||
'IDS', // via GlyFinder
|
||||
],
|
||||
TalA: ['X1X', 'X0X'],
|
||||
IdoA: ['IDR'],
|
||||
// Deoxyhexose
|
||||
Qui: ['G6D'],
|
||||
Qui: ['G6D', 'YYK'],
|
||||
Rha: ['RAM', 'RM4'],
|
||||
'6dGul': [],
|
||||
'6dGul': ['66O'],
|
||||
'6dAlt': [],
|
||||
'6dTal': [],
|
||||
Fuc: ['FUC', 'FUL'],
|
||||
// DeoxyhexNAc
|
||||
QuiNAc: [],
|
||||
QuiNAc: ['Z9W'],
|
||||
RhaNAc: [],
|
||||
'6dAltNAc': [],
|
||||
'6dTalNAc': [],
|
||||
FucNAc: [],
|
||||
// Di-deoxyhexose
|
||||
Oli: ['DDA'],
|
||||
Oli: ['DDA', 'RAE', 'Z5J'],
|
||||
Tyv: ['TYV'],
|
||||
Abe: ['ABE'],
|
||||
Par: ['PZU'],
|
||||
Dig: [],
|
||||
Dig: ['Z3U'],
|
||||
Col: [],
|
||||
// Pentose
|
||||
Ara: ['ARA', 'ARB'],
|
||||
Lyx: ['LDY'],
|
||||
Xyl: ['XYS', 'XYP'],
|
||||
Rib: ['RIP', '0MK'],
|
||||
Ara: ['ARA', 'ARB', 'AHR', 'FUB'],
|
||||
Lyx: ['LDY', 'Z4W'],
|
||||
Xyl: ['XZS', 'XYP', 'XYZ'],
|
||||
Rib: ['YYM', 'RIP', 'RIB', 'BDR'],
|
||||
// Deoxynonulosonate
|
||||
Kdn: ['KDN', 'KDM'],
|
||||
Kdn: ['KDM', 'KDN'],
|
||||
Neu5Ac: ['SIA', 'SLB'],
|
||||
Neu5Gc: ['NGC', 'NGE'],
|
||||
Neu: [],
|
||||
Sia: [],
|
||||
// Di-deoxynonulosonate
|
||||
Pse: ['6PZ'],
|
||||
Pse: [],
|
||||
Leg: [],
|
||||
Aci: [],
|
||||
'4eLeg': [],
|
||||
// Unknown
|
||||
Bac: ['B6D'],
|
||||
Bac: [],
|
||||
LDManHep: ['GMH'],
|
||||
Kdo: ['KDO'],
|
||||
Dha: [],
|
||||
DDManHep: [],
|
||||
MurNAc: ['AMU'],
|
||||
DDManHep: ['289'],
|
||||
MurNAc: ['MUB', 'AMU'],
|
||||
MurNGc: [],
|
||||
Mur: ['MUR'],
|
||||
Mur: ['1S4', 'MUR'],
|
||||
// Assigned
|
||||
Api: ['XXM'],
|
||||
Fru: ['BDF'],
|
||||
Fru: ['BDF', 'Z9N', 'FRU'],
|
||||
Tag: ['T6T'],
|
||||
Sor: ['SOE'],
|
||||
Psi: [],
|
||||
Psi: ['PSV'],
|
||||
}
|
||||
|
||||
const UnknownSaccharideNames = [
|
||||
'NGZ', // via CCD
|
||||
'LAT', // BETA-LACTOSE, Gal-Glc di-saccharide via GlyFinder
|
||||
|
||||
'PUF', 'GDA', '9WJ', // via updated CCD
|
||||
]
|
||||
|
||||
export const SaccharideCompIdMap = (function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -30,6 +30,7 @@ export interface CarbohydrateElement {
|
||||
readonly residueIndex: ResidueIndex,
|
||||
readonly component: SaccharideComponent,
|
||||
readonly ringAltId: string,
|
||||
readonly ringMemberCount: number,
|
||||
}
|
||||
|
||||
/** partial carbohydrate with no ring present */
|
||||
|
||||
@@ -54,7 +54,11 @@ export namespace Loci {
|
||||
}
|
||||
|
||||
export function isEmpty(loci: Loci) {
|
||||
return size(loci) === 0 ? true : false
|
||||
return size(loci) === 0
|
||||
}
|
||||
|
||||
export function isWholeStructure(loci: Loci) {
|
||||
return size(loci) === loci.structure.elementCount
|
||||
}
|
||||
|
||||
export function size(loci: Loci) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { EquivalenceClasses } from '../../../mol-data/util';
|
||||
import { Spacegroup, SpacegroupCell, SymmetryOperator } from '../../../mol-math/geometry';
|
||||
import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext, Task } from '../../../mol-task';
|
||||
import { ModelSymmetry } from '../model';
|
||||
import { ModelSymmetry, Model } from '../model';
|
||||
import { QueryContext, StructureSelection } from '../query';
|
||||
import Structure from './structure';
|
||||
import Unit from './unit';
|
||||
@@ -95,7 +95,7 @@ namespace StructureSymmetry {
|
||||
}
|
||||
}
|
||||
|
||||
function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {
|
||||
function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3, modelCenter: Vec3) {
|
||||
const { spacegroup, ncsOperators } = symmetry;
|
||||
const ncsCount = (ncsOperators && ncsOperators.length) || 0
|
||||
const operators: SymmetryOperator[] = [];
|
||||
@@ -107,13 +107,17 @@ function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {
|
||||
operators[0] = Spacegroup.getSymmetryOperator(spacegroup, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
const { toFractional } = spacegroup.cell
|
||||
const ref = Vec3.transformMat4(Vec3(), modelCenter, toFractional)
|
||||
|
||||
for (let op = 0; op < spacegroup.operators.length; op++) {
|
||||
for (let i = ijkMin[0]; i <= ijkMax[0]; i++) {
|
||||
for (let j = ijkMin[1]; j <= ijkMax[1]; j++) {
|
||||
for (let k = ijkMin[2]; k <= ijkMax[2]; k++) {
|
||||
// check if we have added identity as the 1st operator.
|
||||
if (!ncsCount && op === 0 && i === 0 && j === 0 && k === 0) continue;
|
||||
const symOp = Spacegroup.getSymmetryOperator(spacegroup, op, i, j, k);
|
||||
|
||||
const symOp = Spacegroup.getSymmetryOperatorRef(spacegroup, op, i, j, k, ref)
|
||||
if (ncsCount) {
|
||||
for (let u = 0; u < ncsCount; ++u) {
|
||||
const ncsOp = ncsOperators![u]
|
||||
@@ -131,10 +135,15 @@ function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {
|
||||
return operators;
|
||||
}
|
||||
|
||||
function getOperatorsCached333(symmetry: ModelSymmetry) {
|
||||
if (typeof symmetry._operators_333 !== 'undefined') return symmetry._operators_333;
|
||||
symmetry._operators_333 = getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3));
|
||||
return symmetry._operators_333;
|
||||
function getOperatorsCached333(symmetry: ModelSymmetry, ref: Vec3) {
|
||||
if (symmetry._operators_333 && Vec3.equals(ref, symmetry._operators_333.ref)) {
|
||||
return symmetry._operators_333.operators;
|
||||
}
|
||||
symmetry._operators_333 = {
|
||||
ref: Vec3.clone(ref),
|
||||
operators: getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3), ref)
|
||||
};
|
||||
return symmetry._operators_333.operators;
|
||||
}
|
||||
|
||||
function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) {
|
||||
@@ -164,7 +173,8 @@ async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkM
|
||||
const { spacegroup } = models[0].symmetry;
|
||||
if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
|
||||
|
||||
const operators = getOperators(models[0].symmetry, ijkMin, ijkMax);
|
||||
const modelCenter = Model.getCenter(models[0])
|
||||
const operators = getOperators(models[0].symmetry, ijkMin, ijkMax, modelCenter);
|
||||
return assembleOperators(structure, operators);
|
||||
}
|
||||
|
||||
@@ -177,10 +187,17 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
|
||||
if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
|
||||
|
||||
if (ctx.shouldUpdate) await ctx.update('Initialing...');
|
||||
const operators = getOperatorsCached333(symmetry);
|
||||
const modelCenter = Model.getCenter(models[0])
|
||||
const operators = getOperatorsCached333(symmetry, modelCenter);
|
||||
const lookup = structure.lookup3d;
|
||||
|
||||
const assembler = Structure.Builder();
|
||||
// keep track of added invariant-unit and operator combinations
|
||||
const added = new Set<string>()
|
||||
function hash(unit: Unit, oper: SymmetryOperator) {
|
||||
return `${unit.invariantId}|${oper.name}`
|
||||
}
|
||||
|
||||
const assembler = Structure.Builder({ label: structure.label });
|
||||
|
||||
const { units } = structure;
|
||||
const center = Vec3.zero();
|
||||
@@ -193,13 +210,17 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
|
||||
for (let uI = 0, _uI = closeUnits.count; uI < _uI; uI++) {
|
||||
const closeUnit = units[closeUnits.indices[uI]];
|
||||
if (!closeUnit.lookup3d.check(center[0], center[1], center[2], boundingSphere.radius + radius)) continue;
|
||||
assembler.addWithOperator(unit, oper);
|
||||
|
||||
const h = hash(unit, oper)
|
||||
if (!added.has(h)) {
|
||||
assembler.addWithOperator(unit, oper);
|
||||
added.add(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx.shouldUpdate) await ctx.update('Building symmetry...');
|
||||
}
|
||||
|
||||
|
||||
return assembler.getStructure();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,11 @@ export const DefaultLinkComputationProps: LinkComputationProps = {
|
||||
// H,D,T are all mapped to H
|
||||
const __ElementIndex: { [e: string]: number | undefined } = { 'H': 0, 'h': 0, 'D': 0, 'd': 0, 'T': 0, 't': 0, 'He': 2, 'HE': 2, 'he': 2, 'Li': 3, 'LI': 3, 'li': 3, 'Be': 4, 'BE': 4, 'be': 4, 'B': 5, 'b': 5, 'C': 6, 'c': 6, 'N': 7, 'n': 7, 'O': 8, 'o': 8, 'F': 9, 'f': 9, 'Ne': 10, 'NE': 10, 'ne': 10, 'Na': 11, 'NA': 11, 'na': 11, 'Mg': 12, 'MG': 12, 'mg': 12, 'Al': 13, 'AL': 13, 'al': 13, 'Si': 14, 'SI': 14, 'si': 14, 'P': 15, 'p': 15, 'S': 16, 's': 16, 'Cl': 17, 'CL': 17, 'cl': 17, 'Ar': 18, 'AR': 18, 'ar': 18, 'K': 19, 'k': 19, 'Ca': 20, 'CA': 20, 'ca': 20, 'Sc': 21, 'SC': 21, 'sc': 21, 'Ti': 22, 'TI': 22, 'ti': 22, 'V': 23, 'v': 23, 'Cr': 24, 'CR': 24, 'cr': 24, 'Mn': 25, 'MN': 25, 'mn': 25, 'Fe': 26, 'FE': 26, 'fe': 26, 'Co': 27, 'CO': 27, 'co': 27, 'Ni': 28, 'NI': 28, 'ni': 28, 'Cu': 29, 'CU': 29, 'cu': 29, 'Zn': 30, 'ZN': 30, 'zn': 30, 'Ga': 31, 'GA': 31, 'ga': 31, 'Ge': 32, 'GE': 32, 'ge': 32, 'As': 33, 'AS': 33, 'as': 33, 'Se': 34, 'SE': 34, 'se': 34, 'Br': 35, 'BR': 35, 'br': 35, 'Kr': 36, 'KR': 36, 'kr': 36, 'Rb': 37, 'RB': 37, 'rb': 37, 'Sr': 38, 'SR': 38, 'sr': 38, 'Y': 39, 'y': 39, 'Zr': 40, 'ZR': 40, 'zr': 40, 'Nb': 41, 'NB': 41, 'nb': 41, 'Mo': 42, 'MO': 42, 'mo': 42, 'Tc': 43, 'TC': 43, 'tc': 43, 'Ru': 44, 'RU': 44, 'ru': 44, 'Rh': 45, 'RH': 45, 'rh': 45, 'Pd': 46, 'PD': 46, 'pd': 46, 'Ag': 47, 'AG': 47, 'ag': 47, 'Cd': 48, 'CD': 48, 'cd': 48, 'In': 49, 'IN': 49, 'in': 49, 'Sn': 50, 'SN': 50, 'sn': 50, 'Sb': 51, 'SB': 51, 'sb': 51, 'Te': 52, 'TE': 52, 'te': 52, 'I': 53, 'i': 53, 'Xe': 54, 'XE': 54, 'xe': 54, 'Cs': 55, 'CS': 55, 'cs': 55, 'Ba': 56, 'BA': 56, 'ba': 56, 'La': 57, 'LA': 57, 'la': 57, 'Ce': 58, 'CE': 58, 'ce': 58, 'Pr': 59, 'PR': 59, 'pr': 59, 'Nd': 60, 'ND': 60, 'nd': 60, 'Pm': 61, 'PM': 61, 'pm': 61, 'Sm': 62, 'SM': 62, 'sm': 62, 'Eu': 63, 'EU': 63, 'eu': 63, 'Gd': 64, 'GD': 64, 'gd': 64, 'Tb': 65, 'TB': 65, 'tb': 65, 'Dy': 66, 'DY': 66, 'dy': 66, 'Ho': 67, 'HO': 67, 'ho': 67, 'Er': 68, 'ER': 68, 'er': 68, 'Tm': 69, 'TM': 69, 'tm': 69, 'Yb': 70, 'YB': 70, 'yb': 70, 'Lu': 71, 'LU': 71, 'lu': 71, 'Hf': 72, 'HF': 72, 'hf': 72, 'Ta': 73, 'TA': 73, 'ta': 73, 'W': 74, 'w': 74, 'Re': 75, 'RE': 75, 're': 75, 'Os': 76, 'OS': 76, 'os': 76, 'Ir': 77, 'IR': 77, 'ir': 77, 'Pt': 78, 'PT': 78, 'pt': 78, 'Au': 79, 'AU': 79, 'au': 79, 'Hg': 80, 'HG': 80, 'hg': 80, 'Tl': 81, 'TL': 81, 'tl': 81, 'Pb': 82, 'PB': 82, 'pb': 82, 'Bi': 83, 'BI': 83, 'bi': 83, 'Po': 84, 'PO': 84, 'po': 84, 'At': 85, 'AT': 85, 'at': 85, 'Rn': 86, 'RN': 86, 'rn': 86, 'Fr': 87, 'FR': 87, 'fr': 87, 'Ra': 88, 'RA': 88, 'ra': 88, 'Ac': 89, 'AC': 89, 'ac': 89, 'Th': 90, 'TH': 90, 'th': 90, 'Pa': 91, 'PA': 91, 'pa': 91, 'U': 92, 'u': 92, 'Np': 93, 'NP': 93, 'np': 93, 'Pu': 94, 'PU': 94, 'pu': 94, 'Am': 95, 'AM': 95, 'am': 95, 'Cm': 96, 'CM': 96, 'cm': 96, 'Bk': 97, 'BK': 97, 'bk': 97, 'Cf': 98, 'CF': 98, 'cf': 98, 'Es': 99, 'ES': 99, 'es': 99, 'Fm': 100, 'FM': 100, 'fm': 100, 'Md': 101, 'MD': 101, 'md': 101, 'No': 102, 'NO': 102, 'no': 102, 'Lr': 103, 'LR': 103, 'lr': 103, 'Rf': 104, 'RF': 104, 'rf': 104, 'Db': 105, 'DB': 105, 'db': 105, 'Sg': 106, 'SG': 106, 'sg': 106, 'Bh': 107, 'BH': 107, 'bh': 107, 'Hs': 108, 'HS': 108, 'hs': 108, 'Mt': 109, 'MT': 109, 'mt': 109 };
|
||||
|
||||
const __ElementBondThresholds: { [e: number]: number | undefined } = { 0: 1.42, 1: 1.42, 3: 2.7, 4: 2.7, 6: 1.75, 7: 1.6, 8: 1.52, 11: 2.7, 12: 2.7, 13: 2.7, 14: 1.9, 15: 1.9, 16: 1.9, 17: 1.8, 19: 2.7, 20: 2.7, 21: 2.7, 22: 2.7, 23: 2.7, 24: 2.7, 25: 2.7, 26: 2.7, 27: 2.7, 28: 2.7, 29: 2.7, 30: 2.7, 31: 2.7, 33: 2.68, 37: 2.7, 38: 2.7, 39: 2.7, 40: 2.7, 41: 2.7, 42: 2.7, 43: 2.7, 44: 2.7, 45: 2.7, 46: 2.7, 47: 2.7, 48: 2.7, 49: 2.7, 50: 2.7, 55: 2.7, 56: 2.7, 57: 2.7, 58: 2.7, 59: 2.7, 60: 2.7, 61: 2.7, 62: 2.7, 63: 2.7, 64: 2.7, 65: 2.7, 66: 2.7, 67: 2.7, 68: 2.7, 69: 2.7, 70: 2.7, 71: 2.7, 72: 2.7, 73: 2.7, 74: 2.7, 75: 2.7, 76: 2.7, 77: 2.7, 78: 2.7, 79: 2.7, 80: 2.7, 81: 2.7, 82: 2.7, 83: 2.7, 87: 2.7, 88: 2.7, 89: 2.7, 90: 2.7, 91: 2.7, 92: 2.7, 93: 2.7, 94: 2.7, 95: 2.7, 96: 2.7, 97: 2.7, 98: 2.7, 99: 2.7, 100: 2.7, 101: 2.7, 102: 2.7, 103: 2.7, 104: 2.7, 105: 2.7, 106: 2.7, 107: 2.7, 108: 2.7, 109: 2.88 };
|
||||
// increased P (15) threshold from 1.9 to 2.0 (e.g. for G16 in 1O08)
|
||||
const __ElementBondThresholds: { [e: number]: number | undefined } = { 0: 1.42, 1: 1.42, 3: 2.7, 4: 2.7, 6: 1.75, 7: 1.6, 8: 1.52, 11: 2.7, 12: 2.7, 13: 2.7, 14: 1.9, 15: 2.0, 16: 1.9, 17: 1.8, 19: 2.7, 20: 2.7, 21: 2.7, 22: 2.7, 23: 2.7, 24: 2.7, 25: 2.7, 26: 2.7, 27: 2.7, 28: 2.7, 29: 2.7, 30: 2.7, 31: 2.7, 33: 2.68, 37: 2.7, 38: 2.7, 39: 2.7, 40: 2.7, 41: 2.7, 42: 2.7, 43: 2.7, 44: 2.7, 45: 2.7, 46: 2.7, 47: 2.7, 48: 2.7, 49: 2.7, 50: 2.7, 55: 2.7, 56: 2.7, 57: 2.7, 58: 2.7, 59: 2.7, 60: 2.7, 61: 2.7, 62: 2.7, 63: 2.7, 64: 2.7, 65: 2.7, 66: 2.7, 67: 2.7, 68: 2.7, 69: 2.7, 70: 2.7, 71: 2.7, 72: 2.7, 73: 2.7, 74: 2.7, 75: 2.7, 76: 2.7, 77: 2.7, 78: 2.7, 79: 2.7, 80: 2.7, 81: 2.7, 82: 2.7, 83: 2.7, 87: 2.7, 88: 2.7, 89: 2.7, 90: 2.7, 91: 2.7, 92: 2.7, 93: 2.7, 94: 2.7, 95: 2.7, 96: 2.7, 97: 2.7, 98: 2.7, 99: 2.7, 100: 2.7, 101: 2.7, 102: 2.7, 103: 2.7, 104: 2.7, 105: 2.7, 106: 2.7, 107: 2.7, 108: 2.7, 109: 2.88 };
|
||||
|
||||
const __ElementPairThresholds: { [e: number]: number | undefined } = { 0: 0.8, 20: 1.31, 27: 1.3, 35: 1.3, 44: 1.05, 54: 1, 60: 1.84, 72: 1.88, 84: 1.75, 85: 1.56, 86: 1.76, 98: 1.6, 99: 1.68, 100: 1.63, 112: 1.55, 113: 1.59, 114: 1.36, 129: 1.45, 144: 1.6, 170: 1.4, 180: 1.55, 202: 2.4, 222: 2.24, 224: 1.91, 225: 1.98, 243: 2.02, 269: 2, 293: 1.9, 480: 2.3, 512: 2.3, 544: 2.3, 612: 2.1, 629: 1.54, 665: 1, 813: 2.6, 854: 2.27, 894: 1.93, 896: 2.1, 937: 2.05, 938: 2.06, 981: 1.62, 1258: 2.68, 1309: 2.33, 1484: 1, 1763: 2.14, 1823: 2.48, 1882: 2.1, 1944: 1.72, 2380: 2.34, 3367: 2.44, 3733: 2.11, 3819: 2.6, 3821: 2.36, 4736: 2.75, 5724: 2.73, 5959: 2.63, 6519: 2.84, 6750: 2.87, 8991: 2.81 };
|
||||
// increased N-N (112) threshold from 1.55 to 1.6 (e.g. for 0QH in 1BMA)
|
||||
const __ElementPairThresholds: { [e: number]: number | undefined } = { 0: 0.8, 20: 1.31, 27: 1.3, 35: 1.3, 44: 1.05, 54: 1, 60: 1.84, 72: 1.88, 84: 1.75, 85: 1.56, 86: 1.76, 98: 1.6, 99: 1.68, 100: 1.63, 112: 1.6, 113: 1.59, 114: 1.36, 129: 1.45, 144: 1.6, 170: 1.4, 180: 1.55, 202: 2.4, 222: 2.24, 224: 1.91, 225: 1.98, 243: 2.02, 269: 2, 293: 1.9, 480: 2.3, 512: 2.3, 544: 2.3, 612: 2.1, 629: 1.54, 665: 1, 813: 2.6, 854: 2.27, 894: 1.93, 896: 2.1, 937: 2.05, 938: 2.06, 981: 1.62, 1258: 2.68, 1309: 2.33, 1484: 1, 1763: 2.14, 1823: 2.48, 1882: 2.1, 1944: 1.72, 2380: 2.34, 3367: 2.44, 3733: 2.11, 3819: 2.6, 3821: 2.36, 4736: 2.75, 5724: 2.73, 5959: 2.63, 6519: 2.84, 6750: 2.87, 8991: 2.81 };
|
||||
|
||||
const __DefaultBondingRadius = 2.001;
|
||||
|
||||
|
||||
@@ -36,12 +36,11 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
|
||||
if (!this.ctx.canvas3d) return;
|
||||
|
||||
const p = this.params;
|
||||
const durationMs = typeof p.durationMs === 'undefined' ? 250 : p.durationMs;
|
||||
if (Binding.match(this.params.bindings.clickCenterFocus, buttons, modifiers)) {
|
||||
const sphere = Loci.getBoundingSphere(current.loci);
|
||||
if (sphere) {
|
||||
const radius = Math.max(sphere.radius + p.extraRadius, p.minRadius);
|
||||
this.ctx.canvas3d.camera.focus(sphere.center, radius, durationMs);
|
||||
this.ctx.canvas3d.camera.focus(sphere.center, radius, p.durationMs);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { StateSelection } from '../../../mol-state';
|
||||
import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
|
||||
import { Binding } from '../../../mol-util/binding';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { EmptyLoci } from '../../../mol-model/loci';
|
||||
|
||||
const B = ButtonsType
|
||||
const M = ModifiersKeys
|
||||
@@ -43,13 +44,20 @@ export const HighlightLoci = PluginBehavior.create({
|
||||
register() {
|
||||
this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, buttons, modifiers }) => {
|
||||
if (!this.ctx.canvas3d) return
|
||||
let matched = false
|
||||
|
||||
if (Binding.match(this.params.bindings.hoverHighlightOnly, buttons, modifiers)) {
|
||||
this.ctx.interactivity.lociHighlights.highlightOnly(current)
|
||||
matched = true
|
||||
}
|
||||
|
||||
if (Binding.match(this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) {
|
||||
this.ctx.interactivity.lociHighlights.highlightOnlyExtend(current)
|
||||
matched = true
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
this.ctx.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci })
|
||||
}
|
||||
});
|
||||
this.ctx.interactivity.lociHighlights.addProvider(this.lociMarkProvider)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Structure, StructureElement } from '../../../../mol-model/structure';
|
||||
import { Structure, StructureElement, Link } from '../../../../mol-model/structure';
|
||||
import { PluginBehavior } from '../../../../mol-plugin/behavior';
|
||||
import { PluginCommands } from '../../../../mol-plugin/command';
|
||||
import { PluginStateObject } from '../../../../mol-plugin/state/objects';
|
||||
@@ -17,10 +17,9 @@ import { StateObjectCell, StateSelection, StateTransform } from '../../../../mol
|
||||
import { BuiltInColorThemes } from '../../../../mol-theme/color';
|
||||
import { BuiltInSizeThemes } from '../../../../mol-theme/size';
|
||||
import { ButtonsType, ModifiersKeys } from '../../../../mol-util/input/input-observer';
|
||||
import { Representation } from '../../../../mol-repr/representation';
|
||||
import { Binding } from '../../../../mol-util/binding';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { isEmptyLoci } from '../../../../mol-model/loci';
|
||||
import { isEmptyLoci, Loci, EmptyLoci } from '../../../../mol-model/loci';
|
||||
|
||||
const B = ButtonsType
|
||||
const M = ModifiersKeys
|
||||
@@ -117,19 +116,19 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
|
||||
}
|
||||
|
||||
register(ref: string): void {
|
||||
let lastLoci: Representation.Loci = Representation.Loci.Empty;
|
||||
let lastLoci: Loci = EmptyLoci;
|
||||
|
||||
this.subscribeObservable(this.plugin.events.state.object.removed, o => {
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.obj) || lastLoci.loci.kind !== 'element-loci') return;
|
||||
if (lastLoci.loci.structure === o.obj.data) {
|
||||
lastLoci = Representation.Loci.Empty;
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(lastLoci)) return;
|
||||
if (lastLoci.structure === o.obj.data) {
|
||||
lastLoci = EmptyLoci;
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribeObservable(this.plugin.events.state.object.updated, o => {
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || lastLoci.loci.kind !== 'element-loci') return;
|
||||
if (lastLoci.loci.structure === o.oldObj.data) {
|
||||
lastLoci = Representation.Loci.Empty;
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(lastLoci)) return;
|
||||
if (lastLoci.structure === o.oldObj.data) {
|
||||
lastLoci = EmptyLoci;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -139,26 +138,36 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
|
||||
if (Binding.match(clickInteractionAroundOnly, buttons, modifiers)) {
|
||||
if (isEmptyLoci(current.loci)) {
|
||||
this.clear(StateTransform.RootRef);
|
||||
lastLoci = current;
|
||||
lastLoci = current.loci;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: support link and structure loci as well?
|
||||
if (!StructureElement.Loci.is(current.loci)) return;
|
||||
let loci: StructureElement.Loci;
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
loci = current.loci;
|
||||
} else if (Link.isLoci(current.loci)) {
|
||||
loci = Link.toStructureElementLoci(current.loci);
|
||||
} else if (Structure.isLoci(current.loci)) {
|
||||
loci = Structure.toStructureElementLoci(current.loci);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = this.plugin.helpers.substructureParent.get(current.loci.structure);
|
||||
if (StructureElement.Loci.isEmpty(loci)) return;
|
||||
|
||||
const parent = this.plugin.helpers.substructureParent.get(loci.structure);
|
||||
if (!parent || !parent.obj) return;
|
||||
|
||||
if (Representation.Loci.areEqual(lastLoci, current)) {
|
||||
lastLoci = Representation.Loci.Empty;
|
||||
if (Loci.areEqual(lastLoci, loci)) {
|
||||
lastLoci = EmptyLoci;
|
||||
this.clear(parent.transform.ref);
|
||||
return;
|
||||
}
|
||||
|
||||
lastLoci = current;
|
||||
lastLoci = loci;
|
||||
|
||||
const core = MS.struct.modifier.wholeResidues([
|
||||
StructureElement.Loci.toExpression(current.loci)
|
||||
StructureElement.Loci.toExpression(loci)
|
||||
]);
|
||||
|
||||
const surroundings = MS.struct.modifier.includeSurroundings({
|
||||
|
||||
@@ -22,9 +22,10 @@ import { PluginCommands } from '../../../command';
|
||||
import { StateSelection } from '../../../../mol-state';
|
||||
import { Representation } from '../../../../mol-repr/representation';
|
||||
import { ButtonsType, ModifiersKeys } from '../../../../mol-util/input/input-observer';
|
||||
import { StructureElement, Link } from '../../../../mol-model/structure';
|
||||
import { StructureElement, Link, Structure } from '../../../../mol-model/structure';
|
||||
import { PluginContext } from '../../../context';
|
||||
import { Binding } from '../../../../mol-util/binding';
|
||||
import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
|
||||
|
||||
const B = ButtonsType
|
||||
const M = ModifiersKeys
|
||||
@@ -49,11 +50,11 @@ export namespace VolumeStreaming {
|
||||
valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
|
||||
};
|
||||
|
||||
const DefaultBindings = {
|
||||
export const DefaultBindings = {
|
||||
clickVolumeAroundOnly: Binding(Trigger(B.Flag.Secondary, M.create()), 'Show the volume around only the clicked element using ${trigger}.'),
|
||||
}
|
||||
|
||||
export function createParams(data?: VolumeServerInfo.Data, defaultView?: ViewTypes) {
|
||||
export function createParams(data?: VolumeServerInfo.Data, defaultView?: ViewTypes, binding?: typeof DefaultBindings) {
|
||||
// fake the info
|
||||
const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) };
|
||||
const box = (data && data.structure.boundary.box) || Box3D.empty();
|
||||
@@ -84,7 +85,7 @@ export namespace VolumeStreaming {
|
||||
'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), VolumeIsoValue.relative(3), info.header.sampling[0].valuesInfo[1]),
|
||||
'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), VolumeIsoValue.relative(-3), info.header.sampling[0].valuesInfo[1]),
|
||||
}, { isFlat: true }),
|
||||
bindings: PD.Value(DefaultBindings, { isHidden: true }),
|
||||
bindings: PD.Value(binding || DefaultBindings, { isHidden: true }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ export namespace VolumeStreaming {
|
||||
export class Behavior extends PluginBehavior.WithSubscribers<Params> {
|
||||
private cache = LRUCache.create<ChannelsData>(25);
|
||||
public params: Params = {} as any;
|
||||
private lastLoci: Representation.Loci = Representation.Loci.Empty;
|
||||
private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
|
||||
private ref: string = '';
|
||||
|
||||
channels: Channels = {}
|
||||
@@ -198,68 +199,75 @@ export namespace VolumeStreaming {
|
||||
this.ref = ref;
|
||||
|
||||
this.subscribeObservable(this.plugin.events.state.object.removed, o => {
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.obj) || this.lastLoci.loci.kind !== 'element-loci') return;
|
||||
if (this.lastLoci.loci.structure === o.obj.data) {
|
||||
this.lastLoci = Representation.Loci.Empty;
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(this.lastLoci)) return;
|
||||
if (this.lastLoci.structure === o.obj.data) {
|
||||
this.lastLoci = EmptyLoci;
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribeObservable(this.plugin.events.state.object.updated, o => {
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || this.lastLoci.loci.kind !== 'element-loci') return;
|
||||
if (this.lastLoci.loci.structure === o.oldObj.data) {
|
||||
this.lastLoci = Representation.Loci.Empty;
|
||||
if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(this.lastLoci)) return;
|
||||
if (this.lastLoci.structure === o.oldObj.data) {
|
||||
this.lastLoci = EmptyLoci;
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
|
||||
if (!Binding.match(this.params.bindings.clickVolumeAroundOnly || DefaultBindings.clickVolumeAroundOnly, buttons, modifiers)) return;
|
||||
if (!Binding.match((this.params.bindings && this.params.bindings.clickVolumeAroundOnly) || DefaultBindings.clickVolumeAroundOnly, buttons, modifiers)) return;
|
||||
if (this.params.view.name !== 'selection-box') {
|
||||
this.lastLoci = current;
|
||||
this.lastLoci = this.getNormalizedLoci(current.loci);
|
||||
} else {
|
||||
this.updateInteraction(current);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getBoxFromLoci(current: Representation.Loci) {
|
||||
if (current.loci.kind === 'empty-loci') {
|
||||
return;
|
||||
}
|
||||
|
||||
let loci: StructureElement.Loci;
|
||||
|
||||
// TODO: support structure loci as well?
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
loci = current.loci;
|
||||
} else if (Link.isLoci(current.loci) && current.loci.links.length !== 0) {
|
||||
loci = Link.toStructureElementLoci(current.loci);
|
||||
private getNormalizedLoci(loci: Loci): StructureElement.Loci | EmptyLoci {
|
||||
if (StructureElement.Loci.is(loci)) {
|
||||
return loci;
|
||||
} else if (Link.isLoci(loci)) {
|
||||
return Link.toStructureElementLoci(loci);
|
||||
} else if (Structure.isLoci(loci)) {
|
||||
return Structure.toStructureElementLoci(loci);
|
||||
} else {
|
||||
return;
|
||||
return EmptyLoci;
|
||||
}
|
||||
}
|
||||
|
||||
private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
|
||||
if (isEmptyLoci(loci) || StructureElement.Loci.isEmpty(loci)) {
|
||||
return Box3D.empty();
|
||||
}
|
||||
|
||||
const parent = this.plugin.helpers.substructureParent.get(loci.structure);
|
||||
if (!parent) return;
|
||||
if (!parent) return Box3D.empty();
|
||||
const root = this.getStructureRoot();
|
||||
if (!root || !root.obj || root.obj !== parent.obj) return;
|
||||
if (!root || !root.obj || root.obj !== parent.obj) return Box3D.empty();
|
||||
|
||||
return StructureElement.Loci.getBoundary(StructureElement.Loci.extendToWholeResidues(loci)).box;
|
||||
const extendedLoci = StructureElement.Loci.extendToWholeResidues(loci)
|
||||
const box = StructureElement.Loci.getBoundary(extendedLoci).box
|
||||
if (StructureElement.Loci.size(extendedLoci) === 1) {
|
||||
Box3D.expand(box, box, Vec3.create(1, 1, 1))
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
private updateInteraction(current: Representation.Loci) {
|
||||
if (Representation.Loci.areEqual(this.lastLoci, current)) {
|
||||
this.lastLoci = Representation.Loci.Empty;
|
||||
const loci = this.getNormalizedLoci(current.loci)
|
||||
if (Loci.areEqual(this.lastLoci, loci)) {
|
||||
this.lastLoci = EmptyLoci;
|
||||
this.updateDynamicBox(Box3D.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.loci.kind === 'empty-loci') {
|
||||
this.lastLoci = loci;
|
||||
|
||||
if (isEmptyLoci(loci)) {
|
||||
this.updateDynamicBox(Box3D.empty());
|
||||
this.lastLoci = current;
|
||||
return;
|
||||
}
|
||||
|
||||
const box = this.getBoxFromLoci(current);
|
||||
if (!box) return;
|
||||
const box = this.getBoxFromLoci(loci);
|
||||
this.updateDynamicBox(box);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export const InitVolumeStreaming = StateAction.build({
|
||||
defaultView: PD.Select<VolumeStreaming.ViewTypes>(method === 'em' ? 'cell' : 'selection-box', VolumeStreaming.ViewTypeOptions as any),
|
||||
behaviorRef: PD.Text('', { isHidden: true }),
|
||||
emContourProvider: PD.Select<'wwpdb' | 'pdbe'>('wwpdb', [['wwpdb', 'wwPDB'], ['pdbe', 'PDBe']], { isHidden: true }),
|
||||
bindings: PD.Value(VolumeStreaming.DefaultBindings, { isHidden: true }),
|
||||
};
|
||||
},
|
||||
isApplicable: (a) => a.data.models.length === 1
|
||||
@@ -61,7 +62,7 @@ export const InitVolumeStreaming = StateAction.build({
|
||||
const infoObj = await state.updateTree(infoTree).runInContext(taskCtx);
|
||||
|
||||
const behTree = state.build().to(infoTree.ref).apply(CreateVolumeStreamingBehavior,
|
||||
PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data, params.defaultView)),
|
||||
PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data, params.defaultView, params.bindings)),
|
||||
{ ref: params.behaviorRef ? params.behaviorRef : void 0 });
|
||||
|
||||
if (params.method === 'em') {
|
||||
@@ -147,7 +148,8 @@ const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({
|
||||
})({
|
||||
canAutoUpdate: ({ oldParams, newParams }) => {
|
||||
return oldParams.view === newParams.view
|
||||
|| (oldParams.view.name === newParams.view.name && oldParams.view.name === 'selection-box');
|
||||
|| newParams.view.name === 'selection-box'
|
||||
|| newParams.view.name === 'off';
|
||||
},
|
||||
apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {
|
||||
const behavior = new VolumeStreaming.Behavior(plugin, a.data);
|
||||
|
||||
@@ -12,6 +12,8 @@ import { PluginStateObject as SO } from '../../state/objects';
|
||||
import { getFormattedTime } from '../../../mol-util/date';
|
||||
import { readFromFile } from '../../../mol-util/data-source';
|
||||
import { download } from '../../../mol-util/download';
|
||||
import { Structure } from '../../../mol-model/structure';
|
||||
import { EmptyLoci, EveryLoci } from '../../../mol-model/loci';
|
||||
|
||||
export function registerDefault(ctx: PluginContext) {
|
||||
SyncBehaviors(ctx);
|
||||
@@ -99,24 +101,25 @@ function setVisibilityVisitor(t: StateTransform, tree: StateTree, ctx: { state:
|
||||
ctx.state.updateCellState(t.ref, { isHidden: ctx.value });
|
||||
}
|
||||
|
||||
// TODO make isHighlighted and isSelect part of StateObjectCell.State and subscribe from there???
|
||||
// TODO select structures of subtree
|
||||
// TODO should also work for volumes and shapes
|
||||
export function Highlight(ctx: PluginContext) {
|
||||
PluginCommands.State.Highlight.subscribe(ctx, ({ state, ref }) => {
|
||||
// const cell = state.select(ref)[0]
|
||||
// const repr = cell && SO.isRepresentation3D(cell.obj) ? cell.obj.data : undefined
|
||||
// if (cell && cell.obj && cell.obj.type === PluginStateObject.Molecule.Structure.type) {
|
||||
// ctx.behaviors.canvas3d.highlight.next({ current: { loci: Structure.Loci(cell.obj.data) } });
|
||||
// } else if (repr) {
|
||||
// ctx.behaviors.canvas3d.highlight.next({ current: { loci: EveryLoci, repr } });
|
||||
// }
|
||||
const cell = state.select(ref)[0];
|
||||
if (!cell) return;
|
||||
if (SO.Molecule.Structure.is(cell.obj)) {
|
||||
ctx.interactivity.lociHighlights.highlightOnly({ loci: Structure.Loci(cell.obj.data) });
|
||||
} else if (cell && SO.isRepresentation3D(cell.obj)) {
|
||||
const loci = SO.Molecule.Structure.is(cell.obj.data.source) ? Structure.Loci(cell.obj.data.source.data) : EveryLoci
|
||||
ctx.interactivity.lociHighlights.highlightOnly({ loci, repr: cell.obj.data.repr });
|
||||
}
|
||||
|
||||
// TODO: highlight volumes and shapes?
|
||||
// TODO: select structures of subtree?
|
||||
});
|
||||
}
|
||||
|
||||
export function ClearHighlight(ctx: PluginContext) {
|
||||
PluginCommands.State.ClearHighlight.subscribe(ctx, ({ state, ref }) => {
|
||||
// ctx.behaviors.canvas3d.highlight.next({ current: { loci: EmptyLoci } });
|
||||
ctx.interactivity.lociHighlights.highlightOnly({ loci: EmptyLoci });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PluginLayoutStateProps } from './layout';
|
||||
import { StructureElement } from '../mol-model/structure';
|
||||
import { PluginState } from './state';
|
||||
import { Interactivity } from './util/interactivity';
|
||||
import { PluginToast } from './state/toast';
|
||||
|
||||
export * from './command/base';
|
||||
|
||||
@@ -53,6 +54,10 @@ export const PluginCommands = {
|
||||
Layout: {
|
||||
Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>()
|
||||
},
|
||||
Toast: {
|
||||
Show: PluginCommand<PluginToast>(),
|
||||
Hide: PluginCommand<{ key: string }>()
|
||||
},
|
||||
Camera: {
|
||||
Reset: PluginCommand<{}>(),
|
||||
SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(),
|
||||
|
||||
@@ -40,6 +40,7 @@ import { Interactivity } from './util/interactivity';
|
||||
import { StructureRepresentationHelper } from './util/structure-representation-helper';
|
||||
import { StructureSelectionHelper } from './util/structure-selection-helper';
|
||||
import { StructureOverpaintHelper } from './util/structure-overpaint-helper';
|
||||
import { PluginToastManager } from './state/toast';
|
||||
|
||||
interface Log {
|
||||
entries: List<LogEntry>
|
||||
@@ -100,7 +101,8 @@ export class PluginContext {
|
||||
} as const
|
||||
|
||||
readonly canvas3d: Canvas3D;
|
||||
readonly layout: PluginLayout = new PluginLayout(this);
|
||||
readonly layout = new PluginLayout(this);
|
||||
readonly toasts = new PluginToastManager(this);
|
||||
readonly interactivity: Interactivity;
|
||||
|
||||
readonly lociLabels: LociLabelManager;
|
||||
@@ -135,7 +137,7 @@ export class PluginContext {
|
||||
* Used to store application specific custom state which is then available
|
||||
* to State Actions and similar constructs via the PluginContext.
|
||||
*/
|
||||
readonly customState: any = Object.create(null);
|
||||
readonly customState: unknown = Object.create(null);
|
||||
|
||||
initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
|
||||
try {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const DefaultPluginSpec: PluginSpec = {
|
||||
PluginSpec.Action(StateTransforms.Model.TrajectoryFromPDB),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureSymmetryMatesFromModel),
|
||||
PluginSpec.Action(TransformStructureConformation),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureFromModel),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureFromTrajectory),
|
||||
@@ -49,6 +50,7 @@ export const DefaultPluginSpec: PluginSpec = {
|
||||
PluginSpec.Action(StateTransforms.Model.StructureSelectionFromScript),
|
||||
PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.StructureLabels3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.ModelUnitcell3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3DFromScript),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../mol-util/param-definition';
|
||||
@@ -39,7 +40,7 @@ interface RootState {
|
||||
position: string | null,
|
||||
overflow: string | null,
|
||||
viewports: HTMLElement[],
|
||||
zindex: string | null
|
||||
zIndex: string | null
|
||||
}
|
||||
|
||||
export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
|
||||
@@ -48,7 +49,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
|
||||
}
|
||||
|
||||
private updateProps(state: Partial<PluginLayoutStateProps>) {
|
||||
let prevExpanded = !!this.state.isExpanded;
|
||||
const prevExpanded = !!this.state.isExpanded;
|
||||
this.updateState(state);
|
||||
if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
|
||||
|
||||
@@ -76,15 +77,15 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
|
||||
|
||||
private handleExpand() {
|
||||
try {
|
||||
let body = document.getElementsByTagName('body')[0];
|
||||
let head = document.getElementsByTagName('head')[0];
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
|
||||
if (!body || !head || !this.root) return;
|
||||
|
||||
if (this.state.isExpanded) {
|
||||
let children = head.children;
|
||||
const children = head.children;
|
||||
const viewports: HTMLElement[] = [];
|
||||
let hasExp = false;
|
||||
let viewports: HTMLElement[] = [];
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (children[i] === this.expandedViewport) {
|
||||
hasExp = true;
|
||||
@@ -100,14 +101,14 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
|
||||
if (!hasExp) head.appendChild(this.expandedViewport);
|
||||
|
||||
|
||||
let s = body.style;
|
||||
const s = body.style;
|
||||
|
||||
let doc = this.getScrollElement();
|
||||
let scrollLeft = doc.scrollLeft;
|
||||
let scrollTop = doc.scrollTop;
|
||||
const doc = this.getScrollElement();
|
||||
const scrollLeft = doc.scrollLeft;
|
||||
const scrollTop = doc.scrollTop;
|
||||
|
||||
this.rootState = {
|
||||
top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zindex: this.root.style.zIndex,
|
||||
top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zIndex: this.root.style.zIndex,
|
||||
width: s.width, height: s.height,
|
||||
maxWidth: s.maxWidth, maxHeight: s.maxHeight,
|
||||
margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom
|
||||
@@ -133,7 +134,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
|
||||
// TODO: setting this breaks viewport controls for some reason. Is there a fix?
|
||||
// this.root.style.zIndex = '100000';
|
||||
} else {
|
||||
let children = head.children;
|
||||
const children = head.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (children[i] === this.expandedViewport) {
|
||||
head.removeChild(this.expandedViewport);
|
||||
@@ -142,10 +143,12 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
|
||||
}
|
||||
|
||||
if (this.rootState) {
|
||||
let s = body.style, t = this.rootState;
|
||||
const t = this.rootState;
|
||||
for (let v of t.viewports) {
|
||||
head.appendChild(v);
|
||||
}
|
||||
|
||||
const s = body.style
|
||||
s.top = t.top;
|
||||
s.bottom = t.bottom;
|
||||
s.left = t.left;
|
||||
@@ -162,17 +165,20 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
|
||||
s.marginBottom = t.marginBottom;
|
||||
|
||||
s.position = t.position;
|
||||
s.overflow = t.overflow || 'fixed';
|
||||
let doc = this.getScrollElement();
|
||||
s.overflow = t.overflow || '';
|
||||
|
||||
const doc = this.getScrollElement();
|
||||
doc.scrollTop = t.scrollTop;
|
||||
doc.scrollLeft = t.scrollLeft;
|
||||
|
||||
this.rootState = void 0;
|
||||
this.root.style.zIndex = t.zindex;
|
||||
this.root.style.zIndex = t.zIndex;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.context.log.error('Layout change error, you might have to reload the page.');
|
||||
console.log('Layout change error, you might have to reload the page.', e);
|
||||
const msg = 'Layout change error, you might have to reload the page.'
|
||||
this.context.log.error(msg);
|
||||
console.error(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
|
||||
&[disabled] {
|
||||
background: $default-background;
|
||||
opacity: 0.35;
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
.msp-btn-link, .msp-btn-link:active, .msp-btn-link:focus {
|
||||
color: $msp-btn-link-font-color;
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.msp-btn-link:hover {
|
||||
@@ -75,13 +75,13 @@
|
||||
.msp-btn-#{$name}, .msp-btn-#{$name}:active, .msp-btn-#{$name}:focus {
|
||||
color: $font;
|
||||
background: $bg;
|
||||
}
|
||||
}
|
||||
.msp-btn-#{$name}:hover {
|
||||
color: $hover-font-color;
|
||||
background: color-lower-contrast($bg, 2.5%);
|
||||
}
|
||||
|
||||
.msp-btn-#{$name}[disabled], .msp-btn-#{$name}[disabled]:hover,
|
||||
|
||||
.msp-btn-#{$name}[disabled], .msp-btn-#{$name}[disabled]:hover,
|
||||
.msp-btn-#{$name}[disabled]:active, .msp-btn-#{$name}[disabled]:focus {
|
||||
color: color-lower-contrast($font, 1%);
|
||||
}
|
||||
@@ -104,33 +104,33 @@
|
||||
line-height: $row-height;
|
||||
}
|
||||
|
||||
.msp-form-control {
|
||||
.msp-form-control {
|
||||
width: 100%;
|
||||
background: $msp-form-control-background;
|
||||
color: $font-color;
|
||||
border: none; // !important;
|
||||
padding: 0 $control-spacing;
|
||||
line-height: $row-height - 2px;
|
||||
padding: 0 $control-spacing;
|
||||
line-height: $row-height - 2px;
|
||||
height: $row-height;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
appearance: none;
|
||||
box-shadow: none !important;
|
||||
|
||||
|
||||
&:hover {
|
||||
color: $hover-font-color;
|
||||
background-color: color-increase-contrast($msp-form-control-background, 5%);
|
||||
border: none;
|
||||
outline-offset: -1px;
|
||||
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%);
|
||||
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%);
|
||||
}
|
||||
|
||||
|
||||
&:active, &:focus {
|
||||
color: $font-color;
|
||||
background-color: $msp-form-control-background;
|
||||
border: none;
|
||||
outline-offset: 0;
|
||||
outline: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.msp-btn-commit {
|
||||
.msp-btn-commit {
|
||||
text-align: right;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
@@ -148,22 +148,23 @@
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
font-weight: bold;
|
||||
|
||||
|
||||
.msp-icon {
|
||||
display: block-inline;
|
||||
line-height: $row-height;
|
||||
width: $row-height;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select.msp-form-control {
|
||||
select.msp-form-control {
|
||||
background: none;
|
||||
background-color: $msp-form-control-background;
|
||||
background-size: 8px 12px;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right $control-spacing top (($row-height - 12px) / 2);
|
||||
background-position: right $control-spacing top (($row-height - 12px) / 2);
|
||||
padding-right: ($row-height - 8px);
|
||||
}
|
||||
|
||||
select.msp-form-control:-moz-focusring {
|
||||
|
||||
@@ -362,4 +362,25 @@
|
||||
margin: 0 ($control-spacing / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msp-image-preview {
|
||||
position: relative;
|
||||
background: $default-background;
|
||||
margin-top: 1px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
> canvas {
|
||||
max-height: 200px;
|
||||
border-width: 0px 1px 0px 1px;
|
||||
border-style: solid;
|
||||
border-color: $border-color;
|
||||
|
||||
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);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
}
|
||||
}
|
||||
@@ -70,4 +70,33 @@
|
||||
|
||||
.msp-controls-section {
|
||||
margin-bottom: $control-spacing;
|
||||
}
|
||||
|
||||
.msp-combined-color-button {
|
||||
border: 4px solid $msp-form-control-background !important;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
padding-right: $control-spacing;
|
||||
padding-left: $control-spacing;
|
||||
|
||||
&:hover {
|
||||
border-color: color-increase-contrast($msp-form-control-background, 5%) !important;
|
||||
border: none;
|
||||
outline-offset: -1px !important;
|
||||
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.msp-combined-color-swatch {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-gap: 1px;
|
||||
grid-template-columns: repeat(6, auto);
|
||||
|
||||
.msp-btn {
|
||||
&:hover {
|
||||
outline-offset: -1px !important;
|
||||
outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.msp-sequence-wrapper-non-empty {
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
.msp-sequence-wrapper {
|
||||
span {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
|
||||
text-align-last: center;
|
||||
background: none !important;
|
||||
padding: 0 $control-spacing;
|
||||
|
||||
> option[value = _] {
|
||||
display: none;
|
||||
@@ -224,13 +225,22 @@
|
||||
float: left;
|
||||
margin-right: $control-spacing;
|
||||
position: relative;
|
||||
// background-color: $msp-form-control-background;
|
||||
// background: $default-background;
|
||||
|
||||
> div:first-child {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
> button {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.msp-animation-viewport-controls-select {
|
||||
width: 290px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: $row-height + $control-spacing;
|
||||
margin-top: $control-spacing;
|
||||
background: $control-background;
|
||||
|
||||
.msp-control-row:first-child {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
.msp-toast-container {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
bottom: $control-spacing;
|
||||
right: $control-spacing;
|
||||
margin-left: $control-spacing;
|
||||
position: relative;
|
||||
// bottom: $control-spacing;
|
||||
// right: $control-spacing;
|
||||
// margin-left: $control-spacing;
|
||||
z-index: 1001;
|
||||
|
||||
.msp-toast-entry {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.msp-transformer {
|
||||
|
||||
.msp-transformer {
|
||||
|
||||
.msp-entity-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -15,24 +15,24 @@
|
||||
}
|
||||
|
||||
.msp-transformer-wrapper {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.msp-transformer-wrapper {
|
||||
|
||||
.msp-transformer-wrapper {
|
||||
|
||||
.msp-entity-badge {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
|
||||
&:first-child {
|
||||
.msp-panel-description-content {
|
||||
top: $row-height + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:not(:first-child) {
|
||||
.msp-panel-description-content {
|
||||
.msp-panel-description-content {
|
||||
bottom: $row-height + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
-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);
|
||||
background-size: 60px 60px;
|
||||
background-position: 0 0, 30px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.msp-viewport-controls {
|
||||
@@ -38,21 +46,15 @@
|
||||
|
||||
.msp-viewport-controls-buttons {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
|
||||
> button {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: $row-height;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> button:last-child {
|
||||
margin-left: $control-spacing;
|
||||
}
|
||||
|
||||
// .msp-btn-link, .msp-btn-link-toggle-on {
|
||||
// color: #eee;
|
||||
// }
|
||||
|
||||
.msp-btn-link-toggle-off {
|
||||
color: $msp-btn-link-toggle-off-font-color;
|
||||
}
|
||||
@@ -62,6 +64,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.msp-semi-transparent-background {
|
||||
background: $default-background;
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.msp-viewport-controls-scene-options {
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
@@ -69,29 +81,32 @@
|
||||
right: 0px;
|
||||
position: absolute;
|
||||
background: $control-background;
|
||||
margin-top: $control-spacing;
|
||||
|
||||
.msp-control-group-wrapper:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* highlight */
|
||||
/* highlight & toasts */
|
||||
|
||||
.msp-highlight-toast-wrapper {
|
||||
position: absolute;
|
||||
right: $control-spacing;
|
||||
bottom: $control-spacing;
|
||||
max-width: 95%;
|
||||
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.msp-highlight-info {
|
||||
|
||||
color: $highlight-info-font-color;
|
||||
padding: $info-vertical-padding $control-spacing;
|
||||
background: $default-background; //$highlight-info-background;
|
||||
|
||||
position: absolute;
|
||||
right: $control-spacing;
|
||||
bottom: $control-spacing;
|
||||
text-align: right;
|
||||
min-height: $row-height;
|
||||
max-width: 95%;
|
||||
text-align: right;
|
||||
|
||||
//border-bottom-right-radius: 6px;
|
||||
z-index: 10000;
|
||||
@include non-selectable;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
.msp-layout-region {
|
||||
overflow: hidden;
|
||||
background: $default-background;
|
||||
}
|
||||
|
||||
.msp-layout-static, .msp-layout-scrollable {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
$default-background: #111318;
|
||||
$font-color: #ccd4e0;
|
||||
$font-color: #ccd4e0;
|
||||
$hover-font-color: #51A2FB;
|
||||
$entity-current-font-color: #68BEFD;
|
||||
$msp-btn-remove-background: #DE0A28;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// this is complement of the dark theme
|
||||
|
||||
@function compl($color) {
|
||||
@return rgb(255 - red($color), 255 - green($color), 255 - blue($color));
|
||||
@return rgb(255 - red($color), 255 - green($color), 255 - blue($color));
|
||||
}
|
||||
|
||||
$default-background: compl(#111318);
|
||||
@@ -19,7 +19,7 @@ $log-info: #5E3673;
|
||||
$log-warning: #FCC937;
|
||||
$log-error: #FD354B;
|
||||
|
||||
$logo-background: rgba(204,201,193,0.85);
|
||||
$logo-background: compl(#111318);
|
||||
|
||||
@function color-lower-contrast($color, $amount) {
|
||||
@return lighten($color, $amount);
|
||||
|
||||
110
src/mol-plugin/state/toast.ts
Normal file
110
src/mol-plugin/state/toast.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Adapted from LiteMol (c) David Sehnal
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginComponent } from '../component';
|
||||
import { OrderedMap } from 'immutable';
|
||||
import { PluginContext } from '../context';
|
||||
import { PluginCommands } from '../command';
|
||||
|
||||
export interface PluginToast {
|
||||
title: string,
|
||||
/**
|
||||
* The message can be either a string, html string, or an arbitrary React component.
|
||||
*/
|
||||
message: string | React.ComponentClass,
|
||||
/**
|
||||
* Only one message with a given key can be shown.
|
||||
*/
|
||||
key?: string,
|
||||
/**
|
||||
* Specify a timeout for the message in milliseconds.
|
||||
*/
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
export class PluginToastManager extends PluginComponent<{
|
||||
entries: OrderedMap<number, PluginToastManager.Entry>
|
||||
}> {
|
||||
readonly events = {
|
||||
changed: this.ev()
|
||||
};
|
||||
|
||||
private serialNumber = 0;
|
||||
private serialId = 0;
|
||||
|
||||
private findByKey(key: string): PluginToastManager.Entry | undefined {
|
||||
return this.state.entries.find(e => !!e && e.key === key)
|
||||
}
|
||||
|
||||
private show(toast: PluginToast) {
|
||||
let entries = this.state.entries;
|
||||
let e: PluginToastManager.Entry | undefined = void 0;
|
||||
const id = ++this.serialId;
|
||||
let serialNumber: number;
|
||||
if (toast.key && (e = this.findByKey(toast.key))) {
|
||||
if (e.timeout !== void 0) clearTimeout(e.timeout);
|
||||
serialNumber = e.serialNumber;
|
||||
entries = entries.remove(e.id);
|
||||
} else {
|
||||
serialNumber = ++this.serialNumber;
|
||||
}
|
||||
|
||||
e = {
|
||||
id,
|
||||
serialNumber,
|
||||
key: toast.key,
|
||||
title: toast.title,
|
||||
message: toast.message,
|
||||
timeout: this.timeout(id, toast.timeoutMs),
|
||||
hide: () => this.hideId(id)
|
||||
};
|
||||
|
||||
if (this.updateState({ entries: entries.set(id, e) })) this.events.changed.next();
|
||||
}
|
||||
|
||||
private timeout(id: number, delay?: number) {
|
||||
if (delay === void 0) return void 0;
|
||||
|
||||
if (delay < 0) delay = 500;
|
||||
return <number><any>setTimeout(() => {
|
||||
const e = this.state.entries.get(id);
|
||||
e.timeout = void 0;
|
||||
this.hide(e);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private hideId(id: number) {
|
||||
this.hide(this.state.entries.get(id));
|
||||
}
|
||||
|
||||
private hide(e: PluginToastManager.Entry | undefined) {
|
||||
if (!e) return;
|
||||
if (e.timeout !== void 0) clearTimeout(e.timeout);
|
||||
e.hide = <any>void 0;
|
||||
if (this.updateState({ entries: this.state.entries.delete(e.id) })) this.events.changed.next();
|
||||
}
|
||||
|
||||
constructor(plugin: PluginContext) {
|
||||
super({ entries: OrderedMap<number, PluginToastManager.Entry>() });
|
||||
|
||||
PluginCommands.Toast.Show.subscribe(plugin, e => this.show(e));
|
||||
PluginCommands.Toast.Hide.subscribe(plugin, e => this.hide(this.findByKey(e.key)));
|
||||
}
|
||||
}
|
||||
|
||||
export namespace PluginToastManager {
|
||||
export interface Entry {
|
||||
id: number,
|
||||
serialNumber: number,
|
||||
key?: string,
|
||||
title: string,
|
||||
message: string | React.ComponentClass,
|
||||
hide: () => void,
|
||||
timeout?: number
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ export { StructureFromTrajectory };
|
||||
export { StructureFromModel };
|
||||
export { StructureAssemblyFromModel };
|
||||
export { StructureSymmetryFromModel };
|
||||
export { StructureSymmetryMatesFromModel };
|
||||
export { TransformStructureConformation };
|
||||
export { TransformStructureConformationByMatrix };
|
||||
export { StructureSelectionFromExpression };
|
||||
@@ -301,6 +302,31 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
|
||||
}
|
||||
});
|
||||
|
||||
type StructureSymmetryMatesFromModel = typeof StructureSymmetryMatesFromModel
|
||||
const StructureSymmetryMatesFromModel = PluginStateTransform.BuiltIn({
|
||||
name: 'structure-symmetry-mates-from-model',
|
||||
display: { name: 'Structure Symmetry Mates', description: 'Create molecular structure symmetry mates.' },
|
||||
from: SO.Molecule.Model,
|
||||
to: SO.Molecule.Structure,
|
||||
params(a) {
|
||||
return {
|
||||
radius: PD.Numeric(5),
|
||||
}
|
||||
}
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Build Symmetry Mates', async ctx => {
|
||||
const { radius } = params
|
||||
const model = a.data;
|
||||
const base = Structure.ofModel(model);
|
||||
const s = await StructureSymmetry.builderSymmetryMates(base, radius).runInContext(ctx);
|
||||
await ensureSecondaryStructure(s)
|
||||
const props = { label: `Symmetry Mates`, description: structureDesc(s) };
|
||||
return new SO.Molecule.Structure(s, props);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const _translation = Vec3.zero(), _m = Mat4.zero(), _n = Mat4.zero();
|
||||
type TransformStructureConformation = typeof TransformStructureConformation
|
||||
const TransformStructureConformation = PluginStateTransform.BuiltIn({
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Overpaint } from '../../../mol-theme/overpaint';
|
||||
import { Transparency } from '../../../mol-theme/transparency';
|
||||
import { BaseGeometry } from '../../../mol-geo/geometry/base';
|
||||
import { Script } from '../../../mol-script/script';
|
||||
import { getUnitcellRepresentation, UnitcellParams } from '../../util/model-unitcell';
|
||||
|
||||
export { StructureRepresentation3D }
|
||||
export { StructureRepresentation3DHelpers }
|
||||
@@ -667,4 +668,32 @@ const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export { ModelUnitcell3D }
|
||||
type ModelUnitcell3D = typeof ModelUnitcell3D
|
||||
const ModelUnitcell3D = PluginStateTransform.BuiltIn({
|
||||
name: 'model-unitcell-3d',
|
||||
display: 'Model Unitcell',
|
||||
from: SO.Molecule.Model,
|
||||
to: SO.Shape.Representation3D,
|
||||
params: {
|
||||
...UnitcellParams,
|
||||
}
|
||||
})({
|
||||
canAutoUpdate({ oldParams, newParams }) {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params }) {
|
||||
return Task.create('Model Unitcell', async ctx => {
|
||||
const repr = await getUnitcellRepresentation(ctx, a.data, params);
|
||||
return new SO.Shape.Representation3D({ repr, source: a }, { label: `Unitcell`, description: a.data.symmetry.spacegroup.name });
|
||||
});
|
||||
},
|
||||
update({ a, b, newParams }) {
|
||||
return Task.create('Model Unitcell', async ctx => {
|
||||
await getUnitcellRepresentation(ctx, a.data, newParams, b.data.repr as ShapeRepresentation<any, any, any>);
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -95,6 +95,10 @@ export abstract class CollapsableControls<P extends CollapsableProps = Collapsab
|
||||
|
||||
constructor(props: P, context?: any) {
|
||||
super(props, context)
|
||||
this.state = this.defaultState()
|
||||
|
||||
const state = this.defaultState()
|
||||
if (props.initiallyCollapsed !== undefined) state.isCollapsed = props.initiallyCollapsed
|
||||
if (props.header !== undefined) state.header = props.header
|
||||
this.state = state
|
||||
}
|
||||
}
|
||||
@@ -223,14 +223,17 @@ export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty:
|
||||
render() {
|
||||
// if (!this.state.show) return null;
|
||||
const isPlaying = this.plugin.state.snapshots.state.isPlaying;
|
||||
if (isPlaying) return null;
|
||||
if (isPlaying || this.state.isEmpty) return null;
|
||||
|
||||
const isAnimating = this.state.isAnimating;
|
||||
|
||||
return <div className='msp-animation-viewport-controls'>
|
||||
<IconButton icon={isAnimating || isPlaying ? 'stop' : 'tape'} title={isAnimating ? 'Stop' : 'Select Animation'}
|
||||
onClick={isAnimating || isPlaying ? this.stop : this.toggleExpanded}
|
||||
disabled={isAnimating|| isPlaying ? false : this.state.isUpdating || this.state.isPlaying || this.state.isEmpty} />
|
||||
<div>
|
||||
<div className='msp-semi-transparent-background' />
|
||||
<IconButton icon={isAnimating || isPlaying ? 'stop' : 'tape'} title={isAnimating ? 'Stop' : 'Select Animation'}
|
||||
onClick={isAnimating || isPlaying ? this.stop : this.toggleExpanded}
|
||||
disabled={isAnimating|| isPlaying ? false : this.state.isUpdating || this.state.isPlaying || this.state.isEmpty} />
|
||||
</div>
|
||||
{(this.state.isExpanded && !this.state.isUpdating) && <div className='msp-animation-viewport-controls-select'>
|
||||
<AnimationControls onStart={this.toggleExpanded} />
|
||||
</div>}
|
||||
@@ -238,7 +241,7 @@ export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty:
|
||||
}
|
||||
}
|
||||
|
||||
export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> {
|
||||
export class LociLabels extends PluginUIComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> {
|
||||
state = { entries: [] }
|
||||
|
||||
componentDidMount() {
|
||||
@@ -246,7 +249,9 @@ export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyA
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.entries.length === 0) return null;
|
||||
if (this.state.entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className='msp-highlight-info'>
|
||||
{this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)}
|
||||
|
||||
145
src/mol-plugin/ui/controls/color.tsx
Normal file
145
src/mol-plugin/ui/controls/color.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames, ColorNamesValueMap } from '../../../mol-util/color/names';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { camelCaseToWords } from '../../../mol-util/string';
|
||||
import * as React from 'react';
|
||||
import { _Props, _State } from '../base';
|
||||
import { ParamProps } from './parameters';
|
||||
import { TextInput } from './common';
|
||||
|
||||
export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean }> {
|
||||
state = { isExpanded: false }
|
||||
|
||||
protected update(value: Color) {
|
||||
this.props.onChange({ param: this.props.param, name: this.props.name, value });
|
||||
}
|
||||
|
||||
toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
|
||||
onChangeSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = Color(parseInt(e.target.value));
|
||||
if (value !== this.props.value) {
|
||||
this.update(value);
|
||||
}
|
||||
}
|
||||
|
||||
onClickSwatch = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const value = Color(+(e.currentTarget.getAttribute('data-color') || '0'));
|
||||
if (value !== this.props.value) {
|
||||
this.update(value);
|
||||
}
|
||||
}
|
||||
|
||||
onChangeText = (value: Color) => {
|
||||
if (value !== this.props.value) {
|
||||
this.update(value);
|
||||
}
|
||||
}
|
||||
|
||||
swatch() {
|
||||
// const def = this.props.param.defaultValue;
|
||||
return <div className='msp-combined-color-swatch'>
|
||||
{/* <button title='Default Color' key={def} className='msp-form-control msp-btn' data-color={def} onClick={this.onClickSwatch} style={{ background: Color.toStyle(def) }}></button> */}
|
||||
{SwatchColors.map(c => <button key={c} className='msp-form-control msp-btn' data-color={c} onClick={this.onClickSwatch} style={{ background: Color.toStyle(c) }}></button>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
stripStyle(): React.CSSProperties {
|
||||
return {
|
||||
background: Color.toStyle(this.props.value),
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
height: '4px',
|
||||
right: '0',
|
||||
left: '0'
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const label = this.props.param.label || camelCaseToWords(this.props.name);
|
||||
return <>
|
||||
<div className='msp-control-row'>
|
||||
<span>{label}</span>
|
||||
<div>
|
||||
<button onClick={this.toggleExpanded} className='msp-combined-color-button' style={{ background: Color.toStyle(this.props.value) }}></button>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.isExpanded && <div className='msp-control-offset'>
|
||||
{this.swatch()}
|
||||
<div className='msp-control-row'>
|
||||
<span>RGB</span>
|
||||
<div>
|
||||
<TextInput onChange={this.onChangeText} value={this.props.value}
|
||||
fromValue={formatColorRGB} toValue={getColorFromString} isValid={isValidColorString}
|
||||
className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true}
|
||||
placeholder='e.g. 127 127 127' delayMs={250} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='msp-control-row'>
|
||||
<span>Color List</span>
|
||||
<div>
|
||||
<select value={this.props.value} onChange={this.onChangeSelect}>
|
||||
{ColorValueOption(this.props.value)}
|
||||
{ColorOptions()}
|
||||
</select>
|
||||
<div style={this.stripStyle()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
function formatColorRGB(c: Color) {
|
||||
const [r, g, b] = Color.toRgb(c);
|
||||
return `${r} ${g} ${b}`;
|
||||
}
|
||||
|
||||
function getColorFromString(s: string) {
|
||||
const cs = s.split(/\s+/g);
|
||||
return Color.fromRgb(+cs[0], +cs[1], +cs[2]);
|
||||
}
|
||||
|
||||
function isValidColorString(s: string) {
|
||||
const cs = s.split(/\s+/g);
|
||||
if (cs.length !== 3 && !(cs.length === 4 && cs[3] === '')) return false;
|
||||
for (const c of cs) {
|
||||
if (c === '') continue;
|
||||
const n = +c;
|
||||
if ('' + n !== c) return false;
|
||||
if (n < 0 || n > 255) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// the 1st color is the default value.
|
||||
const SwatchColors = [
|
||||
0x000000, 0x808080, 0xFFFFFF, 0xD33115, 0xE27300, 0xFCC400,
|
||||
0x68BC00, 0x16A5A5, 0x009CE0, 0x7B64FF, 0xFA28FF, 0x7D2187
|
||||
].map(Color);
|
||||
|
||||
let _colors: React.ReactFragment | undefined = void 0;
|
||||
function ColorOptions() {
|
||||
if (_colors) return _colors;
|
||||
_colors = <>{Object.keys(ColorNames).map(name =>
|
||||
<option key={name} value={(ColorNames as { [k: string]: Color })[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color })[name])}` }} >
|
||||
{name}
|
||||
</option>
|
||||
)}</>;
|
||||
return _colors;
|
||||
}
|
||||
|
||||
function ColorValueOption(color: Color) {
|
||||
return !ColorNamesValueMap.has(color) ? <option key={Color.toHexString(color)} value={color} style={{ background: `${Color.toStyle(color)}` }} >
|
||||
{Color.toRgbString(color)}
|
||||
</option> : null
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { PurePluginUIComponent } from '../base';
|
||||
|
||||
export class ControlGroup extends React.Component<{ header: string, initialExpanded?: boolean }, { isExpanded: boolean }> {
|
||||
state = { isExpanded: !!this.props.initialExpanded }
|
||||
@@ -28,6 +29,128 @@ export class ControlGroup extends React.Component<{ header: string, initialExpan
|
||||
}
|
||||
}
|
||||
|
||||
export interface TextInputProps<T> {
|
||||
className?: string,
|
||||
style?: React.CSSProperties,
|
||||
value: T,
|
||||
fromValue?(v: T): string,
|
||||
toValue?(s: string): T,
|
||||
// TODO: add error/help messages here?
|
||||
isValid?(s: string): boolean,
|
||||
onChange(value: T): void,
|
||||
onEnter?(): void,
|
||||
onBlur?(): void,
|
||||
delayMs?: number,
|
||||
blurOnEnter?: boolean,
|
||||
blurOnEscape?: boolean,
|
||||
isDisabled?: boolean,
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
interface TextInputState {
|
||||
originalValue: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
function _id(x: any) { return x; }
|
||||
|
||||
export class TextInput<T = string> extends PurePluginUIComponent<TextInputProps<T>, TextInputState> {
|
||||
private input = React.createRef<HTMLInputElement>();
|
||||
private delayHandle: any = void 0;
|
||||
private pendingValue: T | undefined = void 0;
|
||||
|
||||
state = { originalValue: '', value: '' }
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ value: '' + this.state.originalValue });
|
||||
if (this.props.onBlur) this.props.onBlur();
|
||||
}
|
||||
|
||||
get isPending() { return typeof this.delayHandle !== 'undefined'; }
|
||||
|
||||
clearTimeout() {
|
||||
if (this.isPending) {
|
||||
clearTimeout(this.delayHandle);
|
||||
this.delayHandle = void 0;
|
||||
}
|
||||
}
|
||||
|
||||
raiseOnChange = () => {
|
||||
this.props.onChange(this.pendingValue!);
|
||||
this.pendingValue = void 0;
|
||||
}
|
||||
|
||||
triggerChanged(formatted: string, converted: T) {
|
||||
this.clearTimeout();
|
||||
|
||||
if (formatted === this.state.originalValue) return;
|
||||
|
||||
if (this.props.delayMs) {
|
||||
this.pendingValue = converted;
|
||||
this.delayHandle = setTimeout(this.raiseOnChange, this.props.delayMs);
|
||||
} else {
|
||||
this.props.onChange(converted);
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
|
||||
if (this.props.isValid && !this.props.isValid(value)) {
|
||||
this.clearTimeout();
|
||||
this.setState({ value });
|
||||
return;
|
||||
}
|
||||
|
||||
const converted = (this.props.toValue || _id)(value);
|
||||
const formatted = (this.props.fromValue || _id)(converted);
|
||||
this.setState({ value: formatted }, () => this.triggerChanged(formatted, converted));
|
||||
}
|
||||
|
||||
onKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.charCode === 27 || e.keyCode === 27 /* esc */) {
|
||||
if (this.props.blurOnEscape && this.input.current) {
|
||||
this.input.current.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.keyCode === 13 || e.charCode === 13 /* enter */) {
|
||||
if (this.isPending) {
|
||||
this.clearTimeout();
|
||||
this.raiseOnChange();
|
||||
}
|
||||
if (this.props.blurOnEnter && this.input.current) {
|
||||
this.input.current.blur();
|
||||
}
|
||||
if (this.props.onEnter) this.props.onEnter();
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: TextInputProps<any>, state: TextInputState) {
|
||||
const value = props.fromValue ? props.fromValue(props.value) : props.value;
|
||||
if (value === state.originalValue) return null;
|
||||
return { originalValue: value, value };
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input type='text'
|
||||
className={this.props.className}
|
||||
style={this.props.style}
|
||||
ref={this.input}
|
||||
onBlur={this.onBlur}
|
||||
value={this.state.value}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.onChange}
|
||||
onKeyPress={this.props.onEnter || this.props.blurOnEnter || this.props.blurOnEscape ? this.onKeyPress : void 0}
|
||||
onKeyDown={this.props.blurOnEscape ? this.onKeyUp : void 0}
|
||||
disabled={!!this.props.isDisabled}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace this with parametrized TextInput
|
||||
export class NumericInput extends React.PureComponent<{
|
||||
value: number,
|
||||
onChange: (v: number) => void,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { NumericInput, IconButton, ControlGroup } from './common';
|
||||
import { _Props, _State } from '../base';
|
||||
import { legendFor } from './legend';
|
||||
import { Legend as LegendData } from '../../../mol-util/legend';
|
||||
import { CombinedColorControl } from './color';
|
||||
|
||||
export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
|
||||
params: P,
|
||||
@@ -55,7 +56,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
|
||||
case 'converted': return ConvertedControl;
|
||||
case 'conditioned': return ConditionedControl;
|
||||
case 'multi-select': return MultiSelectControl;
|
||||
case 'color': return ColorControl;
|
||||
case 'color': return CombinedColorControl;
|
||||
case 'color-list': return ColorListControl;
|
||||
case 'vec3': return Vec3Control;
|
||||
case 'file': return FileControl;
|
||||
|
||||
204
src/mol-plugin/ui/image.tsx
Normal file
204
src/mol-plugin/ui/image.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { CollapsableControls, CollapsableState } from './base';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { ParameterControls } from './controls/parameters';
|
||||
import { ImagePass } from '../../mol-canvas3d/passes/image';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { setCanvasSize, canvasToBlob } from '../../mol-canvas3d/util';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
interface ImageControlsState extends CollapsableState {
|
||||
showPreview: boolean
|
||||
|
||||
size: 'canvas' | 'custom'
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const maxWidthUi = 260
|
||||
const maxHeightUi = 180
|
||||
|
||||
export class ImageControls<P, S extends ImageControlsState> extends CollapsableControls<P, S> {
|
||||
private canvasRef = React.createRef<HTMLCanvasElement>()
|
||||
|
||||
private canvas: HTMLCanvasElement
|
||||
private canvasContext: CanvasRenderingContext2D
|
||||
|
||||
private imagePass: ImagePass
|
||||
|
||||
constructor(props: P, context?: any) {
|
||||
super(props, context)
|
||||
|
||||
this.subscribe(this.plugin.events.canvas3d.initialized, () => this.forceUpdate())
|
||||
}
|
||||
|
||||
private getSize() {
|
||||
return this.state.size === 'canvas' ? {
|
||||
width: this.plugin.canvas3d.webgl.gl.drawingBufferWidth,
|
||||
height: this.plugin.canvas3d.webgl.gl.drawingBufferHeight
|
||||
} : {
|
||||
width: this.state.width,
|
||||
height: this.state.height
|
||||
}
|
||||
}
|
||||
|
||||
private preview = () => {
|
||||
const { width, height } = this.getSize()
|
||||
if (width <= 0 || height <= 0) return
|
||||
|
||||
let w: number, h: number
|
||||
const aH = maxHeightUi / height
|
||||
const aW = maxWidthUi / width
|
||||
if (aH < aW) {
|
||||
h = Math.round(Math.min(maxHeightUi, height))
|
||||
w = Math.round(width * (h / height))
|
||||
} else {
|
||||
w = Math.round(Math.min(maxWidthUi, width))
|
||||
h = Math.round(height * (w / width))
|
||||
}
|
||||
setCanvasSize(this.canvas, w, h)
|
||||
const { pixelRatio } = this.plugin.canvas3d.webgl
|
||||
const imageData = this.imagePass.getImageData(w * pixelRatio, h * pixelRatio)
|
||||
this.canvasContext.putImageData(imageData, 0, 0)
|
||||
}
|
||||
|
||||
private downloadTask = () => {
|
||||
return Task.create('Download Image', async ctx => {
|
||||
const { width, height } = this.getSize()
|
||||
if (width <= 0 || height <= 0) return
|
||||
|
||||
await ctx.update('Rendering image...')
|
||||
const imageData = this.imagePass.getImageData(width, height)
|
||||
|
||||
await ctx.update('Encoding image...')
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imageData.width
|
||||
canvas.height = imageData.height
|
||||
const canvasCtx = canvas.getContext('2d')
|
||||
if (!canvasCtx) throw new Error('Could not create canvas 2d context')
|
||||
canvasCtx.putImageData(imageData, 0, 0)
|
||||
|
||||
await ctx.update('Downloading image...')
|
||||
const blob = await canvasToBlob(canvas)
|
||||
download(blob, 'molstar-image')
|
||||
})
|
||||
}
|
||||
|
||||
private download = () => {
|
||||
this.plugin.runTask(this.downloadTask())
|
||||
}
|
||||
|
||||
private syncCanvas() {
|
||||
if (!this.canvasRef.current) return
|
||||
if (this.canvasRef.current === this.canvas) return
|
||||
|
||||
this.canvas = this.canvasRef.current
|
||||
const ctx = this.canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get canvas 2d context')
|
||||
this.canvasContext = ctx
|
||||
}
|
||||
|
||||
private handlePreview() {
|
||||
if (this.state.showPreview) {
|
||||
this.syncCanvas()
|
||||
this.preview()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.handlePreview()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.imagePass = this.plugin.canvas3d.getImagePass()
|
||||
this.imagePass.setProps({
|
||||
multiSample: { mode: 'on', sampleLevel: 2 },
|
||||
postprocessing: this.plugin.canvas3d.props.postprocessing
|
||||
})
|
||||
|
||||
this.handlePreview()
|
||||
|
||||
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => {
|
||||
this.imagePass.setProps({
|
||||
multiSample: { mode: 'on', sampleLevel: 2 },
|
||||
postprocessing: this.plugin.canvas3d.props.postprocessing
|
||||
})
|
||||
this.handlePreview()
|
||||
})
|
||||
|
||||
this.subscribe(this.plugin.canvas3d.didDraw, () => this.handlePreview())
|
||||
}
|
||||
|
||||
private togglePreview = () => this.setState({ showPreview: !this.state.showPreview })
|
||||
|
||||
private setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
|
||||
if (p.name === 'size') {
|
||||
if (p.value.name === 'custom') {
|
||||
this.setState({ size: p.value.name, width: p.value.params.width, height: p.value.params.height })
|
||||
} else {
|
||||
this.setState({ size: p.value.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private get params () {
|
||||
const max = Math.min(this.plugin.canvas3d ? this.plugin.canvas3d.webgl.maxRenderbufferSize : 4096, 8192)
|
||||
const { width, height } = this.defaultState()
|
||||
return {
|
||||
size: PD.MappedStatic('custom', {
|
||||
canvas: PD.Group({}),
|
||||
custom: PD.Group({
|
||||
width: PD.Numeric(width, { min: 1, max, step: 1 }),
|
||||
height: PD.Numeric(height, { min: 1, max, step: 1 }),
|
||||
}, { isFlat: true })
|
||||
}, { options: [['canvas', 'Canvas'], ['custom', 'Custom']] })
|
||||
}
|
||||
}
|
||||
|
||||
private get values () {
|
||||
return this.state.size === 'canvas'
|
||||
? { size: { name: 'canvas', params: {} } }
|
||||
: { size: { name: 'custom', params: { width: this.state.width, height: this.state.height } } }
|
||||
}
|
||||
|
||||
protected defaultState() {
|
||||
return {
|
||||
isCollapsed: false,
|
||||
header: 'Create Image',
|
||||
|
||||
showPreview: false,
|
||||
|
||||
size: 'canvas',
|
||||
width: 1920,
|
||||
height: 1080
|
||||
} as S
|
||||
}
|
||||
|
||||
protected renderControls() {
|
||||
return <div>
|
||||
<div className='msp-control-row'>
|
||||
<button className='msp-btn msp-btn-block' onClick={this.download}>Download</button>
|
||||
</div>
|
||||
<ParameterControls params={this.params} values={this.values} onChange={this.setProps} />
|
||||
<div className='msp-control-group-wrapper'>
|
||||
<div className='msp-control-group-header'>
|
||||
<button className='msp-btn msp-btn-block' onClick={this.togglePreview}>
|
||||
<span className={`msp-icon msp-icon-${this.state.showPreview ? 'collapse' : 'expand'}`} />
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
{this.state.showPreview && <div className='msp-control-offset'>
|
||||
<div className='msp-image-preview'>
|
||||
<canvas width='0px' height='0px' ref={this.canvasRef} />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { LogEntry } from '../../mol-util/log-entry';
|
||||
import * as React from 'react';
|
||||
import { PluginContext } from '../context';
|
||||
import { PluginReactContext, PluginUIComponent } from './base';
|
||||
import { LociLabelControl, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls, StructureToolsWrapper } from './controls';
|
||||
import { LociLabels, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls, StructureToolsWrapper } from './controls';
|
||||
import { StateSnapshots } from './state';
|
||||
import { StateObjectActions } from './state/actions';
|
||||
import { StateTree } from './state/tree';
|
||||
@@ -21,6 +21,8 @@ import { Viewport, ViewportControls } from './viewport';
|
||||
import { StateTransform } from '../../mol-state';
|
||||
import { UpdateTransformControl } from './state/update-transform';
|
||||
import { SequenceView } from './sequence';
|
||||
import { Toasts } from './toast';
|
||||
import { ImageControls } from './image';
|
||||
|
||||
export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
|
||||
region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
|
||||
@@ -128,6 +130,7 @@ export class ControlsWrapper extends PluginUIComponent {
|
||||
{/* <AnimationControlsWrapper /> */}
|
||||
{/* <CameraSnapshots /> */}
|
||||
<StructureToolsWrapper />
|
||||
<ImageControls />
|
||||
<StateSnapshots />
|
||||
</div>;
|
||||
}
|
||||
@@ -146,7 +149,10 @@ export class ViewportWrapper extends PluginUIComponent {
|
||||
<div style={{ position: 'absolute', left: '10px', bottom: '10px' }}>
|
||||
<BackgroundTaskProgress />
|
||||
</div>
|
||||
<LociLabelControl />
|
||||
<div className='msp-highlight-toast-wrapper'>
|
||||
<LociLabels />
|
||||
<Toasts />
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
|
||||
return {
|
||||
structure: PD.Select(structureOptions[0][0], structureOptions, { shortLabel: true }),
|
||||
entity: PD.Select(entityOptions[0][0], entityOptions, { shortLabel: true }),
|
||||
unit: PD.Select(unitOptions[0][0], unitOptions, { shortLabel: true, twoColumns: true }),
|
||||
unit: PD.Select(unitOptions[0][0], unitOptions, { shortLabel: true, twoColumns: true, label: 'Chain' }),
|
||||
operator: PD.Select(operatorOptions[0][0], operatorOptions, { shortLabel: true, twoColumns: true })
|
||||
}
|
||||
}
|
||||
@@ -297,7 +297,7 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
|
||||
</div>
|
||||
|
||||
{typeof sequenceWrapper === 'string'
|
||||
? <div className='msp-sequence-wrapper'>{sequenceWrapper}</div>
|
||||
? <div className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'>{sequenceWrapper}</div>
|
||||
: <Sequence sequenceWrapper={sequenceWrapper} />}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
import { PurePluginUIComponent } from '../base';
|
||||
import { getButtons, getModifiers } from '../../../mol-util/input/input-observer';
|
||||
import { Sequence } from './sequence';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
|
||||
export class Residue extends PurePluginUIComponent<{ seqIdx: number, label: string, parent: Sequence<any>, marker: number, color: Color }> {
|
||||
|
||||
mouseEnter = (e: React.MouseEvent) => {
|
||||
const buttons = getButtons(e.nativeEvent)
|
||||
const modifiers = getModifiers(e.nativeEvent)
|
||||
this.props.parent.hover(this.props.seqIdx, buttons, modifiers);
|
||||
}
|
||||
|
||||
mouseLeave = (e: React.MouseEvent) => {
|
||||
const buttons = getButtons(e.nativeEvent)
|
||||
const modifiers = getModifiers(e.nativeEvent)
|
||||
this.props.parent.hover(undefined, buttons, modifiers);
|
||||
}
|
||||
|
||||
mouseDown = (e: React.MouseEvent) => {
|
||||
const buttons = getButtons(e.nativeEvent)
|
||||
const modifiers = getModifiers(e.nativeEvent)
|
||||
this.props.parent.click(this.props.seqIdx, buttons, modifiers);
|
||||
e.stopPropagation() // so that `parent.mouseDown` is not called
|
||||
}
|
||||
|
||||
get backgroundColor() {
|
||||
// TODO make marker color configurable
|
||||
if (this.props.marker === 0) return ''
|
||||
if (this.props.marker % 2 === 0) return 'rgb(51, 255, 25)' // selected
|
||||
if (this.props.marker === undefined) console.error('unexpected marker value')
|
||||
return 'rgb(255, 102, 153)' // highlighted
|
||||
}
|
||||
|
||||
get margin() {
|
||||
return this.props.label.length > 1
|
||||
? (this.props.seqIdx === 0 ? `0px 2px 0px 0px` : `0px 2px 0px 2px`)
|
||||
: undefined
|
||||
}
|
||||
|
||||
render() {
|
||||
return <span
|
||||
onMouseEnter={this.mouseEnter}
|
||||
onMouseLeave={this.mouseLeave}
|
||||
onMouseDown={this.mouseDown}
|
||||
style={{
|
||||
color: Color.toStyle(this.props.color),
|
||||
backgroundColor: this.backgroundColor,
|
||||
margin: this.margin
|
||||
}}>
|
||||
{this.props.label}
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
@@ -10,48 +10,39 @@ import { PluginUIComponent } from '../base';
|
||||
import { Interactivity } from '../../util/interactivity';
|
||||
import { MarkerAction } from '../../../mol-util/marker-action';
|
||||
import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../../mol-util/input/input-observer';
|
||||
import { ValueBox } from '../../../mol-util';
|
||||
import { Residue } from './residue';
|
||||
import { SequenceWrapper } from './wrapper';
|
||||
import { StructureElement } from '../../../mol-model/structure';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
|
||||
type SequenceProps = { sequenceWrapper: SequenceWrapper.Any }
|
||||
type SequenceState = { markerData: ValueBox<Uint8Array> }
|
||||
|
||||
function getState(markerData: ValueBox<Uint8Array>) {
|
||||
return { markerData: ValueBox.withValue(markerData, markerData.value) }
|
||||
}
|
||||
|
||||
// TODO: this is really inefficient and should be done using a canvas.
|
||||
export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, SequenceState> {
|
||||
state = {
|
||||
markerData: ValueBox.create(this.props.sequenceWrapper.markerArray)
|
||||
}
|
||||
|
||||
private setMarkerData(markerData: ValueBox<Uint8Array>) {
|
||||
this.setState(getState(markerData))
|
||||
}
|
||||
// TODO: this is somewhat inefficient and should be done using a canvas.
|
||||
export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
|
||||
private parentDiv = React.createRef<HTMLDivElement>();
|
||||
private lastMouseOverSeqIdx = -1;
|
||||
private highlightQueue = new Subject<{ seqIdx: number, buttons: number, modifiers: ModifiersKeys }>();
|
||||
|
||||
private lociHighlightProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
|
||||
const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
|
||||
if (changed) this.setMarkerData(this.state.markerData)
|
||||
if (changed) this.updateMarker();
|
||||
}
|
||||
|
||||
private lociSelectionProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
|
||||
const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
|
||||
if (changed) this.setMarkerData(this.state.markerData)
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps: SequenceProps, prevState: SequenceState): SequenceState | null {
|
||||
if (prevState.markerData.value !== nextProps.sequenceWrapper.markerArray) {
|
||||
return getState(ValueBox.create(nextProps.sequenceWrapper.markerArray))
|
||||
}
|
||||
return null
|
||||
if (changed) this.updateMarker();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.plugin.interactivity.lociHighlights.addProvider(this.lociHighlightProvider)
|
||||
this.plugin.interactivity.lociSelects.addProvider(this.lociSelectionProvider)
|
||||
|
||||
this.subscribe(debounceTime<{ seqIdx: number, buttons: number, modifiers: ModifiersKeys }>(15)(this.highlightQueue), (e) => {
|
||||
this.hover(e.seqIdx < 0 ? void 0 : e.seqIdx, e.buttons, e.modifiers);
|
||||
});
|
||||
|
||||
// this.updateMarker()
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -82,31 +73,96 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, Sequ
|
||||
}
|
||||
|
||||
mouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const buttons = getButtons(e.nativeEvent)
|
||||
const modifiers = getModifiers(e.nativeEvent)
|
||||
this.click(undefined, buttons, modifiers);
|
||||
|
||||
let seqIdx: number | undefined = undefined;
|
||||
const el = e.target as HTMLElement;
|
||||
if (el && el.getAttribute) {
|
||||
seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : undefined;
|
||||
}
|
||||
this.click(seqIdx, buttons, modifiers);
|
||||
}
|
||||
|
||||
private getBackgroundColor(marker: number) {
|
||||
// TODO: make marker color configurable
|
||||
if (typeof marker === 'undefined') console.error('unexpected marker value')
|
||||
return marker === 0 ? '' : marker % 2 === 0 ? 'rgb(51, 255, 25)' /* selected */ : 'rgb(255, 102, 153)' /* highlighted */;
|
||||
}
|
||||
|
||||
private residue(seqIdx: number, label: string, marker: number, color: Color) {
|
||||
const margin = label.length > 1 ? (seqIdx === 0 ? `0px 2px 0px 0px` : `0px 2px 0px 2px`) : void 0
|
||||
return <span key={seqIdx} data-seqid={seqIdx} style={{ color: Color.toStyle(color), backgroundColor: this.getBackgroundColor(marker), margin }}>{label}</span>;
|
||||
}
|
||||
|
||||
private updateMarker() {
|
||||
if (!this.parentDiv.current) return;
|
||||
const xs = this.parentDiv.current.children;
|
||||
const { markerArray } = this.props.sequenceWrapper;
|
||||
|
||||
for (let i = 0, _i = markerArray.length; i < _i; i++) {
|
||||
const span = xs[i] as HTMLSpanElement;
|
||||
if (!span) continue;
|
||||
|
||||
const backgroundColor = this.getBackgroundColor(markerArray[i]);
|
||||
if (span.style.backgroundColor !== backgroundColor) span.style.backgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
mouseMove = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const el = e.target as HTMLElement;
|
||||
if (!el || !el.getAttribute) {
|
||||
if (this.lastMouseOverSeqIdx === -1) return;
|
||||
|
||||
this.lastMouseOverSeqIdx = -1;
|
||||
const buttons = getButtons(e.nativeEvent)
|
||||
const modifiers = getModifiers(e.nativeEvent)
|
||||
this.highlightQueue.next({ seqIdx: -1, buttons, modifiers })
|
||||
return;
|
||||
}
|
||||
const seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : -1;
|
||||
if (this.lastMouseOverSeqIdx === seqIdx) {
|
||||
return;
|
||||
} else {
|
||||
const buttons = getButtons(e.nativeEvent)
|
||||
const modifiers = getModifiers(e.nativeEvent)
|
||||
this.lastMouseOverSeqIdx = seqIdx;
|
||||
this.highlightQueue.next({ seqIdx, buttons, modifiers })
|
||||
}
|
||||
}
|
||||
|
||||
mouseLeave = (e: React.MouseEvent) => {
|
||||
if (this.lastMouseOverSeqIdx === -1) return;
|
||||
this.lastMouseOverSeqIdx = -1;
|
||||
const buttons = getButtons(e.nativeEvent)
|
||||
const modifiers = getModifiers(e.nativeEvent)
|
||||
this.highlightQueue.next({ seqIdx: -1, buttons, modifiers })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { markerData } = this.state;
|
||||
const sw = this.props.sequenceWrapper
|
||||
|
||||
const elems: JSX.Element[] = [];
|
||||
for (let i = 0, il = sw.length; i < il; ++i) {
|
||||
elems[elems.length] = <Residue
|
||||
seqIdx={i}
|
||||
label={sw.residueLabel(i)}
|
||||
parent={this}
|
||||
marker={markerData.value[i]}
|
||||
color={sw.residueColor(i)}
|
||||
key={i}
|
||||
/>;
|
||||
elems[elems.length] = this.residue(i, sw.residueLabel(i), sw.markerArray[i], sw.residueColor(i));
|
||||
// TODO: add seq idx markers every N residues? Would need to modify "updateMarker"
|
||||
}
|
||||
|
||||
// calling .updateMarker here is neccesary to ensure existing
|
||||
// residue spans are updated as react won't update them
|
||||
this.updateMarker()
|
||||
|
||||
return <div
|
||||
className='msp-sequence-wrapper'
|
||||
className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'
|
||||
onContextMenu={this.contextMenu}
|
||||
onMouseDown={this.mouseDown}
|
||||
onMouseMove={this.mouseMove}
|
||||
onMouseLeave={this.mouseLeave}
|
||||
ref={this.parentDiv}
|
||||
>
|
||||
{elems}
|
||||
</div>;
|
||||
|
||||
@@ -106,8 +106,8 @@ export class StructureRepresentationControls extends CollapsableControls {
|
||||
const { options } = this.values
|
||||
return {
|
||||
options: PD.Group({
|
||||
showHydrogens: PD.Boolean(options.showHydrogens),
|
||||
visualQuality: PD.Select<VisualQuality>(options.visualQuality, VisualQualityOptions),
|
||||
showHydrogens: PD.Boolean(options.showHydrogens, { description: 'Toggle display of hydrogen atoms in representations' }),
|
||||
visualQuality: PD.Select<VisualQuality>(options.visualQuality, VisualQualityOptions, { description: 'Control the visual/rendering quality of representations' }),
|
||||
}, { isExpanded: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { CollapsableControls } from '../base';
|
||||
import { CollapsableControls, CollapsableState } from '../base';
|
||||
import { StructureSelectionQueries, SelectionModifier } from '../../util/structure-selection-helper';
|
||||
import { ButtonSelect, Options } from '../controls/common';
|
||||
import { PluginCommands } from '../../command';
|
||||
@@ -18,7 +18,13 @@ const StructureSelectionParams = {
|
||||
granularity: Interactivity.Params.granularity,
|
||||
}
|
||||
|
||||
export class StructureSelectionControls extends CollapsableControls {
|
||||
interface StructureSelectionControlsState extends CollapsableState {
|
||||
minRadius: number,
|
||||
extraRadius: number,
|
||||
durationMs: number
|
||||
}
|
||||
|
||||
export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
|
||||
componentDidMount() {
|
||||
this.subscribe(this.plugin.events.interactivity.selectionUpdated, () => {
|
||||
this.forceUpdate()
|
||||
@@ -38,6 +44,14 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
const { extraRadius, minRadius, durationMs } = this.state
|
||||
const { sphere } = this.plugin.helpers.structureSelectionManager.getBoundary();
|
||||
if (sphere.radius === 0) return
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
this.plugin.canvas3d.camera.focus(sphere.center, radius, durationMs);
|
||||
}
|
||||
|
||||
setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
|
||||
if (p.name === 'granularity') {
|
||||
PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { granularity: p.value } });
|
||||
@@ -46,7 +60,7 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
|
||||
get values () {
|
||||
return {
|
||||
granularity: this.plugin.interactivity.props.granularity
|
||||
granularity: this.plugin.interactivity.props.granularity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +76,12 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
isCollapsed: false,
|
||||
header: 'Selection'
|
||||
}
|
||||
header: 'Selection',
|
||||
|
||||
minRadius: 8,
|
||||
extraRadius: 4,
|
||||
durationMs: 250
|
||||
} as S
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
@@ -73,7 +91,10 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
|
||||
return <div>
|
||||
<div className='msp-control-row msp-row-text'>
|
||||
<div>{this.stats}</div>
|
||||
<button className='msp-btn msp-btn-block' onClick={this.focus}>
|
||||
<span className={`msp-icon msp-icon-focus-on-visual`} style={{ position: 'absolute', left: '10px' }} />
|
||||
{this.stats}
|
||||
</button>
|
||||
</div>
|
||||
<ParameterControls params={StructureSelectionParams} values={this.values} onChange={this.setProps} />
|
||||
<div className='msp-control-row'>
|
||||
|
||||
59
src/mol-plugin/ui/toast.tsx
Normal file
59
src/mol-plugin/ui/toast.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Adapted from LiteMol (c) David Sehnal
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { PluginUIComponent } from './base';
|
||||
import { PluginToastManager } from '../state/toast';
|
||||
import { IconButton } from './controls/common';
|
||||
|
||||
class ToastEntry extends PluginUIComponent<{ entry: PluginToastManager.Entry }> {
|
||||
private hide = () => {
|
||||
let entry = this.props.entry;
|
||||
(entry.hide || function () { }).call(null);
|
||||
};
|
||||
|
||||
render() {
|
||||
let entry = this.props.entry;
|
||||
let message = typeof entry.message === 'string'
|
||||
? <div dangerouslySetInnerHTML={{ __html: entry.message }} />
|
||||
: <div><entry.message /></div>;
|
||||
|
||||
return <div className='msp-toast-entry'>
|
||||
<div className='msp-toast-title' onClick={() => this.hide()}>
|
||||
{entry.title}
|
||||
</div>
|
||||
<div className='msp-toast-message'>
|
||||
{message}
|
||||
</div>
|
||||
<div className='msp-toast-clear'></div>
|
||||
<div className='msp-toast-hide'>
|
||||
<IconButton onClick={this.hide} icon='abort' title='Hide' />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export class Toasts extends PluginUIComponent {
|
||||
componentDidMount() {
|
||||
this.subscribe(this.plugin.toasts.events.changed, () => this.forceUpdate());
|
||||
}
|
||||
|
||||
render() {
|
||||
const state = this.plugin.toasts.state;
|
||||
|
||||
if (!state.entries.count()) return null;
|
||||
|
||||
const entries: PluginToastManager.Entry[] = [];
|
||||
state.entries.forEach((t, k) => entries.push(t!));
|
||||
entries.sort(function (x, y) { return x.serialNumber - y.serialNumber; })
|
||||
|
||||
return <div className='msp-toast-container'>
|
||||
{entries.map(e => <ToastEntry key={e.serialNumber} entry={e} />)}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -76,10 +76,11 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
|
||||
render() {
|
||||
return <div className={'msp-viewport-controls'} onMouseMove={this.onMouseMove}>
|
||||
<div className='msp-viewport-controls-buttons'>
|
||||
{this.icon('reset-scene', this.resetCamera, 'Reset Camera')}<br/>
|
||||
{this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}<br/>
|
||||
{this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}<br />
|
||||
{!this.props.hideSettingsIcon && this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}<br/>
|
||||
<div className='msp-semi-transparent-background' />
|
||||
{this.icon('reset-scene', this.resetCamera, 'Reset Camera')}
|
||||
{this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}
|
||||
{this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}
|
||||
{!this.props.hideSettingsIcon && this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}
|
||||
</div>
|
||||
{this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'>
|
||||
<ControlGroup header='Layout' initialExpanded={true}>
|
||||
@@ -96,8 +97,19 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
|
||||
}
|
||||
}
|
||||
|
||||
export const Logo = () =>
|
||||
<div className='msp-logo'>
|
||||
<div>
|
||||
<div>
|
||||
<div />
|
||||
<div className='msp-logo-image' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
interface ViewportState {
|
||||
noWebGl: boolean
|
||||
showLogo: boolean
|
||||
}
|
||||
|
||||
export class Viewport extends PluginUIComponent<{ }, ViewportState> {
|
||||
@@ -105,9 +117,14 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
|
||||
private canvas = React.createRef<HTMLCanvasElement>();
|
||||
|
||||
state: ViewportState = {
|
||||
noWebGl: false
|
||||
noWebGl: false,
|
||||
showLogo: true
|
||||
};
|
||||
|
||||
private handleLogo = () => {
|
||||
this.setState({ showLogo: this.plugin.canvas3d.reprCount.value === 0 })
|
||||
}
|
||||
|
||||
private handleResize = () => {
|
||||
const container = this.container.current;
|
||||
const canvas = this.canvas.current;
|
||||
@@ -121,9 +138,11 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
|
||||
if (!this.canvas.current || !this.container.current || !this.plugin.initViewer(this.canvas.current!, this.container.current!)) {
|
||||
this.setState({ noWebGl: true });
|
||||
}
|
||||
this.handleLogo();
|
||||
this.handleResize();
|
||||
|
||||
const canvas3d = this.plugin.canvas3d;
|
||||
this.subscribe(canvas3d.reprCount, this.handleLogo);
|
||||
this.subscribe(canvas3d.input.resize, this.handleResize);
|
||||
|
||||
this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.interaction.click.next(e));
|
||||
@@ -155,6 +174,7 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
|
||||
<div className='msp-viewport-host3d' ref={this.container}>
|
||||
<canvas ref={this.canvas} />
|
||||
</div>
|
||||
{this.state.showLogo && <Logo />}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
91
src/mol-plugin/util/model-unitcell.ts
Normal file
91
src/mol-plugin/util/model-unitcell.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Model, ModelSymmetry } from '../../mol-model/structure';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { ModelUnitcell3D } from '../state/transforms/representation';
|
||||
import { ShapeRepresentation } from '../../mol-repr/shape/representation';
|
||||
import { Text } from '../../mol-geo/geometry/text/text';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { BoxCage } from '../../mol-geo/primitive/box';
|
||||
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { transformCage, copyCage } from '../../mol-geo/primitive/cage';
|
||||
import { radToDeg } from '../../mol-math/misc';
|
||||
|
||||
const translate05 = Mat4.fromTranslation(Mat4(), Vec3.create(0.5, 0.5, 0.5))
|
||||
const unitCage = transformCage(copyCage(BoxCage()), translate05)
|
||||
|
||||
const tmpRef = Vec3()
|
||||
const tmpTranslate = Mat4()
|
||||
|
||||
interface UnitcellData {
|
||||
symmetry: ModelSymmetry
|
||||
ref: Vec3
|
||||
}
|
||||
|
||||
export const UnitcellParams = {
|
||||
...Mesh.Params,
|
||||
cellColor: PD.Color(ColorNames.orange),
|
||||
cellScale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 })
|
||||
}
|
||||
export type UnitcellParams = typeof UnitcellParams
|
||||
export type UnitcellProps = PD.Values<UnitcellParams>
|
||||
|
||||
function getUnitcellMesh(data: UnitcellData, props: UnitcellProps, mesh?: Mesh) {
|
||||
const state = MeshBuilder.createState(256, 128, mesh)
|
||||
|
||||
const { fromFractional } = data.symmetry.spacegroup.cell
|
||||
|
||||
Vec3.floor(tmpRef, data.ref)
|
||||
Mat4.fromTranslation(tmpTranslate, tmpRef)
|
||||
const cellCage = transformCage(copyCage(unitCage), tmpTranslate)
|
||||
|
||||
const radius = (Math.cbrt(data.symmetry.spacegroup.cell.volume) / 300) * props.cellScale
|
||||
state.currentGroup = 1
|
||||
MeshBuilder.addCage(state, fromFractional, cellCage, radius, 2, 20)
|
||||
|
||||
return MeshBuilder.getMesh(state)
|
||||
}
|
||||
|
||||
export async function getUnitcellRepresentation(ctx: RuntimeContext, model: Model, params: StateTransformer.Params<ModelUnitcell3D>, prev?: ShapeRepresentation<UnitcellData, Text, Text.Params>) {
|
||||
const repr = prev || ShapeRepresentation(getUnitcellShape, Mesh.Utils);
|
||||
const data = {
|
||||
symmetry: model.symmetry,
|
||||
ref: Vec3.transformMat4(Vec3(), Model.getCenter(model), model.symmetry.spacegroup.cell.toFractional)
|
||||
}
|
||||
await repr.createOrUpdate(params, data).runInContext(ctx);
|
||||
return repr;
|
||||
}
|
||||
|
||||
function getUnitcellLabel(data: UnitcellData) {
|
||||
const { cell, name } = data.symmetry.spacegroup
|
||||
const { index, size, anglesInRadians } = cell
|
||||
const a = size[0].toFixed(2)
|
||||
const b = size[1].toFixed(2)
|
||||
const c = size[2].toFixed(2)
|
||||
const alpha = radToDeg(anglesInRadians[0]).toFixed(2)
|
||||
const beta = radToDeg(anglesInRadians[1]).toFixed(2)
|
||||
const gamma = radToDeg(anglesInRadians[2]).toFixed(2)
|
||||
const label: string[] = []
|
||||
// name
|
||||
label.push(`${name} #${index + 1}`)
|
||||
// sizes
|
||||
label.push(`${a}\u00D7${b}\u00D7${c} \u212B`)
|
||||
// angles
|
||||
label.push(`\u03b1=${alpha}\u00B0 \u03b2=${beta}\u00B0 \u03b3=${gamma}\u00B0`)
|
||||
return label.join(' | ')
|
||||
}
|
||||
|
||||
function getUnitcellShape(ctx: RuntimeContext, data: UnitcellData, props: UnitcellProps, shape?: Shape<Mesh>) {
|
||||
const geo = getUnitcellMesh(data, props, shape && shape.geometry);
|
||||
const label = getUnitcellLabel(data)
|
||||
return Shape.create('Unitcell', data, geo, () => props.cellColor, () => 1, () => label)
|
||||
}
|
||||
@@ -12,6 +12,11 @@ import { StateObject } from '../../mol-state';
|
||||
import { PluginContext } from '../context';
|
||||
import { PluginStateObject } from '../state/objects';
|
||||
import { structureElementStatsLabel } from '../../mol-theme/label';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
import { Boundary } from '../../mol-model/structure/structure/util/boundary';
|
||||
|
||||
const boundaryHelper = new BoundaryHelper();
|
||||
|
||||
export { StructureElementSelectionManager };
|
||||
class StructureElementSelectionManager {
|
||||
@@ -30,6 +35,37 @@ class StructureElementSelectionManager {
|
||||
return this.entries.get(ref)!;
|
||||
}
|
||||
|
||||
getBoundary() {
|
||||
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)
|
||||
|
||||
boundaryHelper.reset(0);
|
||||
|
||||
const boundaries: Boundary[] = []
|
||||
this.entries.forEach(v => {
|
||||
const loci = v.selection
|
||||
if (!StructureElement.Loci.isEmpty(loci)) {
|
||||
boundaries.push(StructureElement.Loci.getBoundary(loci))
|
||||
}
|
||||
})
|
||||
|
||||
for (let i = 0, il = boundaries.length; i < il; ++i) {
|
||||
const { box, sphere } = boundaries[i];
|
||||
Vec3.min(min, min, box.min);
|
||||
Vec3.max(max, max, box.max);
|
||||
boundaryHelper.boundaryStep(sphere.center, sphere.radius)
|
||||
}
|
||||
|
||||
boundaryHelper.finishBoundaryStep();
|
||||
|
||||
for (let i = 0, il = boundaries.length; i < il; ++i) {
|
||||
const { sphere } = boundaries[i];
|
||||
boundaryHelper.extendStep(sphere.center, sphere.radius);
|
||||
}
|
||||
|
||||
return { box: { min, max }, sphere: boundaryHelper.getSphere() };
|
||||
}
|
||||
|
||||
get stats() {
|
||||
let structureCount = 0
|
||||
let elementCount = 0
|
||||
|
||||
@@ -178,14 +178,14 @@ export class StructureRepresentationHelper {
|
||||
const s = structures[0].obj!.data
|
||||
|
||||
if (s.elementCount < 50000) {
|
||||
polymerAndLigand(this)
|
||||
await polymerAndLigand(this)
|
||||
} else if (s.elementCount < 200000) {
|
||||
proteinAndNucleic(this)
|
||||
await proteinAndNucleic(this)
|
||||
} else {
|
||||
if (s.unitSymmetryGroups[0].units.length > 10) {
|
||||
capsid(this)
|
||||
await capsid(this)
|
||||
} else {
|
||||
coarseCapsid(this)
|
||||
await coarseCapsid(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +114,11 @@ const branchedConnectedOnly = MS.struct.modifier.union([
|
||||
const ligand = MS.struct.modifier.union([
|
||||
MS.struct.generator.atomGroups({
|
||||
'entity-test': MS.core.logic.and([
|
||||
MS.core.rel.neq([MS.ammp('entityType'), 'branched']),
|
||||
MS.core.rel.eq([MS.ammp('entityType'), 'non-polymer'])
|
||||
MS.core.rel.eq([MS.ammp('entityType'), 'non-polymer']),
|
||||
MS.core.logic.not([MS.core.str.match([
|
||||
MS.re('oligosaccharide', 'i'),
|
||||
MS.ammp('entitySubtype')
|
||||
])])
|
||||
]),
|
||||
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
|
||||
'residue-test': MS.core.logic.not([
|
||||
@@ -124,9 +127,17 @@ const ligand = MS.struct.modifier.union([
|
||||
})
|
||||
])
|
||||
|
||||
// don't innclude branched entities as they have their own link representation
|
||||
const ligandPlusConnected = MS.struct.modifier.union([
|
||||
MS.struct.modifier.includeConnected({
|
||||
0: ligand, 'layer-count': 1, 'as-whole-residues': true
|
||||
MS.struct.modifier.exceptBy({
|
||||
0: MS.struct.modifier.union([
|
||||
MS.struct.modifier.includeConnected({
|
||||
0: ligand,
|
||||
'layer-count': 1,
|
||||
'as-whole-residues': true
|
||||
})
|
||||
]),
|
||||
by: branched
|
||||
})
|
||||
])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { StructureParams, StructureMeshParams, StructureDirectVolumeParams } from './representation';
|
||||
import { Visual, VisualContext } from '../visual';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { Structure, StructureElement } from '../../mol-model/structure';
|
||||
import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
|
||||
import { LocationIterator } from '../../mol-geo/util/location-iterator';
|
||||
import { Theme, createEmptyTheme } from '../../mol-theme/theme';
|
||||
@@ -164,8 +164,17 @@ export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geom
|
||||
if (newGeometry) geometry = newGeometry
|
||||
}
|
||||
|
||||
function lociIsSuperset(loci: Loci) {
|
||||
if (isEveryLoci(loci)) return true
|
||||
if (Structure.isLoci(loci) && Structure.areRootsEquivalent(loci.structure, currentStructure)) return true
|
||||
if (StructureElement.Loci.is(loci) && Structure.areRootsEquivalent(loci.structure, currentStructure)) {
|
||||
if (StructureElement.Loci.isWholeStructure(loci)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
|
||||
if (isEveryLoci(loci) || (Structure.isLoci(loci) && Structure.areRootsEquivalent(loci.structure, currentStructure))) {
|
||||
if (lociIsSuperset(loci)) {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount))
|
||||
} else {
|
||||
return eachLocation(loci, currentStructure, apply)
|
||||
|
||||
@@ -29,7 +29,7 @@ export const BallAndStickParams = {
|
||||
...IntraUnitLinkParams,
|
||||
...InterUnitLinkParams,
|
||||
unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions),
|
||||
sizeFactor: PD.Numeric(0.3, { min: 0.01, max: 10, step: 0.01 }),
|
||||
sizeFactor: PD.Numeric(0.2, { min: 0.01, max: 10, step: 0.01 }),
|
||||
sizeAspectRatio: PD.Numeric(2/3, { min: 0.01, max: 3, step: 0.01 }),
|
||||
visuals: PD.MultiSelect<BallAndStickVisualName>(['element-sphere', 'intra-link', 'inter-link'], BallAndStickVisualOptions),
|
||||
}
|
||||
@@ -50,6 +50,6 @@ export const BallAndStickRepresentationProvider: StructureRepresentationProvider
|
||||
getParams: getBallAndStickParams,
|
||||
defaultValues: PD.getDefaultValues(BallAndStickParams),
|
||||
defaultColorTheme: 'element-symbol',
|
||||
defaultSizeTheme: 'uniform',
|
||||
defaultSizeTheme: 'physical',
|
||||
isApplicable: (structure: Structure) => structure.elementCount > 0
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Structure, Unit } from '../../mol-model/structure';
|
||||
import { Structure, Unit, StructureElement } from '../../mol-model/structure';
|
||||
import { RepresentationProps } from '../representation';
|
||||
import { Visual, VisualContext } from '../visual';
|
||||
import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
|
||||
@@ -214,8 +214,17 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
|
||||
: createEmptyGeometry(geometry)
|
||||
}
|
||||
|
||||
function lociIsSuperset(loci: Loci) {
|
||||
if (isEveryLoci(loci)) return true
|
||||
if (Structure.isLoci(loci) && Structure.areRootsEquivalent(loci.structure, currentStructureGroup.structure)) return true
|
||||
if (StructureElement.Loci.is(loci) && Structure.areRootsEquivalent(loci.structure, currentStructureGroup.structure)) {
|
||||
if (StructureElement.Loci.isWholeStructure(loci)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
|
||||
if (isEveryLoci(loci) || (Structure.isLoci(loci) && Structure.areRootsEquivalent(loci.structure, currentStructureGroup.structure))) {
|
||||
if (lociIsSuperset(loci)) {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount))
|
||||
} else {
|
||||
return eachLocation(loci, currentStructureGroup, apply)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -9,11 +9,11 @@ import { Box, PerforatedBox } from '../../../mol-geo/primitive/box';
|
||||
import { OctagonalPyramid, PerforatedOctagonalPyramid } from '../../../mol-geo/primitive/pyramid';
|
||||
import { Star } from '../../../mol-geo/primitive/star';
|
||||
import { Octahedron, PerforatedOctahedron } from '../../../mol-geo/primitive/octahedron';
|
||||
import { DiamondPrism, PentagonalPrism, HexagonalPrism } from '../../../mol-geo/primitive/prism';
|
||||
import { DiamondPrism, PentagonalPrism, ShiftedHexagonalPrism, HexagonalPrism, HeptagonalPrism } from '../../../mol-geo/primitive/prism';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { getSaccharideShape, SaccharideShapes } from '../../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { getSaccharideShape, SaccharideShape } from '../../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { ComplexMeshParams, ComplexMeshVisual } from '../complex-visual';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -43,6 +43,8 @@ const perforatedOctahedron = PerforatedOctahedron()
|
||||
const diamondPrism = DiamondPrism()
|
||||
const pentagonalPrism = PentagonalPrism()
|
||||
const hexagonalPrism = HexagonalPrism()
|
||||
const shiftedHexagonalPrism = ShiftedHexagonalPrism()
|
||||
const heptagonalPrism = HeptagonalPrism()
|
||||
|
||||
function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateSymbolParams>, mesh?: Mesh) {
|
||||
const builderState = MeshBuilder.createState(256, 128, mesh)
|
||||
@@ -55,7 +57,7 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
|
||||
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const c = carbohydrates.elements[i];
|
||||
const shapeType = getSaccharideShape(c.component.type)
|
||||
const shapeType = getSaccharideShape(c.component.type, c.ringMemberCount)
|
||||
|
||||
l.unit = c.unit
|
||||
l.element = c.unit.elements[c.anomericCarbon]
|
||||
@@ -71,47 +73,47 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
|
||||
builderState.currentGroup = i * 2
|
||||
|
||||
switch (shapeType) {
|
||||
case SaccharideShapes.FilledSphere:
|
||||
case SaccharideShape.FilledSphere:
|
||||
addSphere(builderState, center, radius, detail)
|
||||
break;
|
||||
case SaccharideShapes.FilledCube:
|
||||
case SaccharideShape.FilledCube:
|
||||
Mat4.scaleUniformly(t, t, side)
|
||||
MeshBuilder.addPrimitive(builderState, t, box)
|
||||
break;
|
||||
case SaccharideShapes.CrossedCube:
|
||||
case SaccharideShape.CrossedCube:
|
||||
Mat4.scaleUniformly(t, t, side)
|
||||
MeshBuilder.addPrimitive(builderState, t, perforatedBox)
|
||||
Mat4.mul(t, t, Mat4.rotZ90X180)
|
||||
builderState.currentGroup += 1
|
||||
MeshBuilder.addPrimitive(builderState, t, perforatedBox)
|
||||
break;
|
||||
case SaccharideShapes.FilledCone:
|
||||
case SaccharideShape.FilledCone:
|
||||
Mat4.scaleUniformly(t, t, side * 1.2)
|
||||
MeshBuilder.addPrimitive(builderState, t, octagonalPyramid)
|
||||
break
|
||||
case SaccharideShapes.DevidedCone:
|
||||
case SaccharideShape.DevidedCone:
|
||||
Mat4.scaleUniformly(t, t, side * 1.2)
|
||||
MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid)
|
||||
Mat4.mul(t, t, Mat4.rotZ90)
|
||||
builderState.currentGroup += 1
|
||||
MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid)
|
||||
break
|
||||
case SaccharideShapes.FlatBox:
|
||||
case SaccharideShape.FlatBox:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
|
||||
MeshBuilder.addPrimitive(builderState, t, box)
|
||||
break
|
||||
case SaccharideShapes.FilledStar:
|
||||
case SaccharideShape.FilledStar:
|
||||
Mat4.scaleUniformly(t, t, side)
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
MeshBuilder.addPrimitive(builderState, t, star)
|
||||
break
|
||||
case SaccharideShapes.FilledDiamond:
|
||||
case SaccharideShape.FilledDiamond:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
|
||||
MeshBuilder.addPrimitive(builderState, t, octahedron)
|
||||
break
|
||||
case SaccharideShapes.DividedDiamond:
|
||||
case SaccharideShape.DividedDiamond:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
|
||||
MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron)
|
||||
@@ -119,21 +121,37 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
|
||||
builderState.currentGroup += 1
|
||||
MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron)
|
||||
break
|
||||
case SaccharideShapes.FlatDiamond:
|
||||
case SaccharideShape.FlatDiamond:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side, side / 2, side / 2))
|
||||
MeshBuilder.addPrimitive(builderState, t, diamondPrism)
|
||||
break
|
||||
case SaccharideShapes.Pentagon:
|
||||
case SaccharideShape.DiamondPrism:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
|
||||
MeshBuilder.addPrimitive(builderState, t, diamondPrism)
|
||||
break
|
||||
case SaccharideShape.PentagonalPrism:
|
||||
case SaccharideShape.Pentagon:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
|
||||
MeshBuilder.addPrimitive(builderState, t, pentagonalPrism)
|
||||
break
|
||||
case SaccharideShapes.FlatHexagon:
|
||||
case SaccharideShape.HexagonalPrism:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
|
||||
MeshBuilder.addPrimitive(builderState, t, hexagonalPrism)
|
||||
break
|
||||
case SaccharideShape.HeptagonalPrism:
|
||||
Mat4.mul(t, t, Mat4.rotZY90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
|
||||
MeshBuilder.addPrimitive(builderState, t, heptagonalPrism)
|
||||
break
|
||||
case SaccharideShape.FlatHexagon:
|
||||
default:
|
||||
Mat4.mul(t, t, Mat4.rotZYZ90)
|
||||
Mat4.scale(t, t, Vec3.set(sVec, side / 1.5, side , side / 2))
|
||||
MeshBuilder.addPrimitive(builderState, t, hexagonalPrism)
|
||||
MeshBuilder.addPrimitive(builderState, t, shiftedHexagonalPrism)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,6 @@ function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: num
|
||||
return EmptyLoci
|
||||
}
|
||||
|
||||
// TODO for each link when both of the link elements are in a StructureElement.Loci
|
||||
function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
|
||||
const { getTerminalLinkIndex } = structure.carbohydrates
|
||||
let changed = false
|
||||
@@ -161,6 +160,11 @@ function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: In
|
||||
for (let i = 0, il = indices.length; i < il; ++i) {
|
||||
if (apply(Interval.ofSingleton(indices[i]))) changed = true
|
||||
}
|
||||
} else {
|
||||
const indices = getTerminalLinkIndices(e.unit, e.unit.elements[v])
|
||||
for (let i = 0, il = indices.length; i < il; ++i) {
|
||||
if (apply(Interval.ofSingleton(indices[i]))) changed = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,7 +43,11 @@ function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: Structur
|
||||
const b = bonds[edgeIndex]
|
||||
location.unit = b.unitA
|
||||
location.element = b.unitA.elements[b.indexA]
|
||||
return theme.size.size(location) * sizeFactor * sizeAspectRatio
|
||||
const sizeA = theme.size.size(location)
|
||||
location.unit = b.unitB
|
||||
location.element = b.unitB.elements[b.indexB]
|
||||
const sizeB = theme.size.size(location)
|
||||
return Math.min(sizeA, sizeB) * sizeFactor * sizeAspectRatio
|
||||
},
|
||||
ignore: ignoreHydrogens ? (edgeIndex: number) => {
|
||||
const b = bonds[edgeIndex]
|
||||
|
||||
@@ -63,7 +63,10 @@ function createIntraUnitLinkCylinderMesh(ctx: VisualContext, unit: Unit, structu
|
||||
flags: (edgeIndex: number) => BitFlags.create(_flags[edgeIndex]),
|
||||
radius: (edgeIndex: number) => {
|
||||
location.element = elements[a[edgeIndex]]
|
||||
return theme.size.size(location) * sizeFactor * sizeAspectRatio
|
||||
const sizeA = theme.size.size(location)
|
||||
location.element = elements[b[edgeIndex]]
|
||||
const sizeB = theme.size.size(location)
|
||||
return Math.min(sizeA, sizeB) * sizeFactor * sizeAspectRatio
|
||||
},
|
||||
ignore: ignoreHydrogens ? (edgeIndex: number) => {
|
||||
return isHydrogen(unit, elements[a[edgeIndex]]) || isHydrogen(unit, elements[b[edgeIndex]])
|
||||
|
||||
@@ -81,10 +81,10 @@ namespace Visual {
|
||||
|
||||
const { tOverpaint, uGroupCount, instanceCount } = renderObject.values
|
||||
const count = uGroupCount.ref.value * instanceCount.ref.value
|
||||
const { array } = tOverpaint.ref.value
|
||||
|
||||
// ensure texture has right size
|
||||
createOverpaint(overpaint.layers.length ? count : 0, renderObject.values)
|
||||
const { array } = tOverpaint.ref.value
|
||||
|
||||
// clear all if requested
|
||||
if (clear) clearOverpaint(array, 0, count)
|
||||
@@ -108,12 +108,12 @@ namespace Visual {
|
||||
|
||||
const { tTransparency, uGroupCount, instanceCount } = renderObject.values
|
||||
const count = uGroupCount.ref.value * instanceCount.ref.value
|
||||
const { array } = tTransparency.ref.value
|
||||
|
||||
const { loci, value, variant } = transparency
|
||||
|
||||
// ensure texture has right size and variant
|
||||
createTransparency(value && !isEmptyLoci(loci) ? count : 0, variant, renderObject.values)
|
||||
const { array } = tTransparency.ref.value
|
||||
|
||||
// clear if requested
|
||||
if (clear) clearTransparency(array, 0, count)
|
||||
|
||||
@@ -31,6 +31,8 @@ import { IllustrativeColorThemeProvider } from './color/illustrative';
|
||||
import { HydrophobicityColorThemeProvider } from './color/hydrophobicity';
|
||||
import { ModelIndexColorThemeProvider } from './color/model-index';
|
||||
import { OccupancyColorThemeProvider } from './color/occupancy';
|
||||
import { OperatorNameColorThemeProvider } from './color/operator-name';
|
||||
import { OperatorHklColorThemeProvider } from './color/operator-hkl';
|
||||
|
||||
export type LocationColor = (location: Location, isSecondary: boolean) => Color
|
||||
|
||||
@@ -83,6 +85,8 @@ export const BuiltInColorThemes = {
|
||||
'model-index': ModelIndexColorThemeProvider,
|
||||
'molecule-type': MoleculeTypeColorThemeProvider,
|
||||
'occupancy': OccupancyColorThemeProvider,
|
||||
'operator-hkl': OperatorHklColorThemeProvider,
|
||||
'operator-name': OperatorNameColorThemeProvider,
|
||||
'polymer-id': PolymerIdColorThemeProvider,
|
||||
'polymer-index': PolymerIndexColorThemeProvider,
|
||||
'residue-name': ResidueNameColorThemeProvider,
|
||||
|
||||
@@ -26,7 +26,10 @@ export function getChainIdColorThemeParams(ctx: ThemeDataContext) {
|
||||
if (ctx.structure) {
|
||||
if (getAsymIdSerialMap(ctx.structure.root).size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = { list: 'red-yellow-blue' }
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -79,6 +82,9 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
|
||||
const l = StructureElement.Location.create()
|
||||
const asymIdSerialMap = getAsymIdSerialMap(ctx.structure.root)
|
||||
|
||||
const labelTable = Array.from(asymIdSerialMap.keys())
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(asymIdSerialMap.size, props)
|
||||
legend = palette.legend
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ export function getEntitySourceColorThemeParams(ctx: ThemeDataContext) {
|
||||
if (ctx.structure) {
|
||||
if (getMaps(ctx.structure.root.models).srcKeySerialMap.size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = { list: 'red-yellow-blue' }
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -108,6 +111,13 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E
|
||||
const { models } = ctx.structure.root
|
||||
const { seqToSrcByModelEntity, srcKeySerialMap } = getMaps(models)
|
||||
|
||||
const labelTable = Array.from(srcKeySerialMap.keys()).map(v => {
|
||||
const l = v.split('|')[2]
|
||||
return l === '1' ? 'Unnamed' : l
|
||||
})
|
||||
labelTable.push('Unknown')
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(srcKeySerialMap.size + 1, props)
|
||||
legend = palette.legend
|
||||
|
||||
|
||||
124
src/mol-theme/color/operator-hkl.ts
Normal file
124
src/mol-theme/color/operator-hkl.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { StructureElement, Link, Structure } from '../../mol-model/structure';
|
||||
import { Location } from '../../mol-model/location';
|
||||
import { ColorTheme, LocationColor } from '../color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition'
|
||||
import { ThemeDataContext } from '../theme';
|
||||
import { getPaletteParams, getPalette } from '../../mol-util/color/palette';
|
||||
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { integerDigitCount } from '../../mol-util/number';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC)
|
||||
const Description = `Assigns a color based on the operator HKL value of a transformed chain.`
|
||||
|
||||
export const OperatorHklColorThemeParams = {
|
||||
...getPaletteParams({ type: 'set', setList: 'set-3' }),
|
||||
}
|
||||
export type OperatorHklColorThemeParams = typeof OperatorHklColorThemeParams
|
||||
export function getOperatorHklColorThemeParams(ctx: ThemeDataContext) {
|
||||
const params = PD.clone(OperatorHklColorThemeParams)
|
||||
if (ctx.structure) {
|
||||
if (getOperatorHklSerialMap(ctx.structure.root).map.size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const hklOffset = 10000
|
||||
|
||||
function hklKey(hkl: Vec3) {
|
||||
return hkl.map(v => `${v + hklOffset}`.padStart(5, '0')).join('')
|
||||
}
|
||||
|
||||
function hklKeySplit(key: string) {
|
||||
const len = integerDigitCount(hklOffset, 0)
|
||||
const h = parseInt(key.substr(0, len))
|
||||
const k = parseInt(key.substr(len, len))
|
||||
const l = parseInt(key.substr(len + len, len))
|
||||
return [ h - hklOffset, k - hklOffset, l - hklOffset ] as Vec3
|
||||
}
|
||||
|
||||
function formatHkl(hkl: Vec3) {
|
||||
return hkl.map(v => v + 5).join('')
|
||||
}
|
||||
|
||||
function getOperatorHklSerialMap(structure: Structure) {
|
||||
const map = new Map<string, number>()
|
||||
const set = new Set<string>()
|
||||
for (let i = 0, il = structure.units.length; i < il; ++i) {
|
||||
const k = hklKey(structure.units[i].conformation.operator.hkl)
|
||||
set.add(k)
|
||||
}
|
||||
const arr = Array.from(set).sort()
|
||||
arr.forEach(k => map.set(k, map.size))
|
||||
const min = hklKeySplit(arr[0])
|
||||
const max = hklKeySplit(arr[arr.length - 1])
|
||||
return { min, max, map }
|
||||
}
|
||||
|
||||
export function OperatorHklColorTheme(ctx: ThemeDataContext, props: PD.Values<OperatorHklColorThemeParams>): ColorTheme<OperatorHklColorThemeParams> {
|
||||
let color: LocationColor
|
||||
let legend: ScaleLegend | TableLegend | undefined
|
||||
|
||||
if (ctx.structure) {
|
||||
const { min, max, map } = getOperatorHklSerialMap(ctx.structure.root)
|
||||
|
||||
const labelTable: string[] = []
|
||||
map.forEach((v, k) => {
|
||||
const i = v % map.size
|
||||
const label = formatHkl(hklKeySplit(k))
|
||||
if (labelTable[i] === undefined) labelTable[i] = label
|
||||
else labelTable[i] += `, ${label}`
|
||||
})
|
||||
|
||||
props.palette.params.minLabel = formatHkl(min)
|
||||
props.palette.params.maxLabel = formatHkl(max)
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(map.size, props)
|
||||
legend = palette.legend
|
||||
|
||||
color = (location: Location): Color => {
|
||||
let serial: number | undefined = undefined
|
||||
if (StructureElement.Location.is(location)) {
|
||||
const k = hklKey(location.unit.conformation.operator.hkl)
|
||||
serial = map.get(k)
|
||||
} else if (Link.isLocation(location)) {
|
||||
const k = hklKey(location.aUnit.conformation.operator.hkl)
|
||||
serial = map.get(k)
|
||||
}
|
||||
return serial === undefined ? DefaultColor : palette.color(serial)
|
||||
}
|
||||
} else {
|
||||
color = () => DefaultColor
|
||||
}
|
||||
|
||||
return {
|
||||
factory: OperatorHklColorTheme,
|
||||
granularity: 'instance',
|
||||
color,
|
||||
props,
|
||||
description: Description,
|
||||
legend
|
||||
}
|
||||
}
|
||||
|
||||
export const OperatorHklColorThemeProvider: ColorTheme.Provider<OperatorHklColorThemeParams> = {
|
||||
label: 'Operator HKL',
|
||||
factory: OperatorHklColorTheme,
|
||||
getParams: getOperatorHklColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(OperatorHklColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
|
||||
}
|
||||
90
src/mol-theme/color/operator-name.ts
Normal file
90
src/mol-theme/color/operator-name.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { StructureElement, Link, Structure } from '../../mol-model/structure';
|
||||
import { Location } from '../../mol-model/location';
|
||||
import { ColorTheme, LocationColor } from '../color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition'
|
||||
import { ThemeDataContext } from '../theme';
|
||||
import { getPaletteParams, getPalette } from '../../mol-util/color/palette';
|
||||
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC)
|
||||
const Description = `Assigns a color based on the operator name of a transformed chain.`
|
||||
|
||||
export const OperatorNameColorThemeParams = {
|
||||
...getPaletteParams({ type: 'set', setList: 'set-3' }),
|
||||
}
|
||||
export type OperatorNameColorThemeParams = typeof OperatorNameColorThemeParams
|
||||
export function getOperatorNameColorThemeParams(ctx: ThemeDataContext) {
|
||||
const params = PD.clone(OperatorNameColorThemeParams)
|
||||
if (ctx.structure) {
|
||||
if (getOperatorNameSerialMap(ctx.structure.root).size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function getOperatorNameSerialMap(structure: Structure) {
|
||||
const map = new Map<string, number>()
|
||||
for (let i = 0, il = structure.units.length; i < il; ++i) {
|
||||
const name = structure.units[i].conformation.operator.name
|
||||
if (!map.has(name)) map.set(name, map.size)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export function OperatorNameColorTheme(ctx: ThemeDataContext, props: PD.Values<OperatorNameColorThemeParams>): ColorTheme<OperatorNameColorThemeParams> {
|
||||
let color: LocationColor
|
||||
let legend: ScaleLegend | TableLegend | undefined
|
||||
|
||||
if (ctx.structure) {
|
||||
const operatorNameSerialMap = getOperatorNameSerialMap(ctx.structure.root)
|
||||
|
||||
const labelTable = Array.from(operatorNameSerialMap.keys())
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(operatorNameSerialMap.size, props)
|
||||
legend = palette.legend
|
||||
|
||||
color = (location: Location): Color => {
|
||||
let serial: number | undefined = undefined
|
||||
if (StructureElement.Location.is(location)) {
|
||||
const name = location.unit.conformation.operator.name
|
||||
serial = operatorNameSerialMap.get(name)
|
||||
} else if (Link.isLocation(location)) {
|
||||
const name = location.aUnit.conformation.operator.name
|
||||
serial = operatorNameSerialMap.get(name)
|
||||
}
|
||||
return serial === undefined ? DefaultColor : palette.color(serial)
|
||||
}
|
||||
} else {
|
||||
color = () => DefaultColor
|
||||
}
|
||||
|
||||
return {
|
||||
factory: OperatorNameColorTheme,
|
||||
granularity: 'instance',
|
||||
color,
|
||||
props,
|
||||
description: Description,
|
||||
legend
|
||||
}
|
||||
}
|
||||
|
||||
export const OperatorNameColorThemeProvider: ColorTheme.Provider<OperatorNameColorThemeParams> = {
|
||||
label: 'Operator Name',
|
||||
factory: OperatorNameColorTheme,
|
||||
getParams: getOperatorNameColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(OperatorNameColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user