mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 05:44:23 +08:00
Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
104999b7dc | ||
|
|
e5341623d3 | ||
|
|
0e9238e5ec | ||
|
|
43c292e2df | ||
|
|
fbfd1b20d8 | ||
|
|
5330df87e1 | ||
|
|
ad6b3c6fe0 | ||
|
|
add76a87d9 | ||
|
|
b71c2f365c | ||
|
|
a5443189d3 | ||
|
|
7686b61728 | ||
|
|
844c13cd35 | ||
|
|
d1c8b92fdf | ||
|
|
93d33bca80 | ||
|
|
6550e53414 | ||
|
|
96dddb0998 | ||
|
|
baa64d8109 | ||
|
|
2df145aa8f | ||
|
|
06b9c5f2de | ||
|
|
e03b689f27 | ||
|
|
e4cdcff3ee | ||
|
|
f73150d074 | ||
|
|
451dc12689 | ||
|
|
a3fb7762d8 | ||
|
|
3dfafc3202 | ||
|
|
4fcea991d3 | ||
|
|
0607ed46d1 | ||
|
|
30d6244e82 | ||
|
|
fab8c74365 | ||
|
|
1952922e4e | ||
|
|
1eb351369e | ||
|
|
701d782485 | ||
|
|
dc8457c4dc | ||
|
|
f104cd4d11 | ||
|
|
9b56a6ae65 | ||
|
|
2485ad5a2f | ||
|
|
a56716ab6a | ||
|
|
e0aaaa989e | ||
|
|
9ec0f9e736 | ||
|
|
47968eeeec | ||
|
|
9c157b70e1 | ||
|
|
6d7e4ca227 | ||
|
|
fccd08d2ec | ||
|
|
19bae202d0 | ||
|
|
4ba0ae24e4 | ||
|
|
fcf3718d75 | ||
|
|
df1dd94f1c | ||
|
|
65ba401850 | ||
|
|
a98f5e1047 | ||
|
|
e5cf97d1ea | ||
|
|
1844fc14b2 | ||
|
|
d185c0ef34 | ||
|
|
40a4211e75 | ||
|
|
daa2bbd042 | ||
|
|
ed5b4b27a8 | ||
|
|
408ccb4353 | ||
|
|
99e3cd6654 | ||
|
|
0819ace1dc | ||
|
|
987c9210bd | ||
|
|
84fb42a161 | ||
|
|
53d3480701 | ||
|
|
eb629ef337 | ||
|
|
c26111e8fb | ||
|
|
4853ff7a1a | ||
|
|
1bdebda136 | ||
|
|
fe5b847797 | ||
|
|
19ec5b226c | ||
|
|
4bb32d31dc | ||
|
|
976a469cc7 | ||
|
|
86087aa3ca | ||
|
|
c0e955d472 | ||
|
|
eca052e52e | ||
|
|
a1e05387e4 | ||
|
|
301940c8bd | ||
|
|
d96303627c | ||
|
|
051b48776e | ||
|
|
26054681d8 | ||
|
|
70fa85d7d4 | ||
|
|
5a23cd483e | ||
|
|
d759b07f1b | ||
|
|
4694da0057 | ||
|
|
f930e3dbe0 | ||
|
|
fcf45d20be | ||
|
|
ad4ba7bcf9 | ||
|
|
26644ede49 | ||
|
|
810973ff54 | ||
|
|
6ad09c60c0 | ||
|
|
dc146f5f04 | ||
|
|
e1b771bba4 | ||
|
|
e2ab3a6fd6 | ||
|
|
d1296de676 | ||
|
|
fcac1a62c6 | ||
|
|
5eafddf97a | ||
|
|
e2dcbc3d65 | ||
|
|
54a388da9c | ||
|
|
3849c341b8 | ||
|
|
31f4803c0a | ||
|
|
d6e36d4ca7 | ||
|
|
0d526fdc98 | ||
|
|
04b36170d8 | ||
|
|
db787c9ea4 | ||
|
|
e1e6f9ca48 | ||
|
|
40b5605e10 | ||
|
|
609654b689 | ||
|
|
45ef00f1d1 | ||
|
|
88380ff917 | ||
|
|
bc7bfe9788 | ||
|
|
469ca6cb41 | ||
|
|
c0be790ff1 | ||
|
|
8c1d16353e | ||
|
|
d76d475015 | ||
|
|
69024152cb | ||
|
|
4a19aedec8 | ||
|
|
df89351301 | ||
|
|
9a0c87695f | ||
|
|
a393231522 | ||
|
|
33de60d365 | ||
|
|
3cf67f7605 | ||
|
|
ffdcf798e0 | ||
|
|
397e1235e7 | ||
|
|
4e77699076 | ||
|
|
b47d046505 | ||
|
|
74aa24bfa0 | ||
|
|
30d5b0ddb1 | ||
|
|
1e35ea15eb | ||
|
|
bc998ab328 | ||
|
|
e5e245f4ee | ||
|
|
c6073b894a | ||
|
|
9b11794f22 | ||
|
|
f2b9dceaab | ||
|
|
9ccaaf6c80 | ||
|
|
ecb97e525e | ||
|
|
c36c6a6d97 | ||
|
|
60b92471f1 | ||
|
|
79e283cfbd | ||
|
|
3778dacb08 | ||
|
|
e407f7279b | ||
|
|
ea54209414 | ||
|
|
d10a36509b | ||
|
|
4af560e63a | ||
|
|
ecb8900258 | ||
|
|
7bfc1b0ebc | ||
|
|
5edae9d6f7 | ||
|
|
fe702a8c63 | ||
|
|
c8868464a5 | ||
|
|
720e65d2e6 | ||
|
|
b5123ff36a | ||
|
|
d237034e8e | ||
|
|
aab95d27e0 | ||
|
|
c68306125e | ||
|
|
3173396737 | ||
|
|
212a3eeb6c | ||
|
|
17b25354f5 | ||
|
|
9f176bd2bc | ||
|
|
4a78283ce1 | ||
|
|
81e29533dc | ||
|
|
c1a2c602a1 | ||
|
|
c436653ce9 | ||
|
|
a2b4ed7c1c | ||
|
|
83968aa408 | ||
|
|
71539cc75a | ||
|
|
881d4d2a99 | ||
|
|
ae2f2e7d0e | ||
|
|
e31f0f7660 | ||
|
|
3586207968 | ||
|
|
b575793b83 | ||
|
|
81bf653790 | ||
|
|
6186c60cd9 | ||
|
|
6ab480589a | ||
|
|
571f8187c3 | ||
|
|
d510ff00dc | ||
|
|
7a0f286fb4 | ||
|
|
fccf8d6b87 | ||
|
|
e0c08e89d0 | ||
|
|
ef9885411c | ||
|
|
7542ead360 | ||
|
|
043ab08066 | ||
|
|
ec0933d197 | ||
|
|
aef34a687d | ||
|
|
9b7192f261 | ||
|
|
18212d9ee7 | ||
|
|
8d4e0730e8 | ||
|
|
ba8e9e189f | ||
|
|
b7935de7af | ||
|
|
10ca32f9d7 | ||
|
|
bb86d83c96 | ||
|
|
907b08cc99 | ||
|
|
a07d593909 | ||
|
|
a0a3ff1969 | ||
|
|
7fac8a8f77 | ||
|
|
7266c67e32 | ||
|
|
50c8d09742 | ||
|
|
7377947975 | ||
|
|
a3c4daf30a | ||
|
|
9d7e6f1d99 | ||
|
|
9e105020e3 | ||
|
|
698f7e16bd | ||
|
|
93df548cfe | ||
|
|
a0b1593c82 | ||
|
|
fc81e08d73 | ||
|
|
5369fa5adf | ||
|
|
316a77c716 | ||
|
|
42dfa69ad7 | ||
|
|
cae4eb8b0e | ||
|
|
5514b24fdf | ||
|
|
d570bc352e | ||
|
|
d653a96b25 | ||
|
|
e0a594121b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ tsconfig.commonjs.tsbuildinfo
|
||||
|
||||
*.sublime-workspace
|
||||
.idea
|
||||
|
||||
.DS_Store
|
||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -3,6 +3,67 @@ All notable changes to this project will be documented in this file, following t
|
||||
|
||||
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [v2.2.0] - 2021-07-31
|
||||
|
||||
- Add `tubularHelices` parameter to Cartoon representation
|
||||
- Add `SdfFormat` and update SDF parser to be able to parse data headers according to spec (hopefully :)) #230
|
||||
- Fix mononucleotides detected as polymer components (#229)
|
||||
- Set default outline scale back to 1
|
||||
- Improved DCD reader cell angle handling (interpret near 0 angles as 90 deg)
|
||||
- Handle more residue/atom names commonly used in force-fields
|
||||
- Add USDZ support to ``geo-export`` extension.
|
||||
- Fix `includeParent` support for multi-instance bond visuals.
|
||||
- Add `operator` Loci granularity, selecting everything with the same operator name.
|
||||
- Prefer ``_label_seq_id`` fields in secondary structure assignment.
|
||||
- Support new EMDB API (https://www.ebi.ac.uk/emdb/api/entry/map/[EMBD-ID]) for EM volume contour levels.
|
||||
- ``Canvas3D`` tweaks:
|
||||
- Update ``forceDraw`` logic.
|
||||
- Ensure the scene is re-rendered when viewport size changes.
|
||||
- Support ``noDraw`` mode in ``PluginAnimationLoop``.
|
||||
|
||||
## [v2.1.0] - 2021-07-05
|
||||
|
||||
- Add parameter for to display aromatic bonds as dashes next to solid cylinder/line.
|
||||
- Add backbone representation
|
||||
- Fix outline in orthographic mode and set default scale to 2.
|
||||
|
||||
## [v2.0.7] - 2021-06-23
|
||||
|
||||
- Add ability to specify ``volumeIndex`` in ``Viewer.loadVolumeFromUrl`` to better support Volume Server inputs.
|
||||
- Support in-place reordering for trajectory ``Frame.x/y/z`` arrays for better memory efficiency.
|
||||
- Fixed text CIF encoder edge cases (most notably single whitespace not being escaped).
|
||||
|
||||
## [v2.0.6] - 2021-06-01
|
||||
|
||||
- Add glTF (GLB) and STL support to ``geo-export`` extension.
|
||||
- Protein crosslink improvements
|
||||
- Change O-S bond distance to allow for NOS bridges (doi:10.1038/s41586-021-03513-3)
|
||||
- Added NOS-bridges query & improved disulfide-bridges query
|
||||
- Fix #178: ``IndexPairBonds`` for non-single residue structures (bug due to atom reordering).
|
||||
- Add volumetric color smoothing for MolecularSurface and GaussianSurface representations (#173)
|
||||
- Fix nested 3d grid lookup that caused results being overwritten in non-covalent interactions computation.
|
||||
- Basic implementation of ``BestDatabaseSequenceMapping`` (parse from CIF, color theme, superposition).
|
||||
- Add atom id ranges support to Selection UI.
|
||||
|
||||
## [v2.0.5] - 2021-04-26
|
||||
|
||||
- Ability to pass ``Canvas3DContext`` to ``PluginContext.fromCanvas``.
|
||||
- Relative frame support for ``Canvas3D`` viewport.
|
||||
- Fix bug in screenshot copy UI.
|
||||
- Add ability to select residues from a list of identifiers to the Selection UI.
|
||||
- Fix SSAO bugs when used with ``Canvas3D`` viewport.
|
||||
- Support for full pausing (no draw) rendering: ``Canvas3D.pause(true)``.
|
||||
- Add `MeshBuilder.addMesh`.
|
||||
- Add `Torus` primitive.
|
||||
- Lazy volume loading support.
|
||||
- [Breaking] ``Viewer.loadVolumeFromUrl`` signature change.
|
||||
- ``loadVolumeFromUrl(url, format, isBinary, isovalues, entryId)`` => ``loadVolumeFromUrl({ url, format, isBinary }, isovalues, { entryId, isLazy })``
|
||||
- Add ``TextureMesh`` support to ``geo-export`` extension.
|
||||
|
||||
## [v2.0.4] - 2021-04-20
|
||||
|
||||
- [WIP] Mesh export extension
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
The goal of **Mol\*** (*/'mol-star/*) is to provide a technology stack that serves as a basis for the next-generation data delivery and analysis tools for (not only) macromolecular structure data. Mol* development was jointly initiated by PDBe and RCSB PDB to combine and build on the strengths of [LiteMol](https://litemol.org) (developed by PDBe) and [NGL](https://nglviewer.org) (developed by RCSB PDB) viewers.
|
||||
|
||||
When using Mol*, please cite:
|
||||
|
||||
David Sehnal, Sebastian Bittrich, Mandar Deshpande, Radka Svobodová, Karel Berka, Václav Bazgier, Sameer Velankar, Stephen K Burley, Jaroslav Koča, Alexander S Rose: [Mol* Viewer: modern web app for 3D visualization and analysis of large biomolecular structures](https://doi.org/10.1093/nar/gkab314), *Nucleic Acids Research*, 2021; https://doi.org/10.1093/nar/gkab314.
|
||||
|
||||
## Project Structure Overview
|
||||
|
||||
|
||||
@@ -28,3 +28,8 @@
|
||||
* Multiple models with different sets of ligands or missing ligands (1J6T, 1VRC, 2ICY, 1O2F)
|
||||
* Long linear sugar chain (4HG6)
|
||||
* Anisotropic B-factors/Ellipsoids (1EJG)
|
||||
* NOS bridges (LYS-CSO in 7B0L, 6ZWJ, 6ZWH)
|
||||
|
||||
Assembly symmetries
|
||||
* 5M30 (Assembly 1, C3 local and pseudo)
|
||||
* 1RB8 (Assembly 1, I global)
|
||||
9181
package-lock.json
generated
9181
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "2.0.4",
|
||||
"version": "2.2.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -27,9 +27,9 @@
|
||||
"watch-tsc": "tsc --watch --incremental",
|
||||
"watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
|
||||
"watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch",
|
||||
"watch-webpack": "webpack -w --mode development --display minimal",
|
||||
"watch-webpack-viewer": "webpack -w --mode development --display errors-only --info-verbosity verbose --config ./webpack.config.viewer.js",
|
||||
"watch-webpack-viewer-debug": "webpack -w --mode development --display errors-only --info-verbosity verbose --config ./webpack.config.viewer.debug.js",
|
||||
"watch-webpack": "webpack -w --mode development --stats minimal",
|
||||
"watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
|
||||
"watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
|
||||
"serve": "http-server -p 1338 -g",
|
||||
"model-server": "node lib/commonjs/servers/model/server.js",
|
||||
"model-server-watch": "nodemon --watch lib lib/commonjs/servers/model/server.js",
|
||||
@@ -101,6 +101,7 @@
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^5.3.0",
|
||||
"cpx2": "^3.0.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^5.0.1",
|
||||
"eslint": "^7.15.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
@@ -110,15 +111,17 @@
|
||||
"http-server": "^0.12.3",
|
||||
"jest": "^26.6.3",
|
||||
"mini-css-extract-plugin": "^1.3.2",
|
||||
"node-sass": "^5.0.0",
|
||||
"node-sass": "^6.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass-loader": "^10.1.0",
|
||||
"sass-loader": "^11.1.1",
|
||||
"simple-git": "^2.25.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"ts-jest": "^26.4.4",
|
||||
"typescript": "^4.2.3",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"typescript": "^4.2.4",
|
||||
"webpack": "^5.37.1",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webpack-version-file-plugin": "^0.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
var viewer = new molstar.Viewer('app', {
|
||||
layoutIsExpanded: false,
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: false,
|
||||
layoutShowRemoteState: false,
|
||||
layoutShowSequence: true,
|
||||
@@ -37,6 +37,7 @@
|
||||
});
|
||||
viewer.loadPdb('7bv2');
|
||||
viewer.loadEmdb('EMD-30210', { detail: 6 });
|
||||
|
||||
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -243,28 +243,81 @@ export class Viewer {
|
||||
}));
|
||||
}
|
||||
|
||||
async loadVolumeFromUrl(url: string, format: BuildInVolumeFormat, isBinary: boolean, isovalues: VolumeIsovalueInfo[], entryId?: string) {
|
||||
/**
|
||||
* @example Load X-ray density from volume server
|
||||
viewer.loadVolumeFromUrl({
|
||||
url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/cell?detail=3',
|
||||
format: 'dscif',
|
||||
isBinary: true
|
||||
}, [{
|
||||
type: 'relative',
|
||||
value: 1.5,
|
||||
color: 0x3362B2
|
||||
}, {
|
||||
type: 'relative',
|
||||
value: 3,
|
||||
color: 0x33BB33,
|
||||
volumeIndex: 1
|
||||
}, {
|
||||
type: 'relative',
|
||||
value: -3,
|
||||
color: 0xBB3333,
|
||||
volumeIndex: 1
|
||||
}], {
|
||||
entryId: ['2FO-FC', 'FO-FC'],
|
||||
isLazy: true
|
||||
});
|
||||
* *********************
|
||||
* @example Load EM density from volume server
|
||||
viewer.loadVolumeFromUrl({
|
||||
url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6',
|
||||
format: 'dscif',
|
||||
isBinary: true
|
||||
}, [{
|
||||
type: 'relative',
|
||||
value: 1,
|
||||
color: 0x3377aa
|
||||
}], {
|
||||
entryId: 'EMD-30210',
|
||||
isLazy: true
|
||||
});
|
||||
*/
|
||||
async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string | string[], isLazy?: boolean }) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
if (!plugin.dataFormats.get(format)) {
|
||||
throw new Error(`Unknown density format: ${format}`);
|
||||
}
|
||||
|
||||
if (options?.isLazy) {
|
||||
const update = this.plugin.build();
|
||||
update.toRoot().apply(StateTransforms.Data.LazyVolume, {
|
||||
url,
|
||||
format,
|
||||
entryId: options?.entryId,
|
||||
isBinary,
|
||||
isovalues: isovalues.map(v => ({ alpha: 1, volumeIndex: 0, ...v }))
|
||||
});
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
return plugin.dataTransaction(async () => {
|
||||
const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } });
|
||||
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
|
||||
|
||||
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId });
|
||||
const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
|
||||
if (!volume?.isOk) throw new Error('Failed to parse any volume.');
|
||||
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId });
|
||||
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
|
||||
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
|
||||
|
||||
const repr = plugin.build().to(volume);
|
||||
const repr = plugin.build();
|
||||
for (const iso of isovalues) {
|
||||
repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, {
|
||||
type: 'isosurface',
|
||||
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
|
||||
color: 'uniform',
|
||||
colorParams: { value: iso.color }
|
||||
}));
|
||||
repr
|
||||
.to(parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume)
|
||||
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
|
||||
type: 'isosurface',
|
||||
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
|
||||
color: 'uniform',
|
||||
colorParams: { value: iso.color }
|
||||
}));
|
||||
}
|
||||
|
||||
await repr.commit();
|
||||
@@ -284,5 +337,6 @@ export interface VolumeIsovalueInfo {
|
||||
type: 'absolute' | 'relative',
|
||||
value: number,
|
||||
color: Color,
|
||||
alpha?: number
|
||||
alpha?: number,
|
||||
volumeIndex?: number
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Structure, StructureElement, StructureProperties } from '../../mol-model/structure';
|
||||
import { Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
|
||||
import { Task, RuntimeContext } from '../../mol-task';
|
||||
import { CentroidHelper } from '../../mol-math/geometry/centroid-helper';
|
||||
import { AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area';
|
||||
import { AccessibleSurfaceAreaParams } from '../../mol-model-props/computed/accessible-surface-area';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { getElementMoleculeType } from '../../mol-model/structure/util';
|
||||
import { MoleculeType } from '../../mol-model/structure/model/types';
|
||||
@@ -16,6 +16,10 @@ import { AccessibleSurfaceArea } from '../../mol-model-props/computed/accessible
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { MembraneOrientation } from './prop';
|
||||
|
||||
const LARGE_CA_THRESHOLD = 5000;
|
||||
const DEFAULT_UPDATE_INTERVAL = 10;
|
||||
const LARGE_CA_UPDATE_INTERVAL = 1;
|
||||
|
||||
interface ANVILContext {
|
||||
structure: Structure,
|
||||
|
||||
@@ -24,19 +28,23 @@ interface ANVILContext {
|
||||
minThickness: number,
|
||||
maxThickness: number,
|
||||
asaCutoff: number,
|
||||
adjust: number,
|
||||
|
||||
offsets: ArrayLike<number>,
|
||||
exposed: ArrayLike<boolean>,
|
||||
exposed: ArrayLike<number>,
|
||||
hydrophobic: ArrayLike<boolean>,
|
||||
centroid: Vec3,
|
||||
extent: number
|
||||
extent: number,
|
||||
large: boolean
|
||||
};
|
||||
|
||||
export const ANVILParams = {
|
||||
numberOfSpherePoints: PD.Numeric(120, { min: 35, max: 700, step: 1 }, { description: 'Number of spheres/directions to test for membrane placement. Original value is 350.' }),
|
||||
numberOfSpherePoints: PD.Numeric(140, { min: 35, max: 700, step: 1 }, { description: 'Number of spheres/directions to test for membrane placement. Original value is 350.' }),
|
||||
stepSize: PD.Numeric(1, { min: 0.25, max: 4, step: 0.25 }, { description: 'Thickness of membrane slices that will be tested' }),
|
||||
minThickness: PD.Numeric(20, { min: 10, max: 30, step: 1}, { description: 'Minimum membrane thickness used during refinement' }),
|
||||
maxThickness: PD.Numeric(40, { min: 30, max: 50, step: 1}, { description: 'Maximum membrane thickness used during refinement' }),
|
||||
asaCutoff: PD.Numeric(40, { min: 10, max: 100, step: 1 }, { description: 'Absolute ASA cutoff above which residues will be considered' })
|
||||
asaCutoff: PD.Numeric(40, { min: 10, max: 100, step: 1 }, { description: 'Relative ASA cutoff above which residues will be considered' }),
|
||||
adjust: PD.Numeric(14, { min: 0, max: 30, step: 1 }, { description: 'Minimum length of membrane-spanning regions (original values: 14 for alpha-helices and 5 for beta sheets). Set to 0 to not optimize membrane thickness.' })
|
||||
};
|
||||
export type ANVILParams = typeof ANVILParams
|
||||
export type ANVILProps = PD.Values<ANVILParams>
|
||||
@@ -54,22 +62,33 @@ export function computeANVIL(structure: Structure, props: ANVILProps) {
|
||||
});
|
||||
}
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3add = Vec3.add;
|
||||
const v3clone = Vec3.clone;
|
||||
const v3create = Vec3.create;
|
||||
const v3distance = Vec3.distance;
|
||||
const v3dot = Vec3.dot;
|
||||
const v3magnitude = Vec3.magnitude;
|
||||
const v3normalize = Vec3.normalize;
|
||||
const v3scale = Vec3.scale;
|
||||
const v3scaleAndAdd = Vec3.scaleAndAdd;
|
||||
const v3set = Vec3.set;
|
||||
const v3squaredDistance = Vec3.squaredDistance;
|
||||
const v3sub = Vec3.sub;
|
||||
const v3zero = Vec3.zero;
|
||||
|
||||
const centroidHelper = new CentroidHelper();
|
||||
function initialize(structure: Structure, props: ANVILProps): ANVILContext {
|
||||
async function initialize(structure: Structure, props: ANVILProps, accessibleSurfaceArea: AccessibleSurfaceArea): Promise<ANVILContext> {
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const { label_atom_id, x, y, z } = StructureProperties.atom;
|
||||
const elementCount = structure.polymerResidueCount;
|
||||
const { label_atom_id, label_comp_id, x, y, z } = StructureProperties.atom;
|
||||
const asaCutoff = props.asaCutoff / 100;
|
||||
centroidHelper.reset();
|
||||
|
||||
let offsets = new Int32Array(elementCount);
|
||||
let exposed = new Array<boolean>(elementCount);
|
||||
const offsets = new Array<number>();
|
||||
const exposed = new Array<number>();
|
||||
const hydrophobic = new Array<boolean>();
|
||||
|
||||
const accessibleSurfaceArea = structure && AccessibleSurfaceAreaProvider.get(structure);
|
||||
const asa = accessibleSurfaceArea.value!;
|
||||
|
||||
const vec = Vec3();
|
||||
let m = 0;
|
||||
const vec = v3zero();
|
||||
for (let i = 0, il = structure.units.length; i < il; ++i) {
|
||||
const unit = structure.units[i];
|
||||
const { elements } = unit;
|
||||
@@ -85,64 +104,82 @@ function initialize(structure: Structure, props: ANVILProps): ANVILContext {
|
||||
}
|
||||
|
||||
// only CA is considered for downstream operations
|
||||
if (label_atom_id(l) !== 'CA') {
|
||||
if (label_atom_id(l) !== 'CA' && label_atom_id(l) !== 'BB') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// original ANVIL only considers canonical amino acids
|
||||
if (!MaxAsa[label_comp_id(l)]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// while iterating use first pass to compute centroid
|
||||
Vec3.set(vec, x(l), y(l), z(l));
|
||||
v3set(vec, x(l), y(l), z(l));
|
||||
centroidHelper.includeStep(vec);
|
||||
|
||||
// keep track of offsets and exposed state to reuse
|
||||
offsets[m] = structure.serialMapping.getSerialIndex(l.unit, l.element);
|
||||
exposed[m] = AccessibleSurfaceArea.getValue(l, asa) > props.asaCutoff;
|
||||
|
||||
m++;
|
||||
offsets.push(structure.serialMapping.getSerialIndex(l.unit, l.element));
|
||||
if (AccessibleSurfaceArea.getValue(l, accessibleSurfaceArea) / MaxAsa[label_comp_id(l)] > asaCutoff) {
|
||||
exposed.push(structure.serialMapping.getSerialIndex(l.unit, l.element));
|
||||
hydrophobic.push(isHydrophobic(label_comp_id(l)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// omit potentially empty tail1
|
||||
offsets = offsets.slice(0, m);
|
||||
exposed = exposed.slice(0, m);
|
||||
|
||||
// calculate centroid and extent
|
||||
centroidHelper.finishedIncludeStep();
|
||||
const centroid = centroidHelper.center;
|
||||
const centroid = v3clone(centroidHelper.center);
|
||||
for (let k = 0, kl = offsets.length; k < kl; k++) {
|
||||
setLocation(l, structure, offsets[k]);
|
||||
Vec3.set(vec, x(l), y(l), z(l));
|
||||
v3set(vec, x(l), y(l), z(l));
|
||||
centroidHelper.radiusStep(vec);
|
||||
}
|
||||
const extent = 1.2 * Math.sqrt(centroidHelper.radiusSq);
|
||||
|
||||
return {
|
||||
...props,
|
||||
structure: structure,
|
||||
structure,
|
||||
|
||||
offsets: offsets,
|
||||
exposed: exposed,
|
||||
centroid: centroid,
|
||||
extent: extent
|
||||
offsets,
|
||||
exposed,
|
||||
hydrophobic,
|
||||
centroid,
|
||||
extent,
|
||||
large: offsets.length > LARGE_CA_THRESHOLD
|
||||
};
|
||||
}
|
||||
|
||||
export async function calculate(runtime: RuntimeContext, structure: Structure, params: ANVILProps): Promise<MembraneOrientation> {
|
||||
const { label_comp_id } = StructureProperties.atom;
|
||||
// can't get away with the default 92 points here
|
||||
const asaProps = { ...PD.getDefaultValues(AccessibleSurfaceAreaParams), probeSize: 4.0, traceOnly: true, numberOfSpherePoints: 184 };
|
||||
const accessibleSurfaceArea = await AccessibleSurfaceArea.compute(structure, asaProps).runInContext(runtime);
|
||||
|
||||
const ctx = initialize(structure, params);
|
||||
const initialHphobHphil = HphobHphil.filtered(ctx, label_comp_id);
|
||||
const ctx = await initialize(structure, params, accessibleSurfaceArea);
|
||||
const initialHphobHphil = HphobHphil.initial(ctx);
|
||||
|
||||
const initialMembrane = findMembrane(ctx, generateSpherePoints(ctx, ctx.numberOfSpherePoints), initialHphobHphil, label_comp_id);
|
||||
const alternativeMembrane = findMembrane(ctx, findProximateAxes(ctx, initialMembrane), initialHphobHphil, label_comp_id);
|
||||
const initialMembrane = (await findMembrane(runtime, 'Placing initial membrane...', ctx, generateSpherePoints(ctx, ctx.numberOfSpherePoints), initialHphobHphil))!;
|
||||
const refinedMembrane = (await findMembrane(runtime, 'Refining membrane placement...', ctx, findProximateAxes(ctx, initialMembrane), initialHphobHphil))!;
|
||||
let membrane = initialMembrane.qmax! > refinedMembrane.qmax! ? initialMembrane : refinedMembrane;
|
||||
|
||||
const membrane = initialMembrane.qmax! > alternativeMembrane.qmax! ? initialMembrane : alternativeMembrane;
|
||||
if (ctx.adjust && !ctx.large) {
|
||||
membrane = await adjustThickness(runtime, 'Adjusting membrane thickness...', ctx, membrane, initialHphobHphil);
|
||||
}
|
||||
|
||||
const normalVector = v3zero();
|
||||
const center = v3zero();
|
||||
v3sub(normalVector, membrane.planePoint1, membrane.planePoint2);
|
||||
v3normalize(normalVector, normalVector);
|
||||
|
||||
v3add(center, membrane.planePoint1, membrane.planePoint2);
|
||||
v3scale(center, center, 0.5);
|
||||
const extent = adjustExtent(ctx, membrane, center);
|
||||
|
||||
return {
|
||||
planePoint1: membrane.planePoint1,
|
||||
planePoint2: membrane.planePoint2,
|
||||
normalVector: membrane.normalVector!,
|
||||
radius: ctx.extent,
|
||||
centroid: ctx.centroid
|
||||
normalVector,
|
||||
centroid: center,
|
||||
radius: extent
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,82 +197,79 @@ namespace MembraneCandidate {
|
||||
return {
|
||||
planePoint1: c1,
|
||||
planePoint2: c2,
|
||||
stats: stats
|
||||
stats
|
||||
};
|
||||
}
|
||||
|
||||
export function scored(spherePoint: Vec3, c1: Vec3, c2: Vec3, stats: HphobHphil, qmax: number, centroid: Vec3): MembraneCandidate {
|
||||
const diam_vect = Vec3();
|
||||
Vec3.sub(diam_vect, centroid, spherePoint);
|
||||
export function scored(spherePoint: Vec3, planePoint1: Vec3, planePoint2: Vec3, stats: HphobHphil, qmax: number, centroid: Vec3): MembraneCandidate {
|
||||
const normalVector = v3zero();
|
||||
v3sub(normalVector, centroid, spherePoint);
|
||||
return {
|
||||
planePoint1: c1,
|
||||
planePoint2: c2,
|
||||
stats: stats,
|
||||
normalVector: diam_vect,
|
||||
spherePoint: spherePoint,
|
||||
qmax: qmax
|
||||
planePoint1,
|
||||
planePoint2,
|
||||
stats,
|
||||
normalVector,
|
||||
spherePoint,
|
||||
qmax
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function findMembrane(ctx: ANVILContext, spherePoints: Vec3[], initialStats: HphobHphil, label_comp_id: StructureElement.Property<string>): MembraneCandidate {
|
||||
const { centroid, stepSize, minThickness, maxThickness } = ctx;
|
||||
async function findMembrane(runtime: RuntimeContext, message: string | undefined, ctx: ANVILContext, spherePoints: Vec3[], initialStats: HphobHphil): Promise<MembraneCandidate | undefined> {
|
||||
const { centroid, stepSize, minThickness, maxThickness, large } = ctx;
|
||||
// best performing membrane
|
||||
let membrane: MembraneCandidate;
|
||||
let membrane: MembraneCandidate | undefined;
|
||||
// score of the best performing membrane
|
||||
let qmax = 0;
|
||||
|
||||
// construct slices of thickness 1.0 along the axis connecting the centroid and the spherePoint
|
||||
const diam = Vec3();
|
||||
for (let i = 0, il = spherePoints.length; i < il; i++) {
|
||||
const spherePoint = spherePoints[i];
|
||||
Vec3.sub(diam, centroid, spherePoint);
|
||||
Vec3.scale(diam, diam, 2);
|
||||
const diamNorm = Vec3.magnitude(diam);
|
||||
const qvartemp = [];
|
||||
const diam = v3zero();
|
||||
for (let n = 0, nl = spherePoints.length; n < nl; n++) {
|
||||
if (runtime.shouldUpdate && message && (n + 1) % (large ? LARGE_CA_UPDATE_INTERVAL : DEFAULT_UPDATE_INTERVAL) === 0) {
|
||||
await runtime.update({ message, current: (n + 1), max: nl });
|
||||
}
|
||||
|
||||
const spherePoint = spherePoints[n];
|
||||
v3sub(diam, centroid, spherePoint);
|
||||
v3scale(diam, diam, 2);
|
||||
const diamNorm = v3magnitude(diam);
|
||||
|
||||
const sliceStats = HphobHphil.sliced(ctx, stepSize, spherePoint, diam, diamNorm);
|
||||
const qvartemp = [];
|
||||
for (let i = 0, il = diamNorm - stepSize; i < il; i += stepSize) {
|
||||
const c1 = Vec3();
|
||||
const c2 = Vec3();
|
||||
Vec3.scaleAndAdd(c1, spherePoint, diam, i / diamNorm);
|
||||
Vec3.scaleAndAdd(c2, spherePoint, diam, (i + stepSize) / diamNorm);
|
||||
const c1 = v3zero();
|
||||
const c2 = v3zero();
|
||||
v3scaleAndAdd(c1, spherePoint, diam, i / diamNorm);
|
||||
v3scaleAndAdd(c2, spherePoint, diam, (i + stepSize) / diamNorm);
|
||||
|
||||
// evaluate how well this membrane slice embeddeds the peculiar residues
|
||||
const stats = HphobHphil.filtered(ctx, label_comp_id, (testPoint: Vec3) => isInMembranePlane(testPoint, diam, c1, c2));
|
||||
const stats = sliceStats[Math.round(i / stepSize)];
|
||||
qvartemp.push(MembraneCandidate.initial(c1, c2, stats));
|
||||
}
|
||||
|
||||
let jmax = (minThickness / stepSize) - 1;
|
||||
|
||||
for (let width = 0, widthl = maxThickness; width < widthl;) {
|
||||
const imax = qvartemp.length - 1 - jmax;
|
||||
|
||||
for (let i = 0, il = imax; i < il; i++) {
|
||||
const c1 = qvartemp[i].planePoint1;
|
||||
const c2 = qvartemp[i + jmax].planePoint2;
|
||||
let jmax = Math.floor((minThickness / stepSize) - 1);
|
||||
|
||||
for (let width = 0, widthl = maxThickness; width <= widthl;) {
|
||||
for (let i = 0, il = qvartemp.length - 1 - jmax; i < il; i++) {
|
||||
let hphob = 0;
|
||||
let hphil = 0;
|
||||
let total = 0;
|
||||
for (let j = 0; j < jmax; j++) {
|
||||
const ij = qvartemp[i + j];
|
||||
if (j === 0 || j === jmax - 1) {
|
||||
hphob += 0.5 * ij.stats.hphob;
|
||||
hphob += Math.floor(0.5 * ij.stats.hphob);
|
||||
hphil += 0.5 * ij.stats.hphil;
|
||||
} else {
|
||||
hphob += ij.stats.hphob;
|
||||
hphil += ij.stats.hphil;
|
||||
}
|
||||
total += ij.stats.total;
|
||||
}
|
||||
|
||||
const stats = HphobHphil.of(hphob, hphil, total);
|
||||
|
||||
if (hphob !== 0) {
|
||||
const stats = { hphob, hphil };
|
||||
const qvaltest = qValue(stats, initialStats);
|
||||
if (qvaltest > qmax) {
|
||||
if (qvaltest >= qmax) {
|
||||
qmax = qvaltest;
|
||||
membrane = MembraneCandidate.scored(spherePoint, c1, c2, HphobHphil.of(hphob, hphil, total), qmax, centroid);
|
||||
membrane = MembraneCandidate.scored(spherePoint, qvartemp[i].planePoint1, qvartemp[i + jmax].planePoint2, stats, qmax, centroid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +278,180 @@ function findMembrane(ctx: ANVILContext, spherePoints: Vec3[], initialStats: Hph
|
||||
}
|
||||
}
|
||||
|
||||
return membrane!;
|
||||
return membrane;
|
||||
}
|
||||
|
||||
/** Adjust membrane thickness by maximizing the number of membrane segments. */
|
||||
async function adjustThickness(runtime: RuntimeContext, message: string | undefined, ctx: ANVILContext, membrane: MembraneCandidate, initialHphobHphil: HphobHphil): Promise<MembraneCandidate> {
|
||||
const { minThickness, large } = ctx;
|
||||
const step = 0.3;
|
||||
let maxThickness = v3distance(membrane.planePoint1, membrane.planePoint2);
|
||||
|
||||
let maxNos = membraneSegments(ctx, membrane).length;
|
||||
let optimalThickness = membrane;
|
||||
|
||||
let n = 0;
|
||||
const nl = Math.ceil((maxThickness - minThickness) / step);
|
||||
while (maxThickness > minThickness) {
|
||||
n++;
|
||||
if (runtime.shouldUpdate && message && n % (large ? LARGE_CA_UPDATE_INTERVAL : DEFAULT_UPDATE_INTERVAL) === 0) {
|
||||
await runtime.update({ message, current: n, max: nl });
|
||||
}
|
||||
|
||||
const p = {
|
||||
...ctx,
|
||||
maxThickness,
|
||||
stepSize: step
|
||||
};
|
||||
const temp = await findMembrane(runtime, void 0, p, [membrane.spherePoint!], initialHphobHphil);
|
||||
if (temp) {
|
||||
const nos = membraneSegments(ctx, temp).length;
|
||||
if (nos > maxNos) {
|
||||
maxNos = nos;
|
||||
optimalThickness = temp;
|
||||
}
|
||||
}
|
||||
maxThickness -= step;
|
||||
}
|
||||
|
||||
return optimalThickness;
|
||||
}
|
||||
|
||||
/** Report auth_seq_ids for all transmembrane segments. Will reject segments that are shorter than the adjust parameter specifies. Missing residues are considered in-membrane. */
|
||||
function membraneSegments(ctx: ANVILContext, membrane: MembraneCandidate): ArrayLike<{ start: number, end: number }> {
|
||||
const { offsets, structure, adjust } = ctx;
|
||||
const { normalVector, planePoint1, planePoint2 } = membrane;
|
||||
const { units } = structure;
|
||||
const { elementIndices, unitIndices } = structure.serialMapping;
|
||||
const testPoint = v3zero();
|
||||
const { auth_seq_id } = StructureProperties.residue;
|
||||
|
||||
const d1 = -v3dot(normalVector!, planePoint1);
|
||||
const d2 = -v3dot(normalVector!, planePoint2);
|
||||
const dMin = Math.min(d1, d2);
|
||||
const dMax = Math.max(d1, d2);
|
||||
|
||||
const inMembrane: { [k: string]: Set<number> } = Object.create(null);
|
||||
const outMembrane: { [k: string]: Set<number> } = Object.create(null);
|
||||
const segments: Array<{ start: number, end: number }> = [];
|
||||
let authAsymId;
|
||||
let lastAuthAsymId = null;
|
||||
let authSeqId;
|
||||
let lastAuthSeqId = units[0].model.atomicHierarchy.residues.auth_seq_id.value((units[0] as Unit.Atomic).chainIndex[0]) - 1;
|
||||
let startOffset = 0;
|
||||
let endOffset = 0;
|
||||
|
||||
// collect all residues in membrane layer
|
||||
for (let k = 0, kl = offsets.length; k < kl; k++) {
|
||||
const unit = units[unitIndices[offsets[k]]];
|
||||
if (!Unit.isAtomic(unit)) throw 'Property only available for atomic models.';
|
||||
const elementIndex = elementIndices[offsets[k]];
|
||||
|
||||
authAsymId = unit.model.atomicHierarchy.chains.auth_asym_id.value(unit.chainIndex[elementIndex]);
|
||||
if (authAsymId !== lastAuthAsymId) {
|
||||
if (!inMembrane[authAsymId]) inMembrane[authAsymId] = new Set<number>();
|
||||
if (!outMembrane[authAsymId]) outMembrane[authAsymId] = new Set<number>();
|
||||
lastAuthAsymId = authAsymId;
|
||||
}
|
||||
|
||||
authSeqId = unit.model.atomicHierarchy.residues.auth_seq_id.value(unit.residueIndex[elementIndex]);
|
||||
v3set(testPoint, unit.conformation.x(elementIndex), unit.conformation.y(elementIndex), unit.conformation.z(elementIndex));
|
||||
if (_isInMembranePlane(testPoint, normalVector!, dMin, dMax)) {
|
||||
inMembrane[authAsymId].add(authSeqId);
|
||||
} else {
|
||||
outMembrane[authAsymId].add(authSeqId);
|
||||
}
|
||||
}
|
||||
|
||||
for (let k = 0, kl = offsets.length; k < kl; k++) {
|
||||
const unit = units[unitIndices[offsets[k]]];
|
||||
if (!Unit.isAtomic(unit)) throw 'Property only available for atomic models.';
|
||||
const elementIndex = elementIndices[offsets[k]];
|
||||
|
||||
authAsymId = unit.model.atomicHierarchy.chains.auth_asym_id.value(unit.chainIndex[elementIndex]);
|
||||
authSeqId = unit.model.atomicHierarchy.residues.auth_seq_id.value(unit.residueIndex[elementIndex]);
|
||||
if (inMembrane[authAsymId].has(authSeqId)) {
|
||||
// chain change
|
||||
if (authAsymId !== lastAuthAsymId) {
|
||||
segments.push({ start: startOffset, end: endOffset });
|
||||
lastAuthAsymId = authAsymId;
|
||||
startOffset = k;
|
||||
endOffset = k;
|
||||
}
|
||||
|
||||
// sequence gaps
|
||||
if (authSeqId !== lastAuthSeqId + 1) {
|
||||
if (outMembrane[authAsymId].has(lastAuthSeqId + 1)) {
|
||||
segments.push({ start: startOffset, end: endOffset });
|
||||
startOffset = k;
|
||||
}
|
||||
lastAuthSeqId = authSeqId;
|
||||
endOffset = k;
|
||||
} else {
|
||||
lastAuthSeqId++;
|
||||
endOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
segments.push({ start: startOffset, end: endOffset });
|
||||
|
||||
const l = StructureElement.Location.create(structure);
|
||||
let startAuth;
|
||||
let endAuth;
|
||||
const refinedSegments: Array<{ start: number, end: number }> = [];
|
||||
for (let k = 0, kl = segments.length; k < kl; k++) {
|
||||
const { start, end } = segments[k];
|
||||
if (start === 0 || end === offsets.length - 1) continue;
|
||||
|
||||
// evaluate residues 1 pos outside of membrane
|
||||
setLocation(l, structure, offsets[start - 1]);
|
||||
v3set(testPoint, l.unit.conformation.x(l.element), l.unit.conformation.y(l.element), l.unit.conformation.z(l.element));
|
||||
const d3 = -v3dot(normalVector!, testPoint);
|
||||
|
||||
setLocation(l, structure, offsets[end + 1]);
|
||||
v3set(testPoint, l.unit.conformation.x(l.element), l.unit.conformation.y(l.element), l.unit.conformation.z(l.element));
|
||||
const d4 = -v3dot(normalVector!, testPoint);
|
||||
|
||||
if (Math.min(d3, d4) < dMin && Math.max(d3, d4) > dMax) {
|
||||
// reject this refinement
|
||||
setLocation(l, structure, offsets[start]);
|
||||
startAuth = auth_seq_id(l);
|
||||
setLocation(l, structure, offsets[end]);
|
||||
endAuth = auth_seq_id(l);
|
||||
if (Math.abs(startAuth - endAuth) + 1 < adjust) {
|
||||
return [];
|
||||
}
|
||||
refinedSegments.push(segments[k]);
|
||||
}
|
||||
}
|
||||
|
||||
return refinedSegments;
|
||||
}
|
||||
|
||||
/** Filter for membrane residues and calculate the final extent of the membrane layer */
|
||||
function adjustExtent(ctx: ANVILContext, membrane: MembraneCandidate, centroid: Vec3): number {
|
||||
const { offsets, structure } = ctx;
|
||||
const { normalVector, planePoint1, planePoint2 } = membrane;
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const testPoint = v3zero();
|
||||
const { x, y, z } = StructureProperties.atom;
|
||||
|
||||
const d1 = -v3dot(normalVector!, planePoint1);
|
||||
const d2 = -v3dot(normalVector!, planePoint2);
|
||||
const dMin = Math.min(d1, d2);
|
||||
const dMax = Math.max(d1, d2);
|
||||
let extent = 0;
|
||||
|
||||
for (let k = 0, kl = offsets.length; k < kl; k++) {
|
||||
setLocation(l, structure, offsets[k]);
|
||||
v3set(testPoint, x(l), y(l), z(l));
|
||||
if (_isInMembranePlane(testPoint, normalVector!, dMin, dMax)) {
|
||||
const dsq = v3squaredDistance(testPoint, centroid);
|
||||
if (dsq > extent) extent = dsq;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.sqrt(extent);
|
||||
}
|
||||
|
||||
function qValue(currentStats: HphobHphil, initialStats: HphobHphil): number {
|
||||
@@ -262,23 +469,27 @@ function qValue(currentStats: HphobHphil, initialStats: HphobHphil): number {
|
||||
}
|
||||
|
||||
export function isInMembranePlane(testPoint: Vec3, normalVector: Vec3, planePoint1: Vec3, planePoint2: Vec3): boolean {
|
||||
const d1 = -Vec3.dot(normalVector, planePoint1);
|
||||
const d2 = -Vec3.dot(normalVector, planePoint2);
|
||||
const d = -Vec3.dot(normalVector, testPoint);
|
||||
return d > Math.min(d1, d2) && d < Math.max(d1, d2);
|
||||
const d1 = -v3dot(normalVector, planePoint1);
|
||||
const d2 = -v3dot(normalVector, planePoint2);
|
||||
return _isInMembranePlane(testPoint, normalVector, Math.min(d1, d2), Math.max(d1, d2));
|
||||
}
|
||||
|
||||
// generates a defined number of points on a sphere with radius = extent around the specified centroid
|
||||
function _isInMembranePlane(testPoint: Vec3, normalVector: Vec3, min: number, max: number): boolean {
|
||||
const d = -v3dot(normalVector, testPoint);
|
||||
return d > min && d < max;
|
||||
}
|
||||
|
||||
/** Generates a defined number of points on a sphere with radius = extent around the specified centroid */
|
||||
function generateSpherePoints(ctx: ANVILContext, numberOfSpherePoints: number): Vec3[] {
|
||||
const { centroid, extent } = ctx;
|
||||
const points = [];
|
||||
let oldPhi = 0, h, theta, phi;
|
||||
for(let k = 1, kl = numberOfSpherePoints + 1; k < kl; k++) {
|
||||
h = -1 + 2 * (k - 1) / (numberOfSpherePoints - 1);
|
||||
h = -1 + 2 * (k - 1) / (2 * numberOfSpherePoints - 1);
|
||||
theta = Math.acos(h);
|
||||
phi = (k === 1 || k === numberOfSpherePoints) ? 0 : (oldPhi + 3.6 / Math.sqrt(numberOfSpherePoints * (1 - h * h))) % (2 * Math.PI);
|
||||
phi = (k === 1 || k === numberOfSpherePoints) ? 0 : (oldPhi + 3.6 / Math.sqrt(2 * numberOfSpherePoints * (1 - h * h))) % (2 * Math.PI);
|
||||
|
||||
const point = Vec3.create(
|
||||
const point = v3create(
|
||||
extent * Math.sin(phi) * Math.sin(theta) + centroid[0],
|
||||
extent * Math.cos(theta) + centroid[1],
|
||||
extent * Math.cos(phi) * Math.sin(theta) + centroid[2]
|
||||
@@ -290,18 +501,18 @@ function generateSpherePoints(ctx: ANVILContext, numberOfSpherePoints: number):
|
||||
return points;
|
||||
}
|
||||
|
||||
// generates sphere points close to that of the initial membrane
|
||||
/** Generates sphere points close to that of the initial membrane */
|
||||
function findProximateAxes(ctx: ANVILContext, membrane: MembraneCandidate): Vec3[] {
|
||||
const { numberOfSpherePoints, extent } = ctx;
|
||||
const points = generateSpherePoints(ctx, 30000);
|
||||
let j = 4;
|
||||
let sphere_pts2: Vec3[] = [];
|
||||
const s = 2 * extent / numberOfSpherePoints;
|
||||
while (sphere_pts2.length < numberOfSpherePoints) {
|
||||
const d = 2 * extent / numberOfSpherePoints + j;
|
||||
const dsq = d * d;
|
||||
const dsq = (s + j) * (s + j);
|
||||
sphere_pts2 = [];
|
||||
for (let i = 0, il = points.length; i < il; i++) {
|
||||
if (Vec3.squaredDistance(points[i], membrane.spherePoint!) < dsq) {
|
||||
if (v3squaredDistance(points[i], membrane.spherePoint!) < dsq) {
|
||||
sphere_pts2.push(points[i]);
|
||||
}
|
||||
}
|
||||
@@ -312,56 +523,80 @@ function findProximateAxes(ctx: ANVILContext, membrane: MembraneCandidate): Vec3
|
||||
|
||||
interface HphobHphil {
|
||||
hphob: number,
|
||||
hphil: number,
|
||||
total: number
|
||||
hphil: number
|
||||
}
|
||||
|
||||
namespace HphobHphil {
|
||||
export function of(hphob: number, hphil: number, total?: number) {
|
||||
return {
|
||||
hphob: hphob,
|
||||
hphil: hphil,
|
||||
total: !!total ? total : hphob + hphil
|
||||
};
|
||||
}
|
||||
|
||||
const testPoint = Vec3();
|
||||
export function filtered(ctx: ANVILContext, label_comp_id: StructureElement.Property<string>, filter?: (test: Vec3) => boolean): HphobHphil {
|
||||
const { offsets, exposed, structure } = ctx;
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const { x, y, z } = StructureProperties.atom;
|
||||
export function initial(ctx: ANVILContext): HphobHphil {
|
||||
const { exposed, hydrophobic } = ctx;
|
||||
let hphob = 0;
|
||||
let hphil = 0;
|
||||
for (let k = 0, kl = offsets.length; k < kl; k++) {
|
||||
// ignore buried residues
|
||||
if (!exposed[k]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setLocation(l, structure, offsets[k]);
|
||||
Vec3.set(testPoint, x(l), y(l), z(l));
|
||||
|
||||
// testPoints have to be in putative membrane layer
|
||||
if (filter && !filter(testPoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isHydrophobic(label_comp_id(l))) {
|
||||
for (let k = 0, kl = exposed.length; k < kl; k++) {
|
||||
if (hydrophobic[k]) {
|
||||
hphob++;
|
||||
} else {
|
||||
hphil++;
|
||||
}
|
||||
}
|
||||
return of(hphob, hphil);
|
||||
return { hphob, hphil };
|
||||
}
|
||||
|
||||
const testPoint = v3zero();
|
||||
export function sliced(ctx: ANVILContext, stepSize: number, spherePoint: Vec3, diam: Vec3, diamNorm: number): HphobHphil[] {
|
||||
const { exposed, hydrophobic, structure } = ctx;
|
||||
const { units, serialMapping } = structure;
|
||||
const { unitIndices, elementIndices } = serialMapping;
|
||||
const sliceStats: HphobHphil[] = [];
|
||||
for (let i = 0, il = diamNorm - stepSize; i < il; i += stepSize) {
|
||||
sliceStats[sliceStats.length] = { hphob: 0, hphil: 0 };
|
||||
}
|
||||
|
||||
for (let i = 0, il = exposed.length; i < il; i++) {
|
||||
const unit = units[unitIndices[exposed[i]]];
|
||||
const elementIndex = elementIndices[exposed[i]];
|
||||
v3set(testPoint, unit.conformation.x(elementIndex), unit.conformation.y(elementIndex), unit.conformation.z(elementIndex));
|
||||
v3sub(testPoint, testPoint, spherePoint);
|
||||
if (hydrophobic[i]) {
|
||||
sliceStats[Math.floor(v3dot(testPoint, diam) / diamNorm / stepSize)].hphob++;
|
||||
} else {
|
||||
sliceStats[Math.floor(v3dot(testPoint, diam) / diamNorm / stepSize)].hphil++;
|
||||
}
|
||||
}
|
||||
return sliceStats;
|
||||
}
|
||||
}
|
||||
|
||||
// ANVIL-specific (not general) definition of membrane-favoring amino acids
|
||||
const HYDROPHOBIC_AMINO_ACIDS = new Set(['ALA', 'CYS', 'GLY', 'HIS', 'ILE', 'LEU', 'MET', 'PHE', 'SER', 'THR', 'VAL']);
|
||||
/** ANVIL-specific (not general) definition of membrane-favoring amino acids */
|
||||
const HYDROPHOBIC_AMINO_ACIDS = new Set(['ALA', 'CYS', 'GLY', 'HIS', 'ILE', 'LEU', 'MET', 'PHE', 'SER', 'TRP', 'VAL']);
|
||||
/** Returns true if ANVIL considers this as amino acid that favors being embedded in a membrane */
|
||||
export function isHydrophobic(label_comp_id: string): boolean {
|
||||
return HYDROPHOBIC_AMINO_ACIDS.has(label_comp_id);
|
||||
}
|
||||
|
||||
/** Accessible surface area used for normalization. ANVIL uses 'Total-Side REL' values from NACCESS, from: Hubbard, S. J., & Thornton, J. M. (1993). naccess. Computer Program, Department of Biochemistry and Molecular Biology, University College London, 2(1). */
|
||||
export const MaxAsa: { [k: string]: number } = {
|
||||
'ALA': 69.41,
|
||||
'ARG': 201.25,
|
||||
'ASN': 106.24,
|
||||
'ASP': 102.69,
|
||||
'CYS': 96.75,
|
||||
'GLU': 134.74,
|
||||
'GLN': 140.99,
|
||||
'GLY': 32.33,
|
||||
'HIS': 147.08,
|
||||
'ILE': 137.96,
|
||||
'LEU': 141.12,
|
||||
'LYS': 163.30,
|
||||
'MET': 156.64,
|
||||
'PHE': 164.11,
|
||||
'PRO': 119.90,
|
||||
'SER': 78.11,
|
||||
'THR': 101.70,
|
||||
'TRP': 211.26,
|
||||
'TYR': 177.38,
|
||||
'VAL': 114.28
|
||||
};
|
||||
|
||||
function setLocation(l: StructureElement.Location, structure: Structure, serialIndex: number) {
|
||||
l.structure = structure;
|
||||
l.unit = structure.units[structure.serialMapping.unitIndices[serialIndex]];
|
||||
|
||||
@@ -11,7 +11,6 @@ import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { ANVILParams, ANVILProps, computeANVIL, isInMembranePlane } from './algorithm';
|
||||
import { CustomStructureProperty } from '../../mol-model-props/common/custom-structure-property';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
import { AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { QuerySymbolRuntime } from '../../mol-script/runtime/query/base';
|
||||
import { CustomPropSymbol } from '../../mol-script/language/symbol';
|
||||
@@ -76,7 +75,6 @@ export const MembraneOrientationProvider: CustomStructureProperty.Provider<Membr
|
||||
});
|
||||
|
||||
async function computeAnvil(ctx: CustomProperty.Context, data: Structure, props: Partial<ANVILProps>): Promise<MembraneOrientation> {
|
||||
await AccessibleSurfaceAreaProvider.attach(ctx, data);
|
||||
const p = { ...PD.getDefaultValues(ANVILParams), ...props };
|
||||
return await computeANVIL(data, p).runInContext(ctx.runtime);
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../mol-repr/representation';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
|
||||
import { StructureRepresentationProvider, StructureRepresentation, StructureRepresentationStateBuilder } from '../../mol-repr/structure/representation';
|
||||
import { MembraneOrientation } from './prop';
|
||||
import { ThemeRegistryContext } from '../../mol-theme/theme';
|
||||
@@ -30,18 +29,9 @@ import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
|
||||
const SharedParams = {
|
||||
color: PD.Color(ColorNames.lightgrey),
|
||||
radiusFactor: PD.Numeric(0.8333, { min: 0.1, max: 3.0, step: 0.01 }, { description: 'Scale the radius of the membrane layer' })
|
||||
radiusFactor: PD.Numeric(1.2, { min: 0.1, max: 3.0, step: 0.01 }, { description: 'Scale the radius of the membrane layer' })
|
||||
};
|
||||
|
||||
const BilayerSpheresParams = {
|
||||
...Spheres.Params,
|
||||
...SharedParams,
|
||||
sphereSize: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }, { description: 'Size of spheres that represent membrane planes' }),
|
||||
density: PD.Numeric(1, { min: 0.25, max: 10, step: 0.25 }, { description: 'Distance between spheres'})
|
||||
};
|
||||
export type BilayerSpheresParams = typeof BilayerSpheresParams
|
||||
export type BilayerSpheresProps = PD.Values<BilayerSpheresParams>
|
||||
|
||||
const BilayerPlanesParams = {
|
||||
...Mesh.Params,
|
||||
...SharedParams,
|
||||
@@ -66,7 +56,6 @@ const MembraneOrientationVisuals = {
|
||||
};
|
||||
|
||||
export const MembraneOrientationParams = {
|
||||
...BilayerSpheresParams,
|
||||
...BilayerPlanesParams,
|
||||
...BilayerRimsParams,
|
||||
visuals: PD.MultiSelect(['bilayer-planes', 'bilayer-rims'], PD.objectToOptions(MembraneOrientationVisuals)),
|
||||
@@ -104,16 +93,16 @@ function membraneLabel(data: Structure) {
|
||||
}
|
||||
|
||||
function getBilayerRims(ctx: RuntimeContext, data: Structure, props: BilayerRimsProps, shape?: Shape<Lines>): Shape<Lines> {
|
||||
const { planePoint1: p1, planePoint2: p2, centroid, normalVector: normal, radius } = MembraneOrientationProvider.get(data).value!;
|
||||
const { planePoint1: p1, planePoint2: p2, centroid, radius } = MembraneOrientationProvider.get(data).value!;
|
||||
const scaledRadius = props.radiusFactor * radius;
|
||||
const builder = LinesBuilder.create(128, 64, shape?.geometry);
|
||||
getLayerCircle(builder, p1, centroid, normal, scaledRadius, props);
|
||||
getLayerCircle(builder, p2, centroid, normal, scaledRadius, props);
|
||||
getLayerCircle(builder, p1, centroid, scaledRadius, props);
|
||||
getLayerCircle(builder, p2, centroid, scaledRadius, props);
|
||||
return Shape.create('Bilayer rims', data, builder.getLines(), () => props.color, () => props.linesSize, () => membraneLabel(data));
|
||||
}
|
||||
|
||||
function getLayerCircle(builder: LinesBuilder, p: Vec3, centroid: Vec3, normal: Vec3, radius: number, props: BilayerRimsProps, shape?: Shape<Lines>) {
|
||||
const circle = getCircle(p, centroid, normal, radius);
|
||||
function getLayerCircle(builder: LinesBuilder, p: Vec3, centroid: Vec3, radius: number, props: BilayerRimsProps, shape?: Shape<Lines>) {
|
||||
const circle = getCircle(p, centroid, radius);
|
||||
const { indices, vertices } = circle;
|
||||
for (let j = 0, jl = indices.length; j < jl; j += 3) {
|
||||
if (props.dashedLines && j % 2 === 1) continue; // draw every other segment to get dashes
|
||||
@@ -130,8 +119,13 @@ function getLayerCircle(builder: LinesBuilder, p: Vec3, centroid: Vec3, normal:
|
||||
}
|
||||
|
||||
const tmpMat = Mat4();
|
||||
function getCircle(p: Vec3, centroid: Vec3, normal: Vec3, radius: number) {
|
||||
Mat4.targetTo(tmpMat, p, centroid, normal);
|
||||
const tmpV = Vec3();
|
||||
function getCircle(p: Vec3, centroid: Vec3, radius: number) {
|
||||
if (Vec3.dot(Vec3.unitY, Vec3.sub(tmpV, p, centroid)) === 0) {
|
||||
Mat4.targetTo(tmpMat, p, centroid, Vec3.unitY);
|
||||
} else {
|
||||
Mat4.targetTo(tmpMat, p, centroid, Vec3.unitX);
|
||||
}
|
||||
Mat4.setTranslation(tmpMat, p);
|
||||
Mat4.mul(tmpMat, tmpMat, Mat4.rotX90);
|
||||
|
||||
@@ -140,16 +134,16 @@ function getCircle(p: Vec3, centroid: Vec3, normal: Vec3, radius: number) {
|
||||
}
|
||||
|
||||
function getBilayerPlanes(ctx: RuntimeContext, data: Structure, props: BilayerPlanesProps, shape?: Shape<Mesh>): Shape<Mesh> {
|
||||
const { planePoint1: p1, planePoint2: p2, centroid, normalVector: normal, radius } = MembraneOrientationProvider.get(data).value!;
|
||||
const { planePoint1: p1, planePoint2: p2, centroid, radius } = MembraneOrientationProvider.get(data).value!;
|
||||
const state = MeshBuilder.createState(128, 64, shape && shape.geometry);
|
||||
const scaledRadius = props.radiusFactor * radius;
|
||||
getLayerPlane(state, p1, centroid, normal, scaledRadius);
|
||||
getLayerPlane(state, p2, centroid, normal, scaledRadius);
|
||||
getLayerPlane(state, p1, centroid, scaledRadius);
|
||||
getLayerPlane(state, p2, centroid, scaledRadius);
|
||||
return Shape.create('Bilayer planes', data, MeshBuilder.getMesh(state), () => props.color, () => 1, () => membraneLabel(data));
|
||||
}
|
||||
|
||||
function getLayerPlane(state: MeshBuilder.State, p: Vec3, centroid: Vec3, normal: Vec3, radius: number) {
|
||||
const circle = getCircle(p, centroid, normal, radius);
|
||||
function getLayerPlane(state: MeshBuilder.State, p: Vec3, centroid: Vec3, radius: number) {
|
||||
const circle = getCircle(p, centroid, radius);
|
||||
state.currentGroup = 0;
|
||||
MeshBuilder.addPrimitive(state, Mat4.id, circle);
|
||||
MeshBuilder.addPrimitiveFlipped(state, Mat4.id, circle);
|
||||
|
||||
@@ -181,6 +181,6 @@ export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramid
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ConfalPyramidsProvider.descriptor, false)
|
||||
detach: (data) => data.structure && ConfalPyramidsProvider.ref(data.structure.models[0], false)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,10 +20,10 @@ import { Structure, StructureProperties, Unit } from '../../../mol-model/structu
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
|
||||
import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
|
||||
import { StructureGroup, UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
|
||||
import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { getAltResidueLociFromId } from '../../../mol-repr/structure/visual/util/common';
|
||||
import { getAltResidueLociFromId, StructureGroup } from '../../../mol-repr/structure/visual/util/common';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
|
||||
import { NullLocation } from '../../../mol-model/location';
|
||||
|
||||
@@ -4,17 +4,35 @@
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { getStyle } from '../../mol-gl/renderer';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { PluginComponent } from '../../mol-plugin-state/component';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ObjExporter } from './export';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateSelection } from '../../mol-state';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { SetUtils } from '../../mol-util/set';
|
||||
import { zip } from '../../mol-util/zip/zip';
|
||||
import { GlbExporter } from './glb-exporter';
|
||||
import { ObjExporter } from './obj-exporter';
|
||||
import { StlExporter } from './stl-exporter';
|
||||
import { UsdzExporter } from './usdz-exporter';
|
||||
|
||||
export const GeometryParams = {
|
||||
format: PD.Select('glb', [
|
||||
['glb', 'glTF 2.0 Binary (.glb)'],
|
||||
['stl', 'Stl (.stl)'],
|
||||
['obj', 'Wavefront (.obj)'],
|
||||
['usdz', 'Universal Scene Description (.usdz)']
|
||||
])
|
||||
};
|
||||
|
||||
export class GeometryControls extends PluginComponent {
|
||||
getFilename() {
|
||||
readonly behaviors = {
|
||||
params: this.ev.behavior<PD.Values<typeof GeometryParams>>(PD.getDefaultValues(GeometryParams))
|
||||
}
|
||||
|
||||
private getFilename() {
|
||||
const models = this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Model)).map(s => s.obj!.data);
|
||||
const uniqueIds = new Set<string>();
|
||||
models.forEach(m => uniqueIds.add(m.entryId.toUpperCase()));
|
||||
@@ -22,37 +40,41 @@ export class GeometryControls extends PluginComponent {
|
||||
return `${idString || 'molstar-model'}`;
|
||||
}
|
||||
|
||||
exportObj() {
|
||||
const task = Task.create('Export OBJ', async ctx => {
|
||||
exportGeometry() {
|
||||
const task = Task.create('Export Geometry', async ctx => {
|
||||
try {
|
||||
const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
|
||||
|
||||
const filename = this.getFilename();
|
||||
const objExporter = new ObjExporter(filename);
|
||||
|
||||
const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
|
||||
const boundingSphere = this.plugin.canvas3d?.boundingSphereVisible!;
|
||||
const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
|
||||
let renderObjectExporter: GlbExporter | ObjExporter | StlExporter | UsdzExporter;
|
||||
switch (this.behaviors.params.value.format) {
|
||||
case 'glb':
|
||||
renderObjectExporter = new GlbExporter(style, boundingBox);
|
||||
break;
|
||||
case 'obj':
|
||||
renderObjectExporter = new ObjExporter(filename, boundingBox);
|
||||
break;
|
||||
case 'stl':
|
||||
renderObjectExporter = new StlExporter(boundingBox);
|
||||
break;
|
||||
case 'usdz':
|
||||
renderObjectExporter = new UsdzExporter(style, boundingBox, boundingSphere.radius);
|
||||
break;
|
||||
default: throw new Error('Unsupported format.');
|
||||
}
|
||||
|
||||
for (let i = 0, il = renderObjects.length; i < il; ++i) {
|
||||
await ctx.update({ message: `Exporting object ${i}/${il}` });
|
||||
await objExporter.add(renderObjects[i], ctx);
|
||||
await renderObjectExporter.add(renderObjects[i], this.plugin.canvas3d?.webgl!, ctx);
|
||||
}
|
||||
const { obj, mtl } = objExporter.getData();
|
||||
|
||||
const asciiWrite = (data: Uint8Array, str: string) => {
|
||||
for (let i = 0, il = str.length; i < il; ++i) {
|
||||
data[i] = str.charCodeAt(i);
|
||||
}
|
||||
};
|
||||
const objData = new Uint8Array(obj.length);
|
||||
asciiWrite(objData, obj);
|
||||
const mtlData = new Uint8Array(mtl.length);
|
||||
asciiWrite(mtlData, mtl);
|
||||
|
||||
const zipDataObj = {
|
||||
[filename + '.obj']: objData,
|
||||
[filename + '.mtl']: mtlData
|
||||
};
|
||||
const zipData = await zip(ctx, zipDataObj);
|
||||
const blob = await renderObjectExporter.getBlob(ctx);
|
||||
return {
|
||||
zipData,
|
||||
filename: filename + '.zip'
|
||||
blob,
|
||||
filename: filename + '.' + renderObjectExporter.fileExtension
|
||||
};
|
||||
} catch (e) {
|
||||
this.plugin.log.error('' + e);
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { MeshValues } from '../../mol-gl/renderable/mesh';
|
||||
import { LinesValues } from '../../mol-gl/renderable/lines';
|
||||
import { PointsValues } from '../../mol-gl/renderable/points';
|
||||
import { SpheresValues } from '../../mol-gl/renderable/spheres';
|
||||
import { CylindersValues } from '../../mol-gl/renderable/cylinders';
|
||||
import { BaseValues, SizeValues } from '../../mol-gl/renderable/schema';
|
||||
import { TextureImage } from '../../mol-gl/renderable/util';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { StringBuilder } from '../../mol-util';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { decodeFloatRGB } from '../../mol-util/float-packing';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3transformMat4 = Vec3.transformMat4;
|
||||
const v3transformMat3 = Vec3.transformMat3;
|
||||
const mat3directionTransform = Mat3.directionTransform;
|
||||
|
||||
type RenderObjectExportData = {
|
||||
[k: string]: string | Uint8Array | undefined
|
||||
}
|
||||
|
||||
interface RenderObjectExporter<D extends RenderObjectExportData> {
|
||||
add(renderObject: GraphicsRenderObject, ctx: RuntimeContext): Promise<void> | undefined
|
||||
getData(): D
|
||||
}
|
||||
|
||||
// http://paulbourke.net/dataformats/obj/
|
||||
// http://paulbourke.net/dataformats/mtl/
|
||||
|
||||
export type ObjData = {
|
||||
obj: string
|
||||
mtl: string
|
||||
}
|
||||
|
||||
export class ObjExporter implements RenderObjectExporter<ObjData> {
|
||||
private obj = StringBuilder.create();
|
||||
private mtl = StringBuilder.create();
|
||||
private vertexOffset = 0;
|
||||
private currentColor: Color | undefined;
|
||||
private currentAlpha: number | undefined;
|
||||
private materialSet = new Set<string>();
|
||||
|
||||
private static getSizeFromTexture(tSize: TextureImage<Uint8Array>, i: number): number {
|
||||
const r = tSize.array[i * 3];
|
||||
const g = tSize.array[i * 3 + 1];
|
||||
const b = tSize.array[i * 3 + 2];
|
||||
return decodeFloatRGB(r, g, b);
|
||||
}
|
||||
|
||||
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
|
||||
const tSize = values.tSize.ref.value;
|
||||
let size = 0;
|
||||
switch (values.dSizeType.ref.value) {
|
||||
case 'uniform':
|
||||
size = values.uSize.ref.value;
|
||||
break;
|
||||
case 'instance':
|
||||
size = ObjExporter.getSizeFromTexture(tSize, instanceIndex) / 100;
|
||||
break;
|
||||
case 'group':
|
||||
size = ObjExporter.getSizeFromTexture(tSize, group) / 100;
|
||||
break;
|
||||
case 'groupInstance':
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
size = ObjExporter.getSizeFromTexture(tSize, instanceIndex * groupCount + group) / 100;
|
||||
break;
|
||||
}
|
||||
return size * values.uSizeFactor.ref.value;
|
||||
}
|
||||
|
||||
private updateMaterial(color: Color, alpha: number) {
|
||||
if (this.currentColor === color && this.currentAlpha === alpha) return;
|
||||
|
||||
this.currentColor = color;
|
||||
this.currentAlpha = alpha;
|
||||
const material = Color.toHexString(color) + alpha;
|
||||
StringBuilder.writeSafe(this.obj, `usemtl ${material}`);
|
||||
StringBuilder.newline(this.obj);
|
||||
if (!this.materialSet.has(material)) {
|
||||
this.materialSet.add(material);
|
||||
const [r, g, b] = Color.toRgbNormalized(color);
|
||||
const mtl = this.mtl;
|
||||
StringBuilder.writeSafe(mtl, `newmtl ${material}\n`);
|
||||
StringBuilder.writeSafe(mtl, 'illum 2\n'); // illumination model
|
||||
StringBuilder.writeSafe(mtl, 'Ns 163\n'); // specular exponent
|
||||
StringBuilder.writeSafe(mtl, 'Ni 0.001\n'); // optical density a.k.a. index of refraction
|
||||
StringBuilder.writeSafe(mtl, 'Ka 0 0 0\n'); // ambient reflectivity
|
||||
StringBuilder.writeSafe(mtl, 'Kd '); // diffuse reflectivity
|
||||
StringBuilder.writeFloat(mtl, r, 1000);
|
||||
StringBuilder.whitespace1(mtl);
|
||||
StringBuilder.writeFloat(mtl, g, 1000);
|
||||
StringBuilder.whitespace1(mtl);
|
||||
StringBuilder.writeFloat(mtl, b, 1000);
|
||||
StringBuilder.newline(mtl);
|
||||
StringBuilder.writeSafe(mtl, 'Ks 0.25 0.25 0.25\n'); // specular reflectivity
|
||||
StringBuilder.writeSafe(mtl, 'd '); // dissolve
|
||||
StringBuilder.writeFloat(mtl, alpha, 1000);
|
||||
StringBuilder.newline(mtl);
|
||||
}
|
||||
}
|
||||
|
||||
private async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array, groups: Float32Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, ctx: RuntimeContext) {
|
||||
const obj = this.obj;
|
||||
const t = Mat4();
|
||||
const n = Mat3();
|
||||
const tmpV = Vec3();
|
||||
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const tColor = values.tColor.ref.value.array;
|
||||
const uAlpha = values.uAlpha.ref.value;
|
||||
const aTransform = values.aTransform.ref.value;
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
mat3directionTransform(n, t);
|
||||
|
||||
const currentProgress = (vertexCount * 2 + drawCount) * instanceIndex;
|
||||
await ctx.update({ isIndeterminate: false, current: currentProgress, max: (vertexCount * 2 + drawCount) * values.uInstanceCount.ref.value });
|
||||
|
||||
// position
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + i });
|
||||
v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * 3), t);
|
||||
StringBuilder.writeSafe(obj, 'v ');
|
||||
StringBuilder.writeFloat(obj, tmpV[0], 1000);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[1], 1000);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[2], 1000);
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount + i });
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * 3), n);
|
||||
StringBuilder.writeSafe(obj, 'vn ');
|
||||
StringBuilder.writeFloat(obj, tmpV[0], 100);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[1], 100);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[2], 100);
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
// face
|
||||
for (let i = 0; i < drawCount; i += 3) {
|
||||
if (i % 3000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount * 2 + i });
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
|
||||
break;
|
||||
case 'instance':
|
||||
color = Color.fromArray(tColor, instanceIndex * 3);
|
||||
break;
|
||||
case 'group':
|
||||
color = Color.fromArray(tColor, groups[indices[i]] * 3);
|
||||
break;
|
||||
case 'groupInstance':
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const group = groups[indices[i]];
|
||||
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
|
||||
break;
|
||||
case 'vertex':
|
||||
color = Color.fromArray(tColor, i * 3);
|
||||
break;
|
||||
case 'vertexInstance':
|
||||
color = Color.fromArray(tColor, (instanceIndex * drawCount + i) * 3);
|
||||
break;
|
||||
default: throw new Error('Unsupported color type.');
|
||||
}
|
||||
this.updateMaterial(color, uAlpha);
|
||||
|
||||
const v1 = this.vertexOffset + indices[i] + 1;
|
||||
const v2 = this.vertexOffset + indices[i + 1] + 1;
|
||||
const v3 = this.vertexOffset + indices[i + 2] + 1;
|
||||
StringBuilder.writeSafe(obj, 'f ');
|
||||
StringBuilder.writeInteger(obj, v1);
|
||||
StringBuilder.writeSafe(obj, '//');
|
||||
StringBuilder.writeIntegerAndSpace(obj, v1);
|
||||
StringBuilder.writeInteger(obj, v2);
|
||||
StringBuilder.writeSafe(obj, '//');
|
||||
StringBuilder.writeIntegerAndSpace(obj, v2);
|
||||
StringBuilder.writeInteger(obj, v3);
|
||||
StringBuilder.writeSafe(obj, '//');
|
||||
StringBuilder.writeInteger(obj, v3);
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
this.vertexOffset += vertexCount;
|
||||
}
|
||||
|
||||
private async addMesh(values: MeshValues, ctx: RuntimeContext) {
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aNormal = values.aNormal.ref.value;
|
||||
const elements = values.elements.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const drawCount = values.drawCount.ref.value;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
await this.addMeshWithColors(aPosition, aNormal, elements, aGroup, vertexCount, drawCount, values, instanceIndex, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private async addPoints(values: PointsValues, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private async addSpheres(values: SpheresValues, ctx: RuntimeContext) {
|
||||
const center = Vec3();
|
||||
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; i += 4) {
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = ObjExporter.getSize(values, instanceIndex, group);
|
||||
state.currentGroup = group;
|
||||
addSphere(state, center, radius, 2);
|
||||
}
|
||||
|
||||
const mesh = MeshBuilder.getMesh(state);
|
||||
const vertices = mesh.vertexBuffer.ref.value;
|
||||
const normals = mesh.normalBuffer.ref.value;
|
||||
const indices = mesh.indexBuffer.ref.value;
|
||||
const groups = mesh.groupBuffer.ref.value;
|
||||
await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private async addCylinders(values: CylindersValues, ctx: RuntimeContext) {
|
||||
const start = Vec3();
|
||||
const end = Vec3();
|
||||
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aScale = values.aScale.ref.value;
|
||||
const aCap = values.aCap.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; i += 6) {
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = ObjExporter.getSize(values, instanceIndex, group) * aScale[i];
|
||||
const cap = aCap[i];
|
||||
const topCap = cap === 1 || cap === 3;
|
||||
const bottomCap = cap >= 2;
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments: 32 };
|
||||
state.currentGroup = aGroup[i];
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
}
|
||||
|
||||
const mesh = MeshBuilder.getMesh(state);
|
||||
const vertices = mesh.vertexBuffer.ref.value;
|
||||
const normals = mesh.normalBuffer.ref.value;
|
||||
const indices = mesh.indexBuffer.ref.value;
|
||||
const groups = mesh.groupBuffer.ref.value;
|
||||
await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
add(renderObject: GraphicsRenderObject, ctx: RuntimeContext) {
|
||||
if (!renderObject.state.visible) return;
|
||||
|
||||
switch (renderObject.type) {
|
||||
case 'mesh':
|
||||
return this.addMesh(renderObject.values as MeshValues, ctx);
|
||||
case 'lines':
|
||||
return this.addLines(renderObject.values as LinesValues, ctx);
|
||||
case 'points':
|
||||
return this.addPoints(renderObject.values as PointsValues, ctx);
|
||||
case 'spheres':
|
||||
return this.addSpheres(renderObject.values as SpheresValues, ctx);
|
||||
case 'cylinders':
|
||||
return this.addCylinders(renderObject.values as CylindersValues, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
getData() {
|
||||
return {
|
||||
obj: StringBuilder.getString(this.obj),
|
||||
mtl: StringBuilder.getString(this.mtl)
|
||||
};
|
||||
}
|
||||
|
||||
constructor(filename: string) {
|
||||
StringBuilder.writeSafe(this.obj, `mtllib ${filename}.mtl\n`);
|
||||
}
|
||||
}
|
||||
347
src/extensions/geo-export/glb-exporter.ts
Normal file
347
src/extensions/geo-export/glb-exporter.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { BaseValues } from '../../mol-gl/renderable/schema';
|
||||
import { Style } from '../../mol-gl/renderer';
|
||||
import { asciiWrite } from '../../mol-io/common/ascii';
|
||||
import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { PLUGIN_VERSION } from '../../mol-plugin/version';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { fillSerial } from '../../mol-util/array';
|
||||
import { NumberArray } from '../../mol-util/type-helpers';
|
||||
import { MeshExporter, AddMeshInput } from './mesh-exporter';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3normalize = Vec3.normalize;
|
||||
const v3toArray = Vec3.toArray;
|
||||
|
||||
// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0
|
||||
|
||||
const UNSIGNED_BYTE = 5121;
|
||||
const UNSIGNED_INT = 5125;
|
||||
const FLOAT = 5126;
|
||||
const ARRAY_BUFFER = 34962;
|
||||
const ELEMENT_ARRAY_BUFFER = 34963;
|
||||
|
||||
export type GlbData = {
|
||||
glb: Uint8Array
|
||||
}
|
||||
|
||||
export class GlbExporter extends MeshExporter<GlbData> {
|
||||
readonly fileExtension = 'glb';
|
||||
private nodes: Record<string, any>[] = [];
|
||||
private meshes: Record<string, any>[] = [];
|
||||
private accessors: Record<string, any>[] = [];
|
||||
private bufferViews: Record<string, any>[] = [];
|
||||
private binaryBuffer: ArrayBuffer[] = [];
|
||||
private byteOffset = 0;
|
||||
private centerTransform: Mat4;
|
||||
|
||||
private static vec3MinMax(a: NumberArray) {
|
||||
const min: number[] = [Infinity, Infinity, Infinity];
|
||||
const max: number[] = [-Infinity, -Infinity, -Infinity];
|
||||
for (let i = 0, il = a.length; i < il; i += 3) {
|
||||
for (let j = 0; j < 3; ++j) {
|
||||
min[j] = Math.min(a[i + j], min[j]);
|
||||
max[j] = Math.max(a[i + j], max[j]);
|
||||
}
|
||||
}
|
||||
return [ min, max ];
|
||||
}
|
||||
|
||||
private addBuffer(buffer: ArrayBuffer, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
|
||||
this.binaryBuffer.push(buffer);
|
||||
|
||||
const bufferViewOffset = this.bufferViews.length;
|
||||
this.bufferViews.push({
|
||||
buffer: 0,
|
||||
byteOffset: this.byteOffset,
|
||||
byteLength: buffer.byteLength,
|
||||
target
|
||||
});
|
||||
this.byteOffset += buffer.byteLength;
|
||||
|
||||
const accessorOffset = this.accessors.length;
|
||||
this.accessors.push({
|
||||
bufferView: bufferViewOffset,
|
||||
byteOffset: 0,
|
||||
componentType,
|
||||
count,
|
||||
type,
|
||||
min,
|
||||
max,
|
||||
normalized
|
||||
});
|
||||
return accessorOffset;
|
||||
}
|
||||
|
||||
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
|
||||
const tmpV = Vec3();
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
|
||||
const vertexArray = new Float32Array(vertexCount * 3);
|
||||
const normalArray = new Float32Array(vertexCount * 3);
|
||||
let indexArray: Uint32Array | undefined;
|
||||
|
||||
// position
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(tmpV, vertices, i * stride);
|
||||
v3toArray(tmpV, vertexArray, i * 3);
|
||||
}
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(tmpV, normals, i * stride);
|
||||
v3normalize(tmpV, tmpV);
|
||||
v3toArray(tmpV, normalArray, i * 3);
|
||||
}
|
||||
|
||||
// face
|
||||
if (!isGeoTexture) {
|
||||
indexArray = indices!.slice(0, drawCount);
|
||||
}
|
||||
|
||||
const [ vertexMin, vertexMax ] = GlbExporter.vec3MinMax(vertexArray);
|
||||
|
||||
let vertexBuffer = vertexArray.buffer;
|
||||
let normalBuffer = normalArray.buffer;
|
||||
let indexBuffer = isGeoTexture ? undefined : indexArray!.buffer;
|
||||
if (!IsNativeEndianLittle) {
|
||||
vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
|
||||
normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
|
||||
if (!isGeoTexture) indexBuffer = flipByteOrder(new Uint8Array(indexBuffer!), 4);
|
||||
}
|
||||
|
||||
return {
|
||||
vertexAccessorIndex: this.addBuffer(vertexBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER, vertexMin, vertexMax),
|
||||
normalAccessorIndex: this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER),
|
||||
indexAccessorIndex: isGeoTexture ? undefined : this.addBuffer(indexBuffer!, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
|
||||
};
|
||||
}
|
||||
|
||||
private addColorBuffer(values: BaseValues, groups: Float32Array | Uint8Array, vertexCount: number, instanceIndex: number, isGeoTexture: boolean, interpolatedColors: Uint8Array) {
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const uColor = values.uColor.ref.value;
|
||||
const tColor = values.tColor.ref.value.array;
|
||||
const uAlpha = values.uAlpha.ref.value;
|
||||
const dTransparency = values.dTransparency.ref.value;
|
||||
const tTransparency = values.tTransparency.ref.value;
|
||||
|
||||
const colorArray = new Uint8Array(vertexCount * 4);
|
||||
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
color = Color.fromNormalizedArray(uColor, 0);
|
||||
break;
|
||||
case 'instance':
|
||||
color = Color.fromArray(tColor, instanceIndex * 3);
|
||||
break;
|
||||
case 'group': {
|
||||
const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
|
||||
color = Color.fromArray(tColor, group * 3);
|
||||
break;
|
||||
}
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
|
||||
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
|
||||
break;
|
||||
}
|
||||
case 'vertex':
|
||||
color = Color.fromArray(tColor, i * 3);
|
||||
break;
|
||||
case 'vertexInstance':
|
||||
color = Color.fromArray(tColor, (instanceIndex * vertexCount + i) * 3);
|
||||
break;
|
||||
case 'volume':
|
||||
color = Color.fromArray(interpolatedColors!, i * 3);
|
||||
break;
|
||||
case 'volumeInstance':
|
||||
color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + i) * 3);
|
||||
break;
|
||||
default: throw new Error('Unsupported color type.');
|
||||
}
|
||||
|
||||
let alpha = uAlpha;
|
||||
if (dTransparency) {
|
||||
const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
|
||||
const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
|
||||
alpha *= 1 - transparency;
|
||||
}
|
||||
|
||||
color = Color.sRGBToLinear(color);
|
||||
Color.toArray(color, colorArray, i * 4);
|
||||
colorArray[i * 4 + 3] = Math.round(alpha * 255);
|
||||
}
|
||||
|
||||
let colorBuffer = colorArray.buffer;
|
||||
if (!IsNativeEndianLittle) {
|
||||
colorBuffer = flipByteOrder(new Uint8Array(colorBuffer), 4);
|
||||
}
|
||||
|
||||
return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
|
||||
const t = Mat4();
|
||||
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const dTransparency = values.dTransparency.ref.value;
|
||||
const aTransform = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
let interpolatedColors: Uint8Array;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedColors = GlbExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
|
||||
}
|
||||
|
||||
// instancing
|
||||
const sameGeometryBuffers = mesh !== undefined;
|
||||
const sameColorBuffer = sameGeometryBuffers && colorType !== 'instance' && !colorType.endsWith('Instance') && !dTransparency;
|
||||
let vertexAccessorIndex: number;
|
||||
let normalAccessorIndex: number;
|
||||
let indexAccessorIndex: number | undefined;
|
||||
let colorAccessorIndex: number;
|
||||
let meshIndex: number;
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
|
||||
|
||||
// create a glTF mesh if needed
|
||||
if (instanceIndex === 0 || !sameGeometryBuffers || !sameColorBuffer) {
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = GlbExporter.getInstance(input, instanceIndex);
|
||||
|
||||
// create geometry buffers if needed
|
||||
if (instanceIndex === 0 || !sameGeometryBuffers) {
|
||||
const accessorIndices = this.addGeometryBuffers(vertices, normals, indices, vertexCount, drawCount, isGeoTexture);
|
||||
vertexAccessorIndex = accessorIndices.vertexAccessorIndex;
|
||||
normalAccessorIndex = accessorIndices.normalAccessorIndex;
|
||||
indexAccessorIndex = accessorIndices.indexAccessorIndex;
|
||||
}
|
||||
|
||||
// create a color buffer if needed
|
||||
if (instanceIndex === 0 || !sameColorBuffer) {
|
||||
colorAccessorIndex = this.addColorBuffer(values, groups, vertexCount, instanceIndex, isGeoTexture, interpolatedColors!);
|
||||
}
|
||||
|
||||
// glTF mesh
|
||||
meshIndex = this.meshes.length;
|
||||
this.meshes.push({
|
||||
primitives: [{
|
||||
attributes: {
|
||||
POSITION: vertexAccessorIndex!,
|
||||
NORMAL: normalAccessorIndex!,
|
||||
COLOR_0: colorAccessorIndex!
|
||||
},
|
||||
indices: indexAccessorIndex,
|
||||
material: 0
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
// node
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
const node: Record<string, any> = {
|
||||
mesh: meshIndex!,
|
||||
matrix: t.slice()
|
||||
};
|
||||
this.nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const binaryBufferLength = this.byteOffset;
|
||||
|
||||
const gltf = {
|
||||
asset: {
|
||||
version: '2.0',
|
||||
generator: `Mol* ${PLUGIN_VERSION}`
|
||||
},
|
||||
scenes: [{
|
||||
nodes: fillSerial(new Array(this.nodes.length) as number[])
|
||||
}],
|
||||
nodes: this.nodes,
|
||||
meshes: this.meshes,
|
||||
buffers: [{
|
||||
byteLength: binaryBufferLength,
|
||||
}],
|
||||
bufferViews: this.bufferViews,
|
||||
accessors: this.accessors,
|
||||
materials: [{
|
||||
pbrMetallicRoughness: {
|
||||
baseColorFactor: [1, 1, 1, 1],
|
||||
metallicFactor: this.style.metalness,
|
||||
roughnessFactor: this.style.roughness
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
const createChunk = (chunkType: number, data: ArrayBuffer[], byteLength: number, padChar: number): [ArrayBuffer[], number] => {
|
||||
let padding = null;
|
||||
if (byteLength % 4 !== 0) {
|
||||
const pad = 4 - (byteLength % 4);
|
||||
byteLength += pad;
|
||||
padding = new Uint8Array(pad);
|
||||
padding.fill(padChar);
|
||||
}
|
||||
const preamble = new ArrayBuffer(8);
|
||||
const preambleDataView = new DataView(preamble);
|
||||
preambleDataView.setUint32(0, byteLength, true);
|
||||
preambleDataView.setUint32(4, chunkType, true);
|
||||
const chunk = [preamble, ...data];
|
||||
if (padding) {
|
||||
chunk.push(padding.buffer);
|
||||
}
|
||||
return [ chunk, 8 + byteLength ];
|
||||
};
|
||||
const jsonString = JSON.stringify(gltf);
|
||||
const jsonBuffer = new Uint8Array(jsonString.length);
|
||||
asciiWrite(jsonBuffer, jsonString);
|
||||
|
||||
const [ jsonChunk, jsonChunkLength ] = createChunk(0x4E4F534A, [jsonBuffer.buffer], jsonBuffer.length, 0x20);
|
||||
const [ binaryChunk, binaryChunkLength ] = createChunk(0x004E4942, this.binaryBuffer, binaryBufferLength, 0x00);
|
||||
|
||||
const glbBufferLength = 12 + jsonChunkLength + binaryChunkLength;
|
||||
const header = new ArrayBuffer(12);
|
||||
const headerDataView = new DataView(header);
|
||||
headerDataView.setUint32(0, 0x46546C67, true); // magic number "glTF"
|
||||
headerDataView.setUint32(4, 2, true); // version
|
||||
headerDataView.setUint32(8, glbBufferLength, true); // length
|
||||
const glbBuffer = [header, ...jsonChunk, ...binaryChunk];
|
||||
|
||||
const glb = new Uint8Array(glbBufferLength);
|
||||
let offset = 0;
|
||||
for (const buffer of glbBuffer) {
|
||||
glb.set(new Uint8Array(buffer), offset);
|
||||
offset += buffer.byteLength;
|
||||
}
|
||||
return { glb };
|
||||
}
|
||||
|
||||
async getBlob(ctx: RuntimeContext) {
|
||||
return new Blob([(await this.getData()).glb], { type: 'model/gltf-binary' });
|
||||
}
|
||||
|
||||
constructor(private style: Style, boundingBox: Box3D) {
|
||||
super();
|
||||
const tmpV = Vec3();
|
||||
Vec3.add(tmpV, boundingBox.min, boundingBox.max);
|
||||
Vec3.scale(tmpV, tmpV, -0.5);
|
||||
this.centerTransform = Mat4.fromTranslation(Mat4(), tmpV);
|
||||
}
|
||||
}
|
||||
350
src/extensions/geo-export/mesh-exporter.ts
Normal file
350
src/extensions/geo-export/mesh-exporter.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { sort, arraySwap } from '../../mol-data/util';
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { MeshValues } from '../../mol-gl/renderable/mesh';
|
||||
import { LinesValues } from '../../mol-gl/renderable/lines';
|
||||
import { PointsValues } from '../../mol-gl/renderable/points';
|
||||
import { SpheresValues } from '../../mol-gl/renderable/spheres';
|
||||
import { CylindersValues } from '../../mol-gl/renderable/cylinders';
|
||||
import { TextureMeshValues } from '../../mol-gl/renderable/texture-mesh';
|
||||
import { BaseValues, SizeValues } from '../../mol-gl/renderable/schema';
|
||||
import { TextureImage } from '../../mol-gl/renderable/util';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { getTrilinearlyInterpolated } from '../../mol-geo/geometry/mesh/color-smoothing';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { sizeDataFactor } from '../../mol-geo/geometry/size-data';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { decodeFloatRGB } from '../../mol-util/float-packing';
|
||||
import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
|
||||
|
||||
const GeoExportName = 'geo-export';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
|
||||
export interface AddMeshInput {
|
||||
mesh: {
|
||||
vertices: Float32Array
|
||||
normals: Float32Array
|
||||
indices: Uint32Array | undefined
|
||||
groups: Float32Array | Uint8Array
|
||||
vertexCount: number
|
||||
drawCount: number
|
||||
} | undefined
|
||||
meshes: Mesh[] | undefined
|
||||
values: BaseValues
|
||||
isGeoTexture: boolean
|
||||
webgl: WebGLContext | undefined
|
||||
ctx: RuntimeContext
|
||||
}
|
||||
|
||||
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
|
||||
abstract readonly fileExtension: string;
|
||||
|
||||
private static getSizeFromTexture(tSize: TextureImage<Uint8Array>, i: number): number {
|
||||
const r = tSize.array[i * 3];
|
||||
const g = tSize.array[i * 3 + 1];
|
||||
const b = tSize.array[i * 3 + 2];
|
||||
return decodeFloatRGB(r, g, b) / sizeDataFactor;
|
||||
}
|
||||
|
||||
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
|
||||
const tSize = values.tSize.ref.value;
|
||||
let size = 0;
|
||||
switch (values.dSizeType.ref.value) {
|
||||
case 'uniform':
|
||||
size = values.uSize.ref.value;
|
||||
break;
|
||||
case 'instance':
|
||||
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex);
|
||||
break;
|
||||
case 'group':
|
||||
size = MeshExporter.getSizeFromTexture(tSize, group);
|
||||
break;
|
||||
case 'groupInstance':
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex * groupCount + group);
|
||||
break;
|
||||
}
|
||||
return size * values.uSizeFactor.ref.value;
|
||||
}
|
||||
|
||||
protected static getGroup(groups: Float32Array | Uint8Array, i: number): number {
|
||||
const i4 = i * 4;
|
||||
const r = groups[i4];
|
||||
const g = groups[i4 + 1];
|
||||
const b = groups[i4 + 2];
|
||||
if (groups instanceof Float32Array) {
|
||||
return decodeFloatRGB(r * 255 + 0.5, g * 255 + 0.5, b * 255 + 0.5);
|
||||
}
|
||||
return decodeFloatRGB(r, g, b);
|
||||
}
|
||||
|
||||
protected static getInterpolatedColors(vertices: Float32Array, vertexCount: number, values: BaseValues, stride: number, colorType: 'volume' | 'volumeInstance', webgl: WebGLContext) {
|
||||
const colorGridTransform = values.uColorGridTransform.ref.value;
|
||||
const colorGridDim = values.uColorGridDim.ref.value;
|
||||
const colorTexDim = values.uColorTexDim.ref.value;
|
||||
const aTransform = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
if (!webgl.namedFramebuffers[GeoExportName]) {
|
||||
webgl.namedFramebuffers[GeoExportName] = webgl.resources.framebuffer();
|
||||
}
|
||||
const framebuffer = webgl.namedFramebuffers[GeoExportName];
|
||||
|
||||
const [ width, height ] = colorTexDim;
|
||||
const colorGrid = new Uint8Array(width * height * 4);
|
||||
|
||||
framebuffer.bind();
|
||||
values.tColorGrid.ref.value.attachFramebuffer(framebuffer, 0);
|
||||
webgl.readPixels(0, 0, width, height, colorGrid);
|
||||
|
||||
const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer: aTransform, positionBuffer: vertices, colorType, grid: colorGrid, gridDim: colorGridDim, gridTexDim: colorTexDim, gridTransform: colorGridTransform, vertexStride: stride, colorStride: 4 });
|
||||
return interpolated.array;
|
||||
}
|
||||
|
||||
protected static quantizeColors(colorArray: Uint8Array, vertexCount: number) {
|
||||
if (vertexCount <= 1024) return;
|
||||
const rgb = Vec3();
|
||||
const min = Vec3();
|
||||
const max = Vec3();
|
||||
const sum = Vec3();
|
||||
const colorMap = new Map<Color, Color>();
|
||||
const colorComparers = [
|
||||
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]),
|
||||
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]),
|
||||
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]),
|
||||
];
|
||||
|
||||
const medianCut = (colors: Color[], l: number, r: number, depth: number) => {
|
||||
if (l > r) return;
|
||||
if (l === r || depth >= 10) {
|
||||
// Find the average color.
|
||||
Vec3.set(sum, 0, 0, 0);
|
||||
for (let i = l; i <= r; ++i) {
|
||||
Color.toVec3(rgb, colors[i]);
|
||||
Vec3.add(sum, sum, rgb);
|
||||
}
|
||||
Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1)));
|
||||
const averageColor = Color.fromArray(rgb, 0);
|
||||
for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the color channel with the greatest range.
|
||||
Vec3.set(min, 255, 255, 255);
|
||||
Vec3.set(max, 0, 0, 0);
|
||||
for (let i = l; i <= r; ++i) {
|
||||
Color.toVec3(rgb, colors[i]);
|
||||
for (let j = 0; j < 3; ++j) {
|
||||
Vec3.min(min, min, rgb);
|
||||
Vec3.max(max, max, rgb);
|
||||
}
|
||||
}
|
||||
let k = 0;
|
||||
if (max[1] - min[1] > max[k] - min[k]) k = 1;
|
||||
if (max[2] - min[2] > max[k] - min[k]) k = 2;
|
||||
|
||||
sort(colors, l, r + 1, colorComparers[k], arraySwap);
|
||||
|
||||
const m = (l + r) >> 1;
|
||||
medianCut(colors, l, m, depth + 1);
|
||||
medianCut(colors, m + 1, r, depth + 1);
|
||||
};
|
||||
|
||||
// Create an array of unique colors and use the median cut algorithm.
|
||||
const colorSet = new Set<Color>();
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
colorSet.add(Color.fromArray(colorArray, i * 3));
|
||||
}
|
||||
const colors = Array.from(colorSet);
|
||||
medianCut(colors, 0, colors.length - 1, 0);
|
||||
|
||||
// Map actual colors to quantized colors.
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
const color = colorMap.get(Color.fromArray(colorArray, i * 3));
|
||||
Color.toArray(color!, colorArray, i * 3);
|
||||
}
|
||||
}
|
||||
|
||||
protected static getInstance(input: AddMeshInput, instanceIndex: number) {
|
||||
const { mesh, meshes } = input;
|
||||
if (mesh !== undefined) {
|
||||
return mesh;
|
||||
} else {
|
||||
const mesh = meshes![instanceIndex];
|
||||
return {
|
||||
vertices: mesh.vertexBuffer.ref.value,
|
||||
normals: mesh.normalBuffer.ref.value,
|
||||
indices: mesh.indexBuffer.ref.value,
|
||||
groups: mesh.groupBuffer.ref.value,
|
||||
vertexCount: mesh.vertexCount,
|
||||
drawCount: mesh.triangleCount * 3
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract addMeshWithColors(input: AddMeshInput): void;
|
||||
|
||||
private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aNormal = values.aNormal.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const originalData = Mesh.getOriginalData(values);
|
||||
let indices: Uint32Array;
|
||||
let vertexCount: number;
|
||||
let drawCount: number;
|
||||
if (originalData) {
|
||||
indices = originalData.indexBuffer;
|
||||
vertexCount = originalData.vertexCount;
|
||||
drawCount = originalData.triangleCount * 3;
|
||||
} else {
|
||||
indices = values.elements.ref.value;
|
||||
vertexCount = values.uVertexCount.ref.value;
|
||||
drawCount = values.drawCount.ref.value;
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, webgl, ctx });
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private async addSpheres(values: SpheresValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const center = Vec3();
|
||||
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const sphereCount = vertexCount / 4 * instanceCount;
|
||||
let detail: number;
|
||||
if (sphereCount < 2000) detail = 3;
|
||||
else if (sphereCount < 20000) detail = 2;
|
||||
else detail = 1;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; i += 4) {
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group);
|
||||
state.currentGroup = group;
|
||||
addSphere(state, center, radius, detail);
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
|
||||
}
|
||||
|
||||
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const start = Vec3();
|
||||
const end = Vec3();
|
||||
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aScale = values.aScale.ref.value;
|
||||
const aCap = values.aCap.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const cylinderCount = vertexCount / 6 * instanceCount;
|
||||
let radialSegments: number;
|
||||
if (cylinderCount < 2000) radialSegments = 36;
|
||||
else if (cylinderCount < 20000) radialSegments = 24;
|
||||
else radialSegments = 12;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; i += 6) {
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
|
||||
const cap = aCap[i];
|
||||
const topCap = cap === 1 || cap === 3;
|
||||
const bottomCap = cap >= 2;
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
|
||||
}
|
||||
|
||||
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
if (!webgl.namedFramebuffers[GeoExportName]) {
|
||||
webgl.namedFramebuffers[GeoExportName] = webgl.resources.framebuffer();
|
||||
}
|
||||
const framebuffer = webgl.namedFramebuffers[GeoExportName];
|
||||
|
||||
const [ width, height ] = values.uGeoTexDim.ref.value;
|
||||
const vertices = new Float32Array(width * height * 4);
|
||||
const normals = new Float32Array(width * height * 4);
|
||||
const groups = webgl.isWebGL2 ? new Uint8Array(width * height * 4) : new Float32Array(width * height * 4);
|
||||
|
||||
framebuffer.bind();
|
||||
values.tPosition.ref.value.attachFramebuffer(framebuffer, 0);
|
||||
webgl.readPixels(0, 0, width, height, vertices);
|
||||
values.tNormal.ref.value.attachFramebuffer(framebuffer, 0);
|
||||
webgl.readPixels(0, 0, width, height, normals);
|
||||
values.tGroup.ref.value.attachFramebuffer(framebuffer, 0);
|
||||
webgl.readPixels(0, 0, width, height, groups);
|
||||
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const drawCount = values.drawCount.ref.value;
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, webgl, ctx });
|
||||
}
|
||||
|
||||
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
if (!renderObject.state.visible) return;
|
||||
|
||||
switch (renderObject.type) {
|
||||
case 'mesh':
|
||||
return this.addMesh(renderObject.values as MeshValues, webgl, ctx);
|
||||
case 'lines':
|
||||
return this.addLines(renderObject.values as LinesValues, webgl, ctx);
|
||||
case 'points':
|
||||
return this.addPoints(renderObject.values as PointsValues, webgl, ctx);
|
||||
case 'spheres':
|
||||
return this.addSpheres(renderObject.values as SpheresValues, webgl, ctx);
|
||||
case 'cylinders':
|
||||
return this.addCylinders(renderObject.values as CylindersValues, webgl, ctx);
|
||||
case 'texture-mesh':
|
||||
return this.addTextureMesh(renderObject.values as TextureMeshValues, webgl, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
abstract getData(ctx: RuntimeContext): Promise<D>;
|
||||
|
||||
abstract getBlob(ctx: RuntimeContext): Promise<Blob>;
|
||||
}
|
||||
222
src/extensions/geo-export/obj-exporter.ts
Normal file
222
src/extensions/geo-export/obj-exporter.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { asciiWrite } from '../../mol-io/common/ascii';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { StringBuilder } from '../../mol-util';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { zip } from '../../mol-util/zip/zip';
|
||||
import { MeshExporter, AddMeshInput } from './mesh-exporter';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3transformMat4 = Vec3.transformMat4;
|
||||
const v3transformMat3 = Vec3.transformMat3;
|
||||
const mat3directionTransform = Mat3.directionTransform;
|
||||
|
||||
// http://paulbourke.net/dataformats/obj/
|
||||
// http://paulbourke.net/dataformats/mtl/
|
||||
|
||||
export type ObjData = {
|
||||
obj: string
|
||||
mtl: string
|
||||
}
|
||||
|
||||
export class ObjExporter extends MeshExporter<ObjData> {
|
||||
readonly fileExtension = 'zip';
|
||||
private obj = StringBuilder.create();
|
||||
private mtl = StringBuilder.create();
|
||||
private vertexOffset = 0;
|
||||
private currentColor: Color | undefined;
|
||||
private currentAlpha: number | undefined;
|
||||
private materialSet = new Set<string>();
|
||||
private centerTransform: Mat4;
|
||||
|
||||
private updateMaterial(color: Color, alpha: number) {
|
||||
if (this.currentColor === color && this.currentAlpha === alpha) return;
|
||||
|
||||
this.currentColor = color;
|
||||
this.currentAlpha = alpha;
|
||||
const material = Color.toHexString(color) + alpha;
|
||||
StringBuilder.writeSafe(this.obj, `usemtl ${material}`);
|
||||
StringBuilder.newline(this.obj);
|
||||
if (!this.materialSet.has(material)) {
|
||||
this.materialSet.add(material);
|
||||
const [r, g, b] = Color.toRgbNormalized(color);
|
||||
const mtl = this.mtl;
|
||||
StringBuilder.writeSafe(mtl, `newmtl ${material}\n`);
|
||||
StringBuilder.writeSafe(mtl, 'illum 2\n'); // illumination model
|
||||
StringBuilder.writeSafe(mtl, 'Ns 163\n'); // specular exponent
|
||||
StringBuilder.writeSafe(mtl, 'Ni 0.001\n'); // optical density a.k.a. index of refraction
|
||||
StringBuilder.writeSafe(mtl, 'Ka 0 0 0\n'); // ambient reflectivity
|
||||
StringBuilder.writeSafe(mtl, 'Kd '); // diffuse reflectivity
|
||||
StringBuilder.writeFloat(mtl, r, 1000);
|
||||
StringBuilder.whitespace1(mtl);
|
||||
StringBuilder.writeFloat(mtl, g, 1000);
|
||||
StringBuilder.whitespace1(mtl);
|
||||
StringBuilder.writeFloat(mtl, b, 1000);
|
||||
StringBuilder.newline(mtl);
|
||||
StringBuilder.writeSafe(mtl, 'Ks 0.25 0.25 0.25\n'); // specular reflectivity
|
||||
StringBuilder.writeSafe(mtl, 'd '); // dissolve
|
||||
StringBuilder.writeFloat(mtl, alpha, 1000);
|
||||
StringBuilder.newline(mtl);
|
||||
}
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
|
||||
const obj = this.obj;
|
||||
const t = Mat4();
|
||||
const n = Mat3();
|
||||
const tmpV = Vec3();
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const tColor = values.tColor.ref.value.array;
|
||||
const uAlpha = values.uAlpha.ref.value;
|
||||
const dTransparency = values.dTransparency.ref.value;
|
||||
const tTransparency = values.tTransparency.ref.value;
|
||||
const aTransform = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
let interpolatedColors: Uint8Array;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
interpolatedColors = ObjExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
|
||||
ObjExporter.quantizeColors(interpolatedColors, mesh!.vertexCount);
|
||||
}
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
|
||||
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = ObjExporter.getInstance(input, instanceIndex);
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
mat3directionTransform(n, t);
|
||||
|
||||
// position
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
|
||||
StringBuilder.writeSafe(obj, 'v ');
|
||||
StringBuilder.writeFloat(obj, tmpV[0], 1000);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[1], 1000);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[2], 1000);
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
|
||||
StringBuilder.writeSafe(obj, 'vn ');
|
||||
StringBuilder.writeFloat(obj, tmpV[0], 100);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[1], 100);
|
||||
StringBuilder.whitespace1(obj);
|
||||
StringBuilder.writeFloat(obj, tmpV[2], 100);
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
// face
|
||||
for (let i = 0; i < drawCount; i += 3) {
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
|
||||
break;
|
||||
case 'instance':
|
||||
color = Color.fromArray(tColor, instanceIndex * 3);
|
||||
break;
|
||||
case 'group': {
|
||||
const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
|
||||
color = Color.fromArray(tColor, group * 3);
|
||||
break;
|
||||
}
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
|
||||
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
|
||||
break;
|
||||
}
|
||||
case 'vertex':
|
||||
color = Color.fromArray(tColor, indices![i] * 3);
|
||||
break;
|
||||
case 'vertexInstance':
|
||||
color = Color.fromArray(tColor, (instanceIndex * vertexCount + indices![i]) * 3);
|
||||
break;
|
||||
case 'volume':
|
||||
color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3);
|
||||
break;
|
||||
case 'volumeInstance':
|
||||
color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3);
|
||||
break;
|
||||
default: throw new Error('Unsupported color type.');
|
||||
}
|
||||
|
||||
let alpha = uAlpha;
|
||||
if (dTransparency) {
|
||||
const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
|
||||
const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
|
||||
alpha *= 1 - transparency;
|
||||
}
|
||||
|
||||
this.updateMaterial(color, alpha);
|
||||
|
||||
const v1 = this.vertexOffset + (isGeoTexture ? i : indices![i]) + 1;
|
||||
const v2 = this.vertexOffset + (isGeoTexture ? i + 1 : indices![i + 1]) + 1;
|
||||
const v3 = this.vertexOffset + (isGeoTexture ? i + 2 : indices![i + 2]) + 1;
|
||||
StringBuilder.writeSafe(obj, 'f ');
|
||||
StringBuilder.writeInteger(obj, v1);
|
||||
StringBuilder.writeSafe(obj, '//');
|
||||
StringBuilder.writeIntegerAndSpace(obj, v1);
|
||||
StringBuilder.writeInteger(obj, v2);
|
||||
StringBuilder.writeSafe(obj, '//');
|
||||
StringBuilder.writeIntegerAndSpace(obj, v2);
|
||||
StringBuilder.writeInteger(obj, v3);
|
||||
StringBuilder.writeSafe(obj, '//');
|
||||
StringBuilder.writeInteger(obj, v3);
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
this.vertexOffset += vertexCount;
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
return {
|
||||
obj: StringBuilder.getString(this.obj),
|
||||
mtl: StringBuilder.getString(this.mtl)
|
||||
};
|
||||
}
|
||||
|
||||
async getBlob(ctx: RuntimeContext) {
|
||||
const { obj, mtl } = await this.getData();
|
||||
const objData = new Uint8Array(obj.length);
|
||||
asciiWrite(objData, obj);
|
||||
const mtlData = new Uint8Array(mtl.length);
|
||||
asciiWrite(mtlData, mtl);
|
||||
const zipDataObj = {
|
||||
[this.filename + '.obj']: objData,
|
||||
[this.filename + '.mtl']: mtlData
|
||||
};
|
||||
return new Blob([await zip(ctx, zipDataObj)], { type: 'application/zip' });
|
||||
}
|
||||
|
||||
constructor(private filename: string, boundingBox: Box3D) {
|
||||
super();
|
||||
StringBuilder.writeSafe(this.obj, `mtllib ${filename}.mtl\n`);
|
||||
const tmpV = Vec3();
|
||||
Vec3.add(tmpV, boundingBox.min, boundingBox.max);
|
||||
Vec3.scale(tmpV, tmpV, -0.5);
|
||||
this.centerTransform = Mat4.fromTranslation(Mat4(), tmpV);
|
||||
}
|
||||
}
|
||||
20
src/extensions/geo-export/render-object-exporter.ts
Normal file
20
src/extensions/geo-export/render-object-exporter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
|
||||
export type RenderObjectExportData = {
|
||||
[k: string]: string | Uint8Array | ArrayBuffer | undefined
|
||||
}
|
||||
|
||||
export interface RenderObjectExporter<D extends RenderObjectExportData> {
|
||||
readonly fileExtension: string
|
||||
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext): Promise<void> | undefined
|
||||
getData(ctx: RuntimeContext): Promise<D>
|
||||
getBlob(ctx: RuntimeContext): Promise<Blob>
|
||||
}
|
||||
119
src/extensions/geo-export/stl-exporter.ts
Normal file
119
src/extensions/geo-export/stl-exporter.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { asciiWrite } from '../../mol-io/common/ascii';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { PLUGIN_VERSION } from '../../mol-plugin/version';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { MeshExporter, AddMeshInput } from './mesh-exporter';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3transformMat4 = Vec3.transformMat4;
|
||||
const v3triangleNormal = Vec3.triangleNormal;
|
||||
const v3toArray = Vec3.toArray;
|
||||
|
||||
// https://www.fabbers.com/tech/STL_Format
|
||||
|
||||
export type StlData = {
|
||||
stl: Uint8Array
|
||||
}
|
||||
|
||||
export class StlExporter extends MeshExporter<StlData> {
|
||||
readonly fileExtension = 'stl';
|
||||
private triangleBuffers: ArrayBuffer[] = [];
|
||||
private triangleCount = 0;
|
||||
private centerTransform: Mat4;
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { values, isGeoTexture, ctx } = input;
|
||||
|
||||
const t = Mat4();
|
||||
const tmpV = Vec3();
|
||||
const v1 = Vec3();
|
||||
const v2 = Vec3();
|
||||
const v3 = Vec3();
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
|
||||
const aTransform = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
|
||||
|
||||
const { vertices, indices, vertexCount, drawCount } = StlExporter.getInstance(input, instanceIndex);
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
|
||||
// position
|
||||
const vertexArray = new Float32Array(vertexCount * 3);
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
|
||||
v3toArray(tmpV, vertexArray, i * 3);
|
||||
}
|
||||
|
||||
// face
|
||||
const triangleBuffer = new ArrayBuffer(50 * drawCount);
|
||||
const dataView = new DataView(triangleBuffer);
|
||||
for (let i = 0; i < drawCount; i += 3) {
|
||||
v3fromArray(v1, vertexArray, (isGeoTexture ? i : indices![i]) * 3);
|
||||
v3fromArray(v2, vertexArray, (isGeoTexture ? i + 1 : indices![i + 1]) * 3);
|
||||
v3fromArray(v3, vertexArray, (isGeoTexture ? i + 2 : indices![i + 2]) * 3);
|
||||
v3triangleNormal(tmpV, v1, v2, v3);
|
||||
|
||||
const byteOffset = 50 * i;
|
||||
dataView.setFloat32(byteOffset, tmpV[0], true);
|
||||
dataView.setFloat32(byteOffset + 4, tmpV[1], true);
|
||||
dataView.setFloat32(byteOffset + 8, tmpV[2], true);
|
||||
|
||||
dataView.setFloat32(byteOffset + 12, v1[0], true);
|
||||
dataView.setFloat32(byteOffset + 16, v1[1], true);
|
||||
dataView.setFloat32(byteOffset + 20, v1[2], true);
|
||||
|
||||
dataView.setFloat32(byteOffset + 24, v2[0], true);
|
||||
dataView.setFloat32(byteOffset + 28, v2[1], true);
|
||||
dataView.setFloat32(byteOffset + 32, v2[2], true);
|
||||
|
||||
dataView.setFloat32(byteOffset + 36, v3[0], true);
|
||||
dataView.setFloat32(byteOffset + 40, v3[1], true);
|
||||
dataView.setFloat32(byteOffset + 44, v3[2], true);
|
||||
}
|
||||
|
||||
this.triangleBuffers.push(triangleBuffer);
|
||||
this.triangleCount += drawCount;
|
||||
}
|
||||
}
|
||||
|
||||
async getData() {
|
||||
const stl = new Uint8Array(84 + 50 * this.triangleCount);
|
||||
|
||||
asciiWrite(stl, `Exported from Mol* ${PLUGIN_VERSION}`);
|
||||
|
||||
const dataView = new DataView(stl.buffer);
|
||||
dataView.setUint32(80, this.triangleCount, true);
|
||||
|
||||
let byteOffset = 84;
|
||||
for (const buffer of this.triangleBuffers) {
|
||||
stl.set(new Uint8Array(buffer), byteOffset);
|
||||
byteOffset += buffer.byteLength;
|
||||
}
|
||||
return { stl };
|
||||
}
|
||||
|
||||
async getBlob(ctx: RuntimeContext) {
|
||||
return new Blob([(await this.getData()).stl], { type: 'model/stl' });
|
||||
}
|
||||
|
||||
constructor(boundingBox: Box3D) {
|
||||
super();
|
||||
const tmpV = Vec3();
|
||||
Vec3.add(tmpV, boundingBox.min, boundingBox.max);
|
||||
Vec3.scale(tmpV, tmpV, -0.5);
|
||||
this.centerTransform = Mat4.fromTranslation(Mat4(), tmpV);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { merge } from 'rxjs';
|
||||
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
|
||||
import { Button } from '../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
|
||||
import { GetAppSvg, CubeScanSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { GeometryControls } from './controls';
|
||||
import { GeometryParams, GeometryControls } from './controls';
|
||||
|
||||
interface State {
|
||||
busy?: boolean
|
||||
@@ -16,6 +18,7 @@ interface State {
|
||||
|
||||
export class GeometryExporterUI extends CollapsableControls<{}, State> {
|
||||
private _controls: GeometryControls | undefined;
|
||||
private isARSupported: boolean | undefined;
|
||||
|
||||
get controls() {
|
||||
return this._controls || (this._controls = new GeometryControls(this.plugin));
|
||||
@@ -23,24 +26,46 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
|
||||
|
||||
protected defaultState(): State & CollapsableState {
|
||||
return {
|
||||
header: 'Export Geometries',
|
||||
header: 'Export Geometry',
|
||||
isCollapsed: true,
|
||||
brand: { accent: 'cyan', svg: CubeSendSvg }
|
||||
};
|
||||
}
|
||||
|
||||
protected renderControls(): JSX.Element {
|
||||
if (this.isARSupported === undefined) {
|
||||
this.isARSupported = !!document.createElement('a').relList?.supports?.('ar');
|
||||
}
|
||||
const ctrl = this.controls;
|
||||
return <>
|
||||
<ParameterControls
|
||||
params={GeometryParams}
|
||||
values={ctrl.behaviors.params.value}
|
||||
onChangeValues={xs => ctrl.behaviors.params.next(xs)}
|
||||
isDisabled={this.state.busy}
|
||||
/>
|
||||
<Button icon={GetAppSvg}
|
||||
onClick={this.saveObj} style={{ marginTop: 1 }}
|
||||
onClick={this.save} style={{ marginTop: 1 }}
|
||||
disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
|
||||
Save OBJ + MTL
|
||||
Save
|
||||
</Button>
|
||||
{this.isARSupported && ctrl.behaviors.params.value.format === 'usdz' &&
|
||||
<Button icon={CubeScanSvg}
|
||||
onClick={this.viewInAR} style={{ marginTop: 1 }}
|
||||
disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
|
||||
View in AR
|
||||
</Button>
|
||||
}
|
||||
</>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribe(this.plugin.canvas3d!.reprCount, () => {
|
||||
const merged = merge(
|
||||
this.controls.behaviors.params,
|
||||
this.plugin.canvas3d!.reprCount
|
||||
);
|
||||
|
||||
this.subscribe(merged, () => {
|
||||
if (!this.state.isCollapsed) this.forceUpdate();
|
||||
});
|
||||
}
|
||||
@@ -50,13 +75,31 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
|
||||
this._controls = void 0;
|
||||
}
|
||||
|
||||
saveObj = async () => {
|
||||
save = async () => {
|
||||
try {
|
||||
this.setState({ busy: true });
|
||||
const data = await this.controls.exportObj();
|
||||
const data = await this.controls.exportGeometry();
|
||||
this.setState({ busy: false });
|
||||
|
||||
download(new Blob([data.zipData]), data.filename);
|
||||
download(data.blob, data.filename);
|
||||
} catch {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
|
||||
viewInAR = async () => {
|
||||
try {
|
||||
this.setState({ busy: true });
|
||||
const data = await this.controls.exportGeometry();
|
||||
this.setState({ busy: false });
|
||||
const a = document.createElement('a');
|
||||
a.rel = 'ar';
|
||||
a.href = URL.createObjectURL(data.blob);
|
||||
// For in-place viewing of USDZ on iOS, the link must contain a single child that is either an img or picture.
|
||||
// https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/
|
||||
a.appendChild(document.createElement('img'));
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 4E4); // 40s
|
||||
setTimeout(() => a.dispatchEvent(new MouseEvent('click')));
|
||||
} catch {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
|
||||
262
src/extensions/geo-export/usdz-exporter.ts
Normal file
262
src/extensions/geo-export/usdz-exporter.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
import { Style } from '../../mol-gl/renderer';
|
||||
import { asciiWrite } from '../../mol-io/common/ascii';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { PLUGIN_VERSION } from '../../mol-plugin/version';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { StringBuilder } from '../../mol-util';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { zip } from '../../mol-util/zip/zip';
|
||||
import { MeshExporter, AddMeshInput } from './mesh-exporter';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3transformMat4 = Vec3.transformMat4;
|
||||
const v3transformMat3 = Vec3.transformMat3;
|
||||
const mat3directionTransform = Mat3.directionTransform;
|
||||
|
||||
// https://graphics.pixar.com/usd/docs/index.html
|
||||
|
||||
export type UsdzData = {
|
||||
usdz: ArrayBuffer
|
||||
}
|
||||
|
||||
export class UsdzExporter extends MeshExporter<UsdzData> {
|
||||
readonly fileExtension = 'usdz';
|
||||
private meshes: string[] = [];
|
||||
private materials: string[] = [];
|
||||
private materialSet = new Set<number>();
|
||||
private centerTransform: Mat4;
|
||||
|
||||
private static getMaterialKey(color: Color, alpha: number) {
|
||||
return color * 256 + Math.round(alpha * 255);
|
||||
}
|
||||
|
||||
private addMaterial(color: Color, alpha: number) {
|
||||
const materialKey = UsdzExporter.getMaterialKey(color, alpha);
|
||||
if (this.materialSet.has(materialKey)) return;
|
||||
this.materialSet.add(materialKey);
|
||||
const [r, g, b] = Color.toRgbNormalized(color);
|
||||
this.materials.push(`
|
||||
def Material "material${materialKey}"
|
||||
{
|
||||
token outputs:surface.connect = </material${materialKey}/shader.outputs:surface>
|
||||
def Shader "shader"
|
||||
{
|
||||
uniform token info:id = "UsdPreviewSurface"
|
||||
color3f inputs:diffuseColor = (${r},${g},${b})
|
||||
float inputs:opacity = ${alpha}
|
||||
float inputs:metallic = ${this.style.metalness}
|
||||
float inputs:roughness = ${this.style.roughness}
|
||||
token outputs:surface
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
|
||||
const t = Mat4();
|
||||
const n = Mat3();
|
||||
const tmpV = Vec3();
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const tColor = values.tColor.ref.value.array;
|
||||
const uAlpha = values.uAlpha.ref.value;
|
||||
const dTransparency = values.dTransparency.ref.value;
|
||||
const tTransparency = values.tTransparency.ref.value;
|
||||
const aTransform = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
let interpolatedColors: Uint8Array;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
interpolatedColors = UsdzExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
|
||||
UsdzExporter.quantizeColors(interpolatedColors, mesh!.vertexCount);
|
||||
}
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
|
||||
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = UsdzExporter.getInstance(input, instanceIndex);
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
mat3directionTransform(n, t);
|
||||
|
||||
const vertexBuilder = StringBuilder.create();
|
||||
const normalBuilder = StringBuilder.create();
|
||||
const indexBuilder = StringBuilder.create();
|
||||
|
||||
// position
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
|
||||
StringBuilder.writeSafe(vertexBuilder, (i === 0) ? '(' : ',(');
|
||||
StringBuilder.writeFloat(vertexBuilder, tmpV[0], 10000);
|
||||
StringBuilder.writeSafe(vertexBuilder, ',');
|
||||
StringBuilder.writeFloat(vertexBuilder, tmpV[1], 10000);
|
||||
StringBuilder.writeSafe(vertexBuilder, ',');
|
||||
StringBuilder.writeFloat(vertexBuilder, tmpV[2], 10000);
|
||||
StringBuilder.writeSafe(vertexBuilder, ')');
|
||||
}
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
|
||||
StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',(');
|
||||
StringBuilder.writeFloat(normalBuilder, tmpV[0], 100);
|
||||
StringBuilder.writeSafe(normalBuilder, ',');
|
||||
StringBuilder.writeFloat(normalBuilder, tmpV[1], 100);
|
||||
StringBuilder.writeSafe(normalBuilder, ',');
|
||||
StringBuilder.writeFloat(normalBuilder, tmpV[2], 100);
|
||||
StringBuilder.writeSafe(normalBuilder, ')');
|
||||
}
|
||||
|
||||
// face
|
||||
for (let i = 0; i < drawCount; ++i) {
|
||||
const v = isGeoTexture ? i : indices![i];
|
||||
if (i > 0) StringBuilder.writeSafe(indexBuilder, ',');
|
||||
StringBuilder.writeInteger(indexBuilder, v);
|
||||
}
|
||||
|
||||
// color
|
||||
const faceIndicesByMaterial = new Map<number, number[]>();
|
||||
for (let i = 0; i < drawCount; i += 3) {
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
|
||||
break;
|
||||
case 'instance':
|
||||
color = Color.fromArray(tColor, instanceIndex * 3);
|
||||
break;
|
||||
case 'group': {
|
||||
const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
|
||||
color = Color.fromArray(tColor, group * 3);
|
||||
break;
|
||||
}
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
|
||||
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
|
||||
break;
|
||||
}
|
||||
case 'vertex':
|
||||
color = Color.fromArray(tColor, indices![i] * 3);
|
||||
break;
|
||||
case 'vertexInstance':
|
||||
color = Color.fromArray(tColor, (instanceIndex * vertexCount + indices![i]) * 3);
|
||||
break;
|
||||
case 'volume':
|
||||
color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3);
|
||||
break;
|
||||
case 'volumeInstance':
|
||||
color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3);
|
||||
break;
|
||||
default: throw new Error('Unsupported color type.');
|
||||
}
|
||||
|
||||
let alpha = uAlpha;
|
||||
if (dTransparency) {
|
||||
const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
|
||||
const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
|
||||
alpha *= 1 - transparency;
|
||||
}
|
||||
|
||||
this.addMaterial(color, alpha);
|
||||
|
||||
const materialKey = UsdzExporter.getMaterialKey(color, alpha);
|
||||
let faceIndices = faceIndicesByMaterial.get(materialKey);
|
||||
if (faceIndices === undefined) {
|
||||
faceIndices = [];
|
||||
faceIndicesByMaterial.set(materialKey, faceIndices);
|
||||
}
|
||||
faceIndices.push(i / 3);
|
||||
}
|
||||
|
||||
// If this mesh uses only one material, bind it to the material directly.
|
||||
// Otherwise, use GeomSubsets to bind it to multiple materials.
|
||||
let materialBinding: string;
|
||||
if (faceIndicesByMaterial.size === 1) {
|
||||
const materialKey = faceIndicesByMaterial.keys().next().value;
|
||||
materialBinding = `rel material:binding = </material${materialKey}>`;
|
||||
} else {
|
||||
const geomSubsets: string[] = [];
|
||||
faceIndicesByMaterial.forEach((faceIndices: number[], materialKey: number) => {
|
||||
geomSubsets.push(`
|
||||
def GeomSubset "g${materialKey}"
|
||||
{
|
||||
uniform token elementType = "face"
|
||||
uniform token familyName = "materialBind"
|
||||
int[] indices = [${faceIndices.join(',')}]
|
||||
rel material:binding = </material${materialKey}>
|
||||
}
|
||||
`);
|
||||
});
|
||||
materialBinding = geomSubsets.join('');
|
||||
}
|
||||
|
||||
this.meshes.push(`
|
||||
def Mesh "mesh${this.meshes.length}"
|
||||
{
|
||||
int[] faceVertexCounts = [${new Array(drawCount / 3).fill(3).join(',')}]
|
||||
int[] faceVertexIndices = [${StringBuilder.getString(indexBuilder)}]
|
||||
point3f[] points = [${StringBuilder.getString(vertexBuilder)}]
|
||||
normal3f[] primvars:normals = [${StringBuilder.getString(normalBuilder)}] (
|
||||
interpolation = "vertex"
|
||||
)
|
||||
uniform token subdivisionScheme = "none"
|
||||
${materialBinding}
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
async getData(ctx: RuntimeContext) {
|
||||
const header = `#usda 1.0
|
||||
(
|
||||
customLayerData = {
|
||||
string creator = "Mol* ${PLUGIN_VERSION}"
|
||||
}
|
||||
metersPerUnit = 1
|
||||
)
|
||||
`;
|
||||
const usda = [header, ...this.materials, ...this.meshes].join('');
|
||||
const usdaData = new Uint8Array(usda.length);
|
||||
asciiWrite(usdaData, usda);
|
||||
const zipDataObj = {
|
||||
['model.usda']: usdaData
|
||||
};
|
||||
return {
|
||||
usdz: await zip(ctx, zipDataObj, true)
|
||||
};
|
||||
}
|
||||
|
||||
async getBlob(ctx: RuntimeContext) {
|
||||
const { usdz } = await this.getData(ctx);
|
||||
return new Blob([usdz], { type: 'model/vnd.usdz+zip' });
|
||||
}
|
||||
|
||||
constructor(private style: Style, boundingBox: Box3D, radius: number) {
|
||||
super();
|
||||
const t = Mat4();
|
||||
// scale the model so that it fits within 1 meter
|
||||
Mat4.fromUniformScaling(t, Math.min(1 / (radius * 2), 1));
|
||||
// translate the model so that it sits on the ground plane (y = 0)
|
||||
Mat4.translate(t, t, Vec3.create(
|
||||
-(boundingBox.min[0] + boundingBox.max[0]) / 2,
|
||||
-boundingBox.min[1],
|
||||
-(boundingBox.min[2] + boundingBox.max[2]) / 2
|
||||
));
|
||||
this.centerTransform = t;
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,7 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
|
||||
return {
|
||||
factory: StructureQualityReportColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Assigns residue colors according to the number of quality issues or a specific quality issue. Data from wwPDB Validation Report, obtained via PDBe.',
|
||||
@@ -114,6 +115,6 @@ export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Param
|
||||
isApplicable: (ctx: ThemeDataContext) => StructureQualityReport.isApplicable(ctx.structure?.models[0]),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? StructureQualityReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(StructureQualityReportProvider.descriptor, false)
|
||||
detach: (data) => data.structure && StructureQualityReportProvider.ref(data.structure.models[0], false)
|
||||
}
|
||||
};
|
||||
@@ -109,6 +109,6 @@ export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<Asse
|
||||
isApplicable: (ctx: ThemeDataContext) => AssemblySymmetry.isApplicable(ctx.structure),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? AssemblySymmetryProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(AssemblySymmetryProvider.descriptor, false)
|
||||
detach: (data) => data.structure && AssemblySymmetryProvider.ref(data.structure, false)
|
||||
}
|
||||
};
|
||||
@@ -124,16 +124,20 @@ export function getSymmetrySelectParam(structure?: Structure) {
|
||||
if (structure) {
|
||||
const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value;
|
||||
if (assemblySymmetryData) {
|
||||
const options: [number, string][] = [];
|
||||
const options: [number, string][] = [
|
||||
[-1, 'Off']
|
||||
];
|
||||
for (let i = 0, il = assemblySymmetryData.length; i < il; ++i) {
|
||||
const { symbol, kind } = assemblySymmetryData[i];
|
||||
if (symbol !== 'C1') {
|
||||
options.push([ i, `${i + 1}: ${symbol} ${kind}` ]);
|
||||
}
|
||||
}
|
||||
if (options.length) {
|
||||
if (options.length > 1) {
|
||||
param.options = options;
|
||||
param.defaultValue = options[0][0];
|
||||
param.defaultValue = options[1][0];
|
||||
} else {
|
||||
options.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
|
||||
const structure = this.pivot.cell.obj?.data;
|
||||
const params = PD.clone(structure ? AssemblySymmetryProvider.getParams(structure) : AssemblySymmetryProvider.defaultParams);
|
||||
params.serverUrl.isHidden = true;
|
||||
params.symmetryIndex.options = [[-1, 'Off'], ...params.symmetryIndex.options];
|
||||
return params;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
|
||||
return {
|
||||
factory: DensityFitColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color,
|
||||
props,
|
||||
contextHash,
|
||||
@@ -76,6 +77,6 @@ export const DensityFitColorThemeProvider: ColorTheme.Provider<{}, ValidationRep
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ValidationReport.isApplicable(ctx.structure.models[0]) && Model.isFromXray(ctx.structure.models[0]) && Model.probablyHasDensityMap(ctx.structure.models[0]),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ValidationReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ValidationReportProvider.descriptor, false)
|
||||
detach: (data) => data.structure && ValidationReportProvider.ref(data.structure.models[0], false)
|
||||
}
|
||||
};
|
||||
@@ -96,6 +96,7 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
return {
|
||||
factory: GeometryQualityColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color,
|
||||
props,
|
||||
contextHash,
|
||||
@@ -114,6 +115,6 @@ export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQua
|
||||
isApplicable: (ctx: ThemeDataContext) => ValidationReport.isApplicable(ctx.structure?.models[0]),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ValidationReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ValidationReportProvider.descriptor, false)
|
||||
detach: (data) => data.structure && ValidationReportProvider.ref(data.structure.models[0], false)
|
||||
}
|
||||
};
|
||||
@@ -49,6 +49,7 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
|
||||
return {
|
||||
factory: RandomCoilIndexColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color,
|
||||
props,
|
||||
contextHash,
|
||||
@@ -67,6 +68,6 @@ export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}, Validati
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ValidationReport.isApplicable(ctx.structure.models[0]) && Model.isFromNmr(ctx.structure.models[0]),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ValidationReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ValidationReportProvider.descriptor, false)
|
||||
detach: (data) => data.structure && ValidationReportProvider.ref(data.structure.models[0], false)
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,7 @@ import { RepresentationContext, RepresentationParamsGetter, Representation } fro
|
||||
import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider, ComplexRepresentation } from '../../../mol-repr/structure/representation';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
|
||||
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
|
||||
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr/structure/units-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { ClashesProvider, IntraUnitClashes, InterUnitClashes, ValidationReport } from './prop';
|
||||
@@ -28,6 +28,7 @@ import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { bondLabel } from '../../../mol-theme/label';
|
||||
import { getUnitKindsParam } from '../../../mol-repr/structure/params';
|
||||
import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
|
||||
|
||||
//
|
||||
|
||||
|
||||
@@ -87,7 +87,12 @@ class Camera implements ICamera {
|
||||
|
||||
if (changed) {
|
||||
Mat4.mul(this.projectionView, this.projection, this.view);
|
||||
Mat4.invert(this.inverseProjectionView, this.projectionView);
|
||||
if (!Mat4.tryInvert(this.inverseProjectionView, this.projectionView)) {
|
||||
Mat4.copy(this.view, this.prevView);
|
||||
Mat4.copy(this.projection, this.prevProjection);
|
||||
Mat4.mul(this.projectionView, this.projection, this.view);
|
||||
return false;
|
||||
}
|
||||
|
||||
Mat4.copy(this.prevView, this.view);
|
||||
Mat4.copy(this.prevProjection, this.projection);
|
||||
@@ -230,7 +235,7 @@ namespace Camera {
|
||||
target: Vec3.create(0, 0, 0),
|
||||
|
||||
radius: 0,
|
||||
radiusMax: 0,
|
||||
radiusMax: 10,
|
||||
fog: 50,
|
||||
clipFar: true
|
||||
};
|
||||
@@ -267,6 +272,18 @@ namespace Camera {
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function areSnapshotsEqual(a: Snapshot, b: Snapshot) {
|
||||
return a.mode === b.mode
|
||||
&& a.fov === b.fov
|
||||
&& a.radius === b.radius
|
||||
&& a.radiusMax === b.radiusMax
|
||||
&& a.fog === b.fog
|
||||
&& a.clipFar === b.clipFar
|
||||
&& Vec3.exactEquals(a.position, b.position)
|
||||
&& Vec3.exactEquals(a.up, b.up)
|
||||
&& Vec3.exactEquals(a.target, b.target);
|
||||
}
|
||||
}
|
||||
|
||||
function updateOrtho(camera: Camera) {
|
||||
@@ -347,8 +364,9 @@ function updateClip(camera: Camera) {
|
||||
near = Math.max(Math.min(radiusMax, 5), near);
|
||||
far = Math.max(5, far);
|
||||
} else {
|
||||
near = Math.max(0, near);
|
||||
far = Math.max(0, far);
|
||||
// not too close to 0 as it causes issues with outline rendering
|
||||
near = Math.max(Math.min(radiusMax, 5), near);
|
||||
far = Math.max(5, far);
|
||||
}
|
||||
|
||||
if (near === far) {
|
||||
|
||||
@@ -39,6 +39,9 @@ class CameraTransitionManager {
|
||||
this._target.radius = this._target.radiusMax;
|
||||
}
|
||||
|
||||
if (this._target.radius < 0.01) this._target.radius = 0.01;
|
||||
if (this._target.radiusMax < 0.01) this._target.radiusMax = 0.01;
|
||||
|
||||
if (!this.inTransition && durationMs <= 0 || (typeof to.mode !== 'undefined' && to.mode !== this.camera.state.mode)) {
|
||||
this.finish(this._target);
|
||||
return;
|
||||
|
||||
@@ -61,11 +61,17 @@ export const Canvas3DParams = {
|
||||
}, { pivot: 'radius' }),
|
||||
viewport: PD.MappedStatic('canvas', {
|
||||
canvas: PD.Group({}),
|
||||
custom: PD.Group({
|
||||
'static-frame': PD.Group({
|
||||
x: PD.Numeric(0),
|
||||
y: PD.Numeric(0),
|
||||
width: PD.Numeric(128),
|
||||
height: PD.Numeric(128)
|
||||
}),
|
||||
'relative-frame': PD.Group({
|
||||
x: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
|
||||
y: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
|
||||
width: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 }),
|
||||
height: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 })
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -100,7 +106,7 @@ interface Canvas3DContext {
|
||||
}
|
||||
|
||||
namespace Canvas3DContext {
|
||||
const DefaultAttribs = {
|
||||
export const DefaultAttribs = {
|
||||
/** true by default to avoid issues with Safari (Jan 2021) */
|
||||
antialias: true,
|
||||
/** true to support multiple Canvas3D objects with a single context */
|
||||
@@ -201,7 +207,7 @@ interface Canvas3D {
|
||||
*/
|
||||
commit(isSynchronous?: boolean): void
|
||||
/**
|
||||
* Funcion for external "animation" control
|
||||
* Function for external "animation" control
|
||||
* Calls commit.
|
||||
*/
|
||||
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
|
||||
@@ -214,7 +220,13 @@ interface Canvas3D {
|
||||
/** Reset the timers, used by "animate" */
|
||||
resetTime(t: number): void
|
||||
animate(): void
|
||||
pause(): void
|
||||
/**
|
||||
* Pause animation loop and optionally any rendering
|
||||
* @param noDraw pause any rendering (drawPaused = true)
|
||||
*/
|
||||
pause(noDraw?: boolean): void
|
||||
/** Sets drawPaused = false without starting the built in animation loop */
|
||||
resume(): void
|
||||
identify(x: number, y: number): PickData | undefined
|
||||
mark(loci: Representation.Loci, action: MarkerAction): void
|
||||
getLoci(pickingId: PickingId | undefined): Representation.Loci
|
||||
@@ -232,6 +244,7 @@ interface Canvas3D {
|
||||
requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider }): void
|
||||
readonly camera: Camera
|
||||
readonly boundingSphere: Readonly<Sphere3D>
|
||||
readonly boundingSphereVisible: Readonly<Sphere3D>
|
||||
setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
|
||||
getImagePass(props: Partial<ImageProps>): ImagePass
|
||||
getRenderObjects(): GraphicsRenderObject[]
|
||||
@@ -294,7 +307,6 @@ namespace Canvas3D {
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
|
||||
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
|
||||
|
||||
let drawPending = false;
|
||||
let cameraResetRequested = false;
|
||||
let nextCameraResetDuration: number | undefined = void 0;
|
||||
let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
|
||||
@@ -362,9 +374,13 @@ namespace Canvas3D {
|
||||
let didRender = false;
|
||||
controls.update(currentTime);
|
||||
const cameraChanged = camera.update();
|
||||
const multiSampleChanged = multiSampleHelper.update(force || cameraChanged, p.multiSample);
|
||||
|
||||
if (resized || force || cameraChanged || multiSampleChanged) {
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender;
|
||||
forceNextRender = false;
|
||||
|
||||
const multiSampleChanged = multiSampleHelper.update(shouldRender, p.multiSample);
|
||||
|
||||
if (shouldRender || multiSampleChanged) {
|
||||
let cam: Camera | StereoCamera = camera;
|
||||
if (p.camera.stereo.name === 'on') {
|
||||
stereoCamera.update();
|
||||
@@ -383,22 +399,20 @@ namespace Canvas3D {
|
||||
return didRender;
|
||||
}
|
||||
|
||||
let forceNextDraw = false;
|
||||
let forceNextRender = false;
|
||||
let forceDrawAfterAllCommited = false;
|
||||
let currentTime = 0;
|
||||
let drawPaused = false;
|
||||
|
||||
function draw(force?: boolean) {
|
||||
if (render(!!force || forceNextDraw) && notifyDidDraw) {
|
||||
if (drawPaused) return;
|
||||
if (render(!!force) && notifyDidDraw) {
|
||||
didDraw.next(now() - startTime as now.Timestamp);
|
||||
}
|
||||
forceNextDraw = false;
|
||||
drawPending = false;
|
||||
}
|
||||
|
||||
function requestDraw(force?: boolean) {
|
||||
if (drawPending) return;
|
||||
drawPending = true;
|
||||
forceNextDraw = !!force;
|
||||
forceNextRender = forceNextRender || !!force;
|
||||
}
|
||||
|
||||
let animationFrameHandle = 0;
|
||||
@@ -429,11 +443,13 @@ namespace Canvas3D {
|
||||
}
|
||||
|
||||
function animate() {
|
||||
drawPaused = false;
|
||||
controls.start(now());
|
||||
if (animationFrameHandle === 0) _animate();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
function pause(noDraw = false) {
|
||||
drawPaused = noDraw;
|
||||
cancelAnimationFrame(animationFrameHandle);
|
||||
animationFrameHandle = 0;
|
||||
}
|
||||
@@ -688,6 +704,7 @@ namespace Canvas3D {
|
||||
animate,
|
||||
resetTime,
|
||||
pause,
|
||||
resume: () => { drawPaused = false; },
|
||||
identify,
|
||||
mark,
|
||||
getLoci,
|
||||
@@ -703,6 +720,7 @@ namespace Canvas3D {
|
||||
},
|
||||
camera,
|
||||
boundingSphere: scene.boundingSphere,
|
||||
boundingSphereVisible: scene.boundingSphereVisible,
|
||||
get notifyDidDraw() { return notifyDidDraw; },
|
||||
set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
|
||||
didDraw,
|
||||
@@ -800,16 +818,30 @@ namespace Canvas3D {
|
||||
};
|
||||
|
||||
function updateViewport() {
|
||||
const oldX = x, oldY = y, oldWidth = width, oldHeight = height;
|
||||
|
||||
if (p.viewport.name === 'canvas') {
|
||||
x = 0;
|
||||
y = 0;
|
||||
width = gl.drawingBufferWidth;
|
||||
height = gl.drawingBufferHeight;
|
||||
} else {
|
||||
} else if (p.viewport.name === 'static-frame') {
|
||||
x = p.viewport.params.x * webgl.pixelRatio;
|
||||
y = p.viewport.params.y * webgl.pixelRatio;
|
||||
width = p.viewport.params.width * webgl.pixelRatio;
|
||||
height = p.viewport.params.height * webgl.pixelRatio;
|
||||
y = gl.drawingBufferHeight - height - p.viewport.params.y * webgl.pixelRatio;
|
||||
width = p.viewport.params.width * webgl.pixelRatio;
|
||||
} else if (p.viewport.name === 'relative-frame') {
|
||||
x = Math.round(p.viewport.params.x * gl.drawingBufferWidth);
|
||||
height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
|
||||
y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
|
||||
width = Math.round(p.viewport.params.width * gl.drawingBufferWidth);
|
||||
// if (x + width >= gl.drawingBufferWidth) width = gl.drawingBufferWidth - x;
|
||||
// if (y + height >= gl.drawingBufferHeight) height = gl.drawingBufferHeight - y - 1;
|
||||
// console.log({ x, y, width, height });
|
||||
}
|
||||
|
||||
if (oldX !== x || oldY !== y || oldWidth !== width || oldHeight !== height) {
|
||||
forceNextRender = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -290,13 +290,18 @@ export class DrawPass {
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera);
|
||||
|
||||
if (transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
|
||||
this.drawTarget.bind();
|
||||
renderer.clear(false);
|
||||
}
|
||||
|
||||
if (this.wboitEnabled) {
|
||||
this._renderWboit(renderer, camera, scene, transparentBackground, postprocessingProps);
|
||||
} else {
|
||||
this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, transparentBackground, postprocessingProps);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (postprocessingEnabled) {
|
||||
this.postprocessing.target.bind();
|
||||
} else if (!toDrawingBuffer || volumeRendering || this.wboitEnabled) {
|
||||
this.colorTarget.bind();
|
||||
@@ -323,7 +328,7 @@ export class DrawPass {
|
||||
this.drawTarget.bind();
|
||||
|
||||
this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (postprocessingEnabled) {
|
||||
this.copyFboPostprocessing.render();
|
||||
} else if (volumeRendering || this.wboitEnabled) {
|
||||
this.copyFboTarget.render();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -103,7 +103,9 @@ export class ImagePass {
|
||||
} else {
|
||||
this.webgl.readPixels(viewport.x, height - viewport.y - viewport.height, w, h, array);
|
||||
}
|
||||
PixelData.flipY({ array, width: w, height: h });
|
||||
const pixelData = PixelData.create(array, w, h);
|
||||
PixelData.flipY(pixelData);
|
||||
PixelData.divideByAlpha(pixelData);
|
||||
return new ImageData(new Uint8ClampedArray(array), w, h);
|
||||
}
|
||||
}
|
||||
@@ -128,8 +128,8 @@ export class PickHelper {
|
||||
this.pickX = Math.ceil(x * this.pickScale);
|
||||
this.pickY = Math.ceil(y * this.pickScale);
|
||||
|
||||
const pickWidth = Math.ceil(width * this.pickScale);
|
||||
const pickHeight = Math.ceil(height * this.pickScale);
|
||||
const pickWidth = Math.floor(width * this.pickScale);
|
||||
const pickHeight = Math.floor(height * this.pickScale);
|
||||
|
||||
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
|
||||
this.pickWidth = pickWidth;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -13,7 +13,7 @@ import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { DrawPass } from './draw';
|
||||
@@ -70,6 +70,7 @@ const SsaoSchema = {
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
@@ -89,6 +90,7 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
|
||||
|
||||
@@ -118,6 +120,7 @@ const SsaoBlurSchema = {
|
||||
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
dOrthographic: DefineSpec('number'),
|
||||
};
|
||||
|
||||
@@ -139,6 +142,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
|
||||
|
||||
uNear: ValueCell.create(0.0),
|
||||
uFar: ValueCell.create(10000.0),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
dOrthographic: ValueCell.create(0),
|
||||
};
|
||||
|
||||
@@ -286,10 +290,14 @@ export class PostprocessingPass {
|
||||
|
||||
private readonly renderable: PostprocessingRenderable
|
||||
|
||||
private scale: number
|
||||
private ssaoScale: number
|
||||
private calcSsaoScale() {
|
||||
// downscale ssao for high pixel-ratios
|
||||
return Math.min(1, 1 / this.webgl.pixelRatio);
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, drawPass: DrawPass) {
|
||||
this.scale = 1 / this.webgl.pixelRatio;
|
||||
this.ssaoScale = this.calcSsaoScale();
|
||||
|
||||
const { colorTarget, depthTexture } = drawPass;
|
||||
const width = colorTarget.getWidth();
|
||||
@@ -298,6 +306,7 @@ export class PostprocessingPass {
|
||||
this.nSamples = 1;
|
||||
this.blurKernelSize = 1;
|
||||
|
||||
// needs to be linear for anti-aliasing pass
|
||||
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
|
||||
|
||||
this.outlinesTarget = webgl.createRenderTarget(width, height, false);
|
||||
@@ -317,14 +326,14 @@ export class PostprocessingPass {
|
||||
this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
|
||||
this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
|
||||
|
||||
const sw = Math.floor(width * this.scale);
|
||||
const sh = Math.floor(height * this.scale);
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
|
||||
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
|
||||
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
|
||||
|
||||
this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
|
||||
this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
|
||||
this.ssaoDepthBlurProxyTexture.define(sw, sh);
|
||||
this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
|
||||
|
||||
@@ -338,9 +347,13 @@ export class PostprocessingPass {
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
if (width !== w || height !== h) {
|
||||
const sw = Math.floor(width * this.scale);
|
||||
const sh = Math.floor(height * this.scale);
|
||||
const ssaoScale = this.calcSsaoScale();
|
||||
|
||||
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
|
||||
this.ssaoScale = ssaoScale;
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
this.target.setSize(width, height);
|
||||
this.outlinesTarget.setSize(width, height);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
@@ -349,8 +362,8 @@ export class PostprocessingPass {
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,8 +380,22 @@ export class PostprocessingPass {
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
if (props.occlusion.name === 'on') {
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uProjection, camera.projection);
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uInvProjection, invProjection);
|
||||
ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const b = this.ssaoRenderable.values.uBounds;
|
||||
const v = camera.viewport;
|
||||
const s = this.ssaoScale;
|
||||
Vec4.set(b.ref.value,
|
||||
Math.floor(v.x * s) / (w * s),
|
||||
Math.floor(v.y * s) / (h * s),
|
||||
Math.ceil((v.x + v.width) * s) / (w * s),
|
||||
Math.ceil((v.y + v.height) * s) / (h * s)
|
||||
);
|
||||
ValueCell.update(b, b.ref.value);
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
|
||||
@@ -376,7 +403,9 @@ export class PostprocessingPass {
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
|
||||
|
||||
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateSsaoBlur = true; }
|
||||
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
|
||||
|
||||
@@ -384,7 +413,7 @@ export class PostprocessingPass {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
this.nSamples = props.occlusion.params.samples;
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
|
||||
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
|
||||
@@ -394,10 +423,10 @@ export class PostprocessingPass {
|
||||
needsUpdateSsaoBlur = true;
|
||||
|
||||
this.blurKernelSize = props.occlusion.params.blurKernelSize;
|
||||
let kernel = getBlurKernel(this.blurKernelSize);
|
||||
const kernel = getBlurKernel(this.blurKernelSize);
|
||||
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
}
|
||||
@@ -405,8 +434,12 @@ export class PostprocessingPass {
|
||||
}
|
||||
|
||||
if (props.outline.name === 'on') {
|
||||
const factor = Math.pow(1000, props.outline.params.threshold) / 1000;
|
||||
const maxPossibleViewZDiff = factor * (camera.far - camera.near);
|
||||
let { threshold } = props.outline.params;
|
||||
// orthographic needs lower threshold
|
||||
if (camera.state.mode === 'orthographic') threshold /= 5;
|
||||
const factor = Math.pow(1000, threshold) / 1000;
|
||||
// use radiusMax for stable outlines when zooming
|
||||
const maxPossibleViewZDiff = factor * camera.state.radiusMax;
|
||||
const outlineScale = props.outline.params.scale - 1;
|
||||
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
|
||||
@@ -414,7 +447,6 @@ export class PostprocessingPass {
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, props.outline.params.threshold);
|
||||
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
|
||||
}
|
||||
@@ -467,10 +499,10 @@ export class PostprocessingPass {
|
||||
|
||||
if (props.occlusion.name === 'on') {
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
const sx = Math.floor(x * this.scale);
|
||||
const sy = Math.floor(y * this.scale);
|
||||
const sw = Math.floor(width * this.scale);
|
||||
const sh = Math.floor(height * this.scale);
|
||||
const sx = Math.floor(x * this.ssaoScale);
|
||||
const sy = Math.floor(y * this.ssaoScale);
|
||||
const sw = Math.ceil(width * this.ssaoScale);
|
||||
const sh = Math.ceil(height * this.ssaoScale);
|
||||
this.webgl.gl.viewport(sx, sy, sw, sh);
|
||||
this.webgl.gl.scissor(sx, sy, sw, sh);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export class SmaaPass {
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, input: Texture) {
|
||||
if (typeof HTMLImageElement === undefined) {
|
||||
if (typeof HTMLImageElement === 'undefined') {
|
||||
if (isDebugMode) console.log(`Missing "HTMLImageElement" required for "SMAA"`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { LocationIterator } from '../util/location-iterator';
|
||||
import { NullLocation } from '../../mol-model/location';
|
||||
import { LocationColor, ColorTheme } from '../../mol-theme/color';
|
||||
import { Geometry } from './geometry';
|
||||
import { createNullTexture, Texture } from '../../mol-gl/webgl/texture';
|
||||
|
||||
export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 'vertex' | 'vertexInstance'
|
||||
export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 'vertex' | 'vertexInstance' | 'volume' | 'volumeInstance'
|
||||
|
||||
export type ColorData = {
|
||||
uColor: ValueCell<Vec3>,
|
||||
tColor: ValueCell<TextureImage<Uint8Array>>,
|
||||
tColorGrid: ValueCell<Texture>,
|
||||
tPalette: ValueCell<TextureImage<Uint8Array>>,
|
||||
uColorTexDim: ValueCell<Vec2>,
|
||||
uColorGridDim: ValueCell<Vec3>,
|
||||
uColorGridTransform: ValueCell<Vec4>,
|
||||
dColorType: ValueCell<string>,
|
||||
dUsePalette: ValueCell<boolean>,
|
||||
}
|
||||
@@ -43,9 +48,44 @@ function _createColors(locationIt: LocationIterator, positionIt: LocationIterato
|
||||
case 'groupInstance': return createGroupInstanceColor(locationIt, colorTheme.color, colorData);
|
||||
case 'vertex': return createVertexColor(positionIt, colorTheme.color, colorData);
|
||||
case 'vertexInstance': return createVertexInstanceColor(positionIt, colorTheme.color, colorData);
|
||||
case 'volume': return createGridColor((colorTheme as any).grid, 'volume', colorData);
|
||||
case 'volumeInstance': return createGridColor((colorTheme as any).grid, 'volumeInstance', colorData);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaletteTexture(palette: ColorTheme.Palette, cell: ValueCell<TextureImage<Uint8Array>>) {
|
||||
let isSynced = true;
|
||||
const texture = cell.ref.value;
|
||||
if (palette.colors.length !== texture.width || texture.filter !== palette.filter) {
|
||||
isSynced = false;
|
||||
} else {
|
||||
const data = texture.array;
|
||||
let o = 0;
|
||||
for (const c of palette.colors) {
|
||||
const [r, g, b] = Color.toRgb(c);
|
||||
if (data[o++] !== r || data[o++] !== g || data[o++] !== b) {
|
||||
isSynced = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSynced) return;
|
||||
|
||||
const array = new Uint8Array(palette.colors.length * 3);
|
||||
let o = 0;
|
||||
for (const c of palette.colors) {
|
||||
const [r, g, b] = Color.toRgb(c);
|
||||
array[o++] = r;
|
||||
array[o++] = g;
|
||||
array[o++] = b;
|
||||
}
|
||||
|
||||
ValueCell.update(cell, { array, height: 1, width: palette.colors.length, filter: palette.filter });
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export function createValueColor(value: Color, colorData?: ColorData): ColorData {
|
||||
if (colorData) {
|
||||
ValueCell.update(colorData.uColor, Color.toVec3Normalized(colorData.uColor.ref.value, value));
|
||||
@@ -55,8 +95,11 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
|
||||
return {
|
||||
uColor: ValueCell.create(Color.toVec3Normalized(Vec3(), value)),
|
||||
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
tColorGrid: ValueCell.create(createNullTexture()),
|
||||
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
|
||||
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
uColorGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
|
||||
dColorType: ValueCell.create('uniform'),
|
||||
dUsePalette: ValueCell.create(false),
|
||||
};
|
||||
@@ -68,7 +111,9 @@ function createUniformColor(locationIt: LocationIterator, color: LocationColor,
|
||||
return createValueColor(color(NullLocation, false), colorData);
|
||||
}
|
||||
|
||||
function createTextureColor(colors: TextureImage<Uint8Array>, type: ColorType, colorData?: ColorData): ColorData {
|
||||
//
|
||||
|
||||
export function createTextureColor(colors: TextureImage<Uint8Array>, type: ColorType, colorData?: ColorData): ColorData {
|
||||
if (colorData) {
|
||||
ValueCell.update(colorData.tColor, colors);
|
||||
ValueCell.update(colorData.uColorTexDim, Vec2.create(colors.width, colors.height));
|
||||
@@ -78,8 +123,11 @@ function createTextureColor(colors: TextureImage<Uint8Array>, type: ColorType, c
|
||||
return {
|
||||
uColor: ValueCell.create(Vec3()),
|
||||
tColor: ValueCell.create(colors),
|
||||
tColorGrid: ValueCell.create(createNullTexture()),
|
||||
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(colors.width, colors.height)),
|
||||
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
uColorGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
|
||||
dColorType: ValueCell.create(type),
|
||||
dUsePalette: ValueCell.create(false),
|
||||
};
|
||||
@@ -156,33 +204,36 @@ function createVertexInstanceColor(locationIt: LocationIterator, color: Location
|
||||
return createTextureColor(colors, 'vertexInstance', colorData);
|
||||
}
|
||||
|
||||
function updatePaletteTexture(palette: ColorTheme.Palette, cell: ValueCell<TextureImage<Uint8Array>>) {
|
||||
let isSynced = true;
|
||||
const texture = cell.ref.value;
|
||||
if (palette.colors.length !== texture.width || texture.filter !== palette.filter) {
|
||||
isSynced = false;
|
||||
//
|
||||
|
||||
interface ColorVolume {
|
||||
colors: Texture
|
||||
dimension: Vec3
|
||||
transform: Vec4
|
||||
}
|
||||
|
||||
export function createGridColor(grid: ColorVolume, type: ColorType, colorData?: ColorData): ColorData {
|
||||
const { colors, dimension, transform } = grid;
|
||||
const width = colors.getWidth();
|
||||
const height = colors.getHeight();
|
||||
if (colorData) {
|
||||
ValueCell.update(colorData.tColorGrid, colors);
|
||||
ValueCell.update(colorData.uColorTexDim, Vec2.create(width, height));
|
||||
ValueCell.update(colorData.uColorGridDim, Vec3.clone(dimension));
|
||||
ValueCell.update(colorData.uColorGridTransform, Vec4.clone(transform));
|
||||
ValueCell.updateIfChanged(colorData.dColorType, type);
|
||||
return colorData;
|
||||
} else {
|
||||
const data = texture.array;
|
||||
let o = 0;
|
||||
for (const c of palette.colors) {
|
||||
const [r, g, b] = Color.toRgb(c);
|
||||
if (data[o++] !== r || data[o++] !== g || data[o++] !== b) {
|
||||
isSynced = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
uColor: ValueCell.create(Vec3()),
|
||||
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
tColorGrid: ValueCell.create(colors),
|
||||
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(width, height)),
|
||||
uColorGridDim: ValueCell.create(Vec3.clone(dimension)),
|
||||
uColorGridTransform: ValueCell.create(Vec4.clone(transform)),
|
||||
dColorType: ValueCell.create(type),
|
||||
dUsePalette: ValueCell.create(false),
|
||||
};
|
||||
}
|
||||
|
||||
if (isSynced) return;
|
||||
|
||||
const array = new Uint8Array(palette.colors.length * 3);
|
||||
let o = 0;
|
||||
for (const c of palette.colors) {
|
||||
const [r, g, b] = Color.toRgb(c);
|
||||
array[o++] = r;
|
||||
array[o++] = g;
|
||||
array[o++] = b;
|
||||
}
|
||||
|
||||
ValueCell.update(cell, { array, height: 1, width: palette.colors.length, filter: palette.filter });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface GeometryUtils<G extends Geometry, P extends PD.Params = Geometr
|
||||
createValuesSimple(geometry: G, props: Partial<PD.Values<P>>, colorValue: Color, sizeValue: number, transform?: TransformData): V
|
||||
updateValues(values: V, props: PD.Values<P>): void
|
||||
updateBoundingSphere(values: V, geometry: G): void
|
||||
createRenderableState(props: Partial<PD.Values<P>>): RenderableState
|
||||
createRenderableState(props: PD.Values<P>): RenderableState
|
||||
updateRenderableState(state: RenderableState, props: PD.Values<P>): void
|
||||
createPositionIterator(geometry: G, transform: TransformData): LocationIterator
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -25,12 +25,13 @@ const tmpCylinderMatScale = Mat4();
|
||||
const tmpCylinderStart = Vec3();
|
||||
const tmpUp = Vec3();
|
||||
|
||||
function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number) {
|
||||
function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number, matchDir: boolean) {
|
||||
Vec3.setMagnitude(tmpCylinderMatDir, dir, length / 2);
|
||||
Vec3.add(tmpCylinderCenter, start, tmpCylinderMatDir);
|
||||
// ensure the direction used to create the rotation is always pointing in the same
|
||||
// direction so the triangles of adjacent cylinder will line up
|
||||
Vec3.matchDirection(tmpUp, up, tmpCylinderMatDir);
|
||||
if (matchDir) Vec3.matchDirection(tmpUp, up, tmpCylinderMatDir);
|
||||
else Vec3.copy(tmpUp, up);
|
||||
Mat4.fromScaling(tmpCylinderMatScale, Vec3.set(tmpCylinderScale, 1, length, 1));
|
||||
Vec3.makeRotation(tmpCylinderMatRot, tmpUp, tmpCylinderMatDir);
|
||||
Mat4.mul(m, tmpCylinderMatRot, tmpCylinderMatScale);
|
||||
@@ -69,10 +70,17 @@ function getCylinder(props: CylinderProps) {
|
||||
|
||||
export type BasicCylinderProps = Omit<CylinderProps, 'height'>
|
||||
|
||||
export function addSimpleCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, props: BasicCylinderProps) {
|
||||
const d = Vec3.distance(start, end);
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
setCylinderMat(tmpCylinderMat, start, tmpCylinderDir, d, false);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props));
|
||||
}
|
||||
|
||||
export function addCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, props: BasicCylinderProps) {
|
||||
const d = Vec3.distance(start, end) * lengthScale;
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
setCylinderMat(tmpCylinderMat, start, tmpCylinderDir, d);
|
||||
setCylinderMat(tmpCylinderMat, start, tmpCylinderDir, d, true);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props));
|
||||
}
|
||||
|
||||
@@ -82,11 +90,11 @@ export function addDoubleCylinder(state: MeshBuilder.State, start: Vec3, end: Ve
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
// positivly shifted cylinder
|
||||
Vec3.add(tmpCylinderStart, start, shift);
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d);
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d, true);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
|
||||
// negativly shifted cylinder
|
||||
Vec3.sub(tmpCylinderStart, start, shift);
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d);
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d, true);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
|
||||
}
|
||||
|
||||
@@ -109,7 +117,7 @@ export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec
|
||||
const f = step * (j * 2 + 1);
|
||||
Vec3.setMagnitude(tmpCylinderDir, tmpCylinderDir, d * f);
|
||||
Vec3.add(tmpCylinderStart, start, tmpCylinderDir);
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d * step);
|
||||
setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d * step, false);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ const v3magnitude = Vec3.magnitude;
|
||||
const v3negate = Vec3.negate;
|
||||
const v3copy = Vec3.copy;
|
||||
const v3cross = Vec3.cross;
|
||||
const v3set = Vec3.set;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
@@ -43,14 +44,14 @@ function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLi
|
||||
const { vertices, normals, indices } = state;
|
||||
const vertexCount = vertices.elementCount;
|
||||
|
||||
v3fromArray(verticalLeftVector, normalVectors, offset);
|
||||
v3scale(verticalLeftVector, verticalLeftVector, leftHeight);
|
||||
v3fromArray(tA, normalVectors, offset);
|
||||
v3scale(verticalLeftVector, tA, leftHeight);
|
||||
v3scale(verticalRightVector, tA, rightHeight);
|
||||
|
||||
v3fromArray(verticalRightVector, normalVectors, offset);
|
||||
v3scale(verticalRightVector, verticalRightVector, rightHeight);
|
||||
v3fromArray(tB, binormalVectors, offset);
|
||||
v3scale(horizontalVector, tB, width);
|
||||
|
||||
v3fromArray(horizontalVector, binormalVectors, offset);
|
||||
v3scale(horizontalVector, horizontalVector, width);
|
||||
v3cross(normalVector, tB, tA);
|
||||
|
||||
v3fromArray(positionVector, controlPoints, offset);
|
||||
|
||||
@@ -73,8 +74,6 @@ function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLi
|
||||
v3copy(verticalVector, verticalLeftVector);
|
||||
}
|
||||
|
||||
v3cross(normalVector, horizontalVector, verticalVector);
|
||||
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
}
|
||||
@@ -93,6 +92,8 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
v3fromArray(tA, controlPoints, 0);
|
||||
v3fromArray(tB, controlPoints, linearSegments * 3);
|
||||
offsetLength = arrowHeight / v3magnitude(v3sub(tV, tB, tA));
|
||||
} else {
|
||||
v3set(normalOffset, 0, 0, 0);
|
||||
}
|
||||
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
@@ -119,7 +120,7 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
v3fromArray(torsionVector, binormalVectors, i3);
|
||||
|
||||
v3add(tA, v3add(tA, positionVector, horizontalVector), verticalVector);
|
||||
v3copy(tB, normalVector);
|
||||
v3add(tB, normalVector, normalOffset);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
@@ -128,7 +129,7 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
// v3add(tA, v3sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
v3add(tB, v3negate(tB, torsionVector), normalOffset);
|
||||
v3negate(tB, torsionVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
@@ -137,7 +138,7 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
// v3sub(tA, v3sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
v3negate(tB, normalVector);
|
||||
v3add(tB, v3negate(tB, normalVector), normalOffset);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
@@ -146,7 +147,7 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
// v3sub(tA, v3add(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
v3add(tB, torsionVector, normalOffset);
|
||||
v3copy(tB, torsionVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
|
||||
229
src/mol-geo/geometry/mesh/color-smoothing.ts
Normal file
229
src/mol-geo/geometry/mesh/color-smoothing.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createTextureImage, TextureImage } from '../../../mol-gl/renderable/util';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../../mol-gl/webgl/texture';
|
||||
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { getVolumeTexture2dLayout } from '../../../mol-repr/volume/util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
|
||||
interface ColorSmoothingInput {
|
||||
vertexCount: number
|
||||
instanceCount: number
|
||||
groupCount: number
|
||||
transformBuffer: Float32Array
|
||||
instanceBuffer: Float32Array
|
||||
positionBuffer: Float32Array
|
||||
groupBuffer: Float32Array
|
||||
colorData: TextureImage<Uint8Array>
|
||||
colorType: 'group' | 'groupInstance'
|
||||
boundingSphere: Sphere3D
|
||||
invariantBoundingSphere: Sphere3D
|
||||
}
|
||||
|
||||
export function calcMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl?: WebGLContext, texture?: Texture) {
|
||||
const { colorType, vertexCount, groupCount, positionBuffer, transformBuffer, groupBuffer } = input;
|
||||
|
||||
const isInstanceType = colorType.endsWith('Instance');
|
||||
const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
|
||||
|
||||
const scaleFactor = 1 / resolution;
|
||||
const scaledBox = Box3D.scale(Box3D(), box, scaleFactor);
|
||||
const gridDim = Box3D.size(Vec3(), scaledBox);
|
||||
Vec3.ceil(gridDim, gridDim);
|
||||
Vec3.add(gridDim, gridDim, Vec3.create(2, 2, 2));
|
||||
const { min } = box;
|
||||
|
||||
const [ xn, yn ] = gridDim;
|
||||
const { width, height } = getVolumeTexture2dLayout(gridDim);
|
||||
// console.log({ width, height, dim });
|
||||
|
||||
const itemSize = 3;
|
||||
const data = new Float32Array(width * height * itemSize);
|
||||
const count = new Float32Array(width * height);
|
||||
|
||||
const grid = new Uint8Array(width * height * itemSize);
|
||||
const textureImage: TextureImage<Uint8Array> = { array: grid, width, height, filter: 'linear' };
|
||||
|
||||
const instanceCount = isInstanceType ? input.instanceCount : 1;
|
||||
const colors = input.colorData.array;
|
||||
|
||||
function getIndex(x: number, y: number, z: number) {
|
||||
const column = Math.floor(((z * xn) % width) / xn);
|
||||
const row = Math.floor((z * xn) / width);
|
||||
const px = column * xn + x;
|
||||
return itemSize * ((row * yn * width) + (y * width) + px);
|
||||
}
|
||||
|
||||
const p = 2;
|
||||
const [dimX, dimY, dimZ] = gridDim;
|
||||
const v = Vec3();
|
||||
|
||||
for (let i = 0; i < instanceCount; ++i) {
|
||||
for (let j = 0; j < vertexCount; j += stride) {
|
||||
Vec3.fromArray(v, positionBuffer, j * 3);
|
||||
if (isInstanceType) Vec3.transformMat4Offset(v, v, transformBuffer, 0, 0, i * 16);
|
||||
Vec3.sub(v, v, min);
|
||||
Vec3.scale(v, v, scaleFactor);
|
||||
const [vx, vy, vz] = v;
|
||||
|
||||
// vertex mapped to grid
|
||||
const x = Math.floor(vx);
|
||||
const y = Math.floor(vy);
|
||||
const z = Math.floor(vz);
|
||||
|
||||
// group colors
|
||||
const ci = i * groupCount + groupBuffer[j];
|
||||
const r = colors[ci * 3];
|
||||
const g = colors[ci * 3 + 1];
|
||||
const b = colors[ci * 3 + 2];
|
||||
|
||||
// Extents of grid to consider for this atom
|
||||
const begX = Math.max(0, x - p);
|
||||
const begY = Math.max(0, y - p);
|
||||
const begZ = Math.max(0, z - p);
|
||||
|
||||
// Add two to these points:
|
||||
// - x, y, z are floor'd values so this ensures coverage
|
||||
// - these are loop limits (exclusive)
|
||||
const endX = Math.min(dimX, x + p + 2);
|
||||
const endY = Math.min(dimY, y + p + 2);
|
||||
const endZ = Math.min(dimZ, z + p + 2);
|
||||
|
||||
for (let xi = begX; xi < endX; ++xi) {
|
||||
const dx = xi - vx;
|
||||
for (let yi = begY; yi < endY; ++yi) {
|
||||
const dy = yi - vy;
|
||||
for (let zi = begZ; zi < endZ; ++zi) {
|
||||
const dz = zi - vz;
|
||||
const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (d > p) continue;
|
||||
|
||||
let s = p - d;
|
||||
const index = getIndex(xi, yi, zi);
|
||||
data[index] += r * s;
|
||||
data[index + 1] += g * s;
|
||||
data[index + 2] += b * s;
|
||||
count[index / 3] += s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, il = count.length; i < il; ++i) {
|
||||
const i3 = i * 3;
|
||||
const c = count[i];
|
||||
grid[i3] = Math.round(data[i3] / c);
|
||||
grid[i3 + 1] = Math.round(data[i3 + 1] / c);
|
||||
grid[i3 + 2] = Math.round(data[i3 + 2] / c);
|
||||
}
|
||||
|
||||
const gridTexDim = Vec2.create(width, height);
|
||||
const gridTransform = Vec4.create(min[0], min[1], min[2], scaleFactor);
|
||||
const type = isInstanceType ? 'volumeInstance' as const : 'volume' as const;
|
||||
|
||||
if (webgl) {
|
||||
if (!texture) texture = webgl.resources.texture('image-uint8', 'rgb', 'ubyte', 'linear');
|
||||
texture.load(textureImage);
|
||||
|
||||
return { kind: 'volume' as const, texture, gridTexDim, gridDim, gridTransform, type };
|
||||
} else {
|
||||
const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer, positionBuffer, colorType: type, grid, gridDim, gridTexDim, gridTransform, vertexStride: 3, colorStride: 3 });
|
||||
|
||||
return {
|
||||
kind: 'vertex' as const,
|
||||
texture: interpolated,
|
||||
texDim: Vec2.create(interpolated.width, interpolated.height),
|
||||
type: isInstanceType ? 'vertexInstance' : 'vertex'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
interface ColorInterpolationInput {
|
||||
vertexCount: number
|
||||
instanceCount: number
|
||||
transformBuffer: Float32Array
|
||||
positionBuffer: Float32Array
|
||||
colorType: 'volumeInstance' | 'volume'
|
||||
grid: Uint8Array // 2d layout
|
||||
gridTexDim: Vec2
|
||||
gridDim: Vec3
|
||||
gridTransform: Vec4
|
||||
vertexStride: number
|
||||
colorStride: number
|
||||
}
|
||||
|
||||
export function getTrilinearlyInterpolated(input: ColorInterpolationInput): TextureImage<Uint8Array> {
|
||||
const { vertexCount, positionBuffer, transformBuffer, grid, gridDim, gridTexDim, gridTransform, vertexStride, colorStride } = input;
|
||||
|
||||
const isInstanceType = input.colorType.endsWith('Instance');
|
||||
const instanceCount = isInstanceType ? input.instanceCount : 1;
|
||||
const image = createTextureImage(Math.max(1, instanceCount * vertexCount), 3, Uint8Array);
|
||||
const { array } = image;
|
||||
|
||||
const [xn, yn] = gridDim;
|
||||
const width = gridTexDim[0];
|
||||
const min = Vec3.fromArray(Vec3(), gridTransform, 0);
|
||||
const scaleFactor = gridTransform[3];
|
||||
|
||||
function getIndex(x: number, y: number, z: number) {
|
||||
const column = Math.floor(((z * xn) % width) / xn);
|
||||
const row = Math.floor((z * xn) / width);
|
||||
const px = column * xn + x;
|
||||
return colorStride * ((row * yn * width) + (y * width) + px);
|
||||
}
|
||||
|
||||
const v = Vec3();
|
||||
const v0 = Vec3();
|
||||
const v1 = Vec3();
|
||||
const vd = Vec3();
|
||||
|
||||
for (let i = 0; i < instanceCount; ++i) {
|
||||
for (let j = 0; j < vertexCount; ++j) {
|
||||
Vec3.fromArray(v, positionBuffer, j * vertexStride);
|
||||
if (isInstanceType) Vec3.transformMat4Offset(v, v, transformBuffer, 0, 0, i * 16);
|
||||
Vec3.sub(v, v, min);
|
||||
Vec3.scale(v, v, scaleFactor);
|
||||
|
||||
Vec3.floor(v0, v);
|
||||
Vec3.ceil(v1, v);
|
||||
|
||||
Vec3.sub(vd, v, v0);
|
||||
Vec3.sub(v, v1, v0);
|
||||
Vec3.div(vd, vd, v);
|
||||
|
||||
const [x0, y0, z0] = v0;
|
||||
const [x1, y1, z1] = v1;
|
||||
const [xd, yd, zd] = vd;
|
||||
|
||||
const s000 = Color.fromArray(grid, getIndex(x0, y0, z0));
|
||||
const s100 = Color.fromArray(grid, getIndex(x1, y0, z0));
|
||||
const s001 = Color.fromArray(grid, getIndex(x0, y0, z1));
|
||||
const s101 = Color.fromArray(grid, getIndex(x1, y0, z1));
|
||||
const s010 = Color.fromArray(grid, getIndex(x0, y1, z0));
|
||||
const s110 = Color.fromArray(grid, getIndex(x1, y1, z0));
|
||||
const s011 = Color.fromArray(grid, getIndex(x0, y1, z1));
|
||||
const s111 = Color.fromArray(grid, getIndex(x1, y1, z1));
|
||||
|
||||
const s00 = Color.interpolate(s000, s100, xd);
|
||||
const s01 = Color.interpolate(s001, s101, xd);
|
||||
const s10 = Color.interpolate(s010, s110, xd);
|
||||
const s11 = Color.interpolate(s011, s111, xd);
|
||||
|
||||
const s0 = Color.interpolate(s00, s10, yd);
|
||||
const s1 = Color.interpolate(s01, s11, yd);
|
||||
|
||||
Color.toArray(Color.interpolate(s0, s1, zd), array, (i * vertexCount + j) * 3);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
@@ -144,6 +144,14 @@ export namespace MeshBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
export function addMesh(state: State, t: Mat4, mesh: Mesh) {
|
||||
addPrimitive(state, t, {
|
||||
vertices: mesh.vertexBuffer.ref.value.subarray(0, mesh.vertexCount * 3),
|
||||
normals: mesh.normalBuffer.ref.value.subarray(0, mesh.vertexCount * 3),
|
||||
indices: mesh.indexBuffer.ref.value.subarray(0, mesh.triangleCount * 3),
|
||||
});
|
||||
}
|
||||
|
||||
export function getMesh (state: State): Mesh {
|
||||
const { vertices, normals, indices, groups, mesh } = state;
|
||||
const vb = ChunkedArray.compact(vertices, true) as Float32Array;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -49,6 +49,8 @@ export interface Mesh {
|
||||
readonly groupMapping: GroupMapping
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
|
||||
readonly meta: { [k: string]: unknown }
|
||||
}
|
||||
|
||||
export namespace Mesh {
|
||||
@@ -109,7 +111,8 @@ export namespace Mesh {
|
||||
setBoundingSphere(sphere: Sphere3D) {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(mesh);
|
||||
}
|
||||
},
|
||||
meta: {}
|
||||
};
|
||||
return mesh;
|
||||
}
|
||||
@@ -174,6 +177,18 @@ export namespace Mesh {
|
||||
ValueCell.update(mesh.vertexBuffer, v);
|
||||
}
|
||||
|
||||
export type OriginalData = {
|
||||
indexBuffer: Uint32Array
|
||||
vertexCount: number
|
||||
triangleCount: number
|
||||
}
|
||||
|
||||
/** Meshes may contain some original data in case any processing was done. */
|
||||
export function getOriginalData(x: Mesh | MeshValues) {
|
||||
const { originalData } = 'kind' in x ? x.meta : x.meta.ref.value as Mesh['meta'];
|
||||
return originalData as OriginalData | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that each vertices of each triangle have the same group id.
|
||||
* Note that normals are copied over and can't be re-created from the new mesh.
|
||||
@@ -322,6 +337,9 @@ export namespace Mesh {
|
||||
ValueCell.update(indexBuffer, newIb) as ValueCell<Uint32Array>;
|
||||
ValueCell.update(normalBuffer, newNb) as ValueCell<Float32Array>;
|
||||
|
||||
// keep some original data, e.g., for geometry export
|
||||
(mesh.meta.originalData as OriginalData) = { indexBuffer: ib, vertexCount, triangleCount };
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
@@ -369,9 +387,6 @@ export namespace Mesh {
|
||||
|
||||
function createValues(mesh: Mesh, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): MeshValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (instanceCount !== transform.instanceCount.ref.value) {
|
||||
throw new Error('instanceCount values in TransformData and LocationIterator differ');
|
||||
}
|
||||
const positionIt = createPositionIterator(mesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
@@ -406,6 +421,8 @@ export namespace Mesh {
|
||||
dFlipSided: ValueCell.create(props.flipSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
|
||||
meta: ValueCell.create(mesh.meta),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export function createSizes(locationIt: LocationIterator, sizeTheme: SizeTheme<a
|
||||
}
|
||||
}
|
||||
|
||||
const sizeFactor = 100; // NOTE same factor is set in shaders
|
||||
export const sizeDataFactor = 100; // NOTE same factor is set in shaders
|
||||
|
||||
export function getMaxSize(sizeData: SizeData): number {
|
||||
const type = sizeData.dSizeType.ref.value as SizeType;
|
||||
@@ -47,7 +47,7 @@ export function getMaxSize(sizeData: SizeData): number {
|
||||
const value = decodeFloatRGB(array[i], array[i + 1], array[i + 2]);
|
||||
if (maxSize < value) maxSize = value;
|
||||
}
|
||||
return maxSize / sizeFactor;
|
||||
return maxSize / sizeDataFactor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export function createInstanceSize(locationIt: LocationIterator, sizeFn: Locatio
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext && !locationIt.isNextNewInstance) {
|
||||
const v = locationIt.move();
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeFactor, sizes.array, v.instanceIndex * 3);
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.instanceIndex * 3);
|
||||
locationIt.skipInstance();
|
||||
}
|
||||
return createTextureSize(sizes, 'instance', sizeData);
|
||||
@@ -116,7 +116,7 @@ export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSi
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext && !locationIt.isNextNewInstance) {
|
||||
const v = locationIt.move();
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeFactor, sizes.array, v.groupIndex * 3);
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.groupIndex * 3);
|
||||
}
|
||||
return createTextureSize(sizes, 'group', sizeData);
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: Lo
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext) {
|
||||
const v = locationIt.move();
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeFactor, sizes.array, v.index * 3);
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.index * 3);
|
||||
}
|
||||
return createTextureSize(sizes, 'groupInstance', sizeData);
|
||||
}
|
||||
@@ -163,9 +163,6 @@ export namespace Spheres {
|
||||
|
||||
function createValues(spheres: Spheres, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): SpheresValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (instanceCount !== transform.instanceCount.ref.value) {
|
||||
throw new Error('instanceCount values in TransformData and LocationIterator differ');
|
||||
}
|
||||
const positionIt = createPositionIterator(spheres, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
|
||||
@@ -206,9 +206,6 @@ export namespace Text {
|
||||
|
||||
function createValues(text: Text, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (instanceCount !== transform.instanceCount.ref.value) {
|
||||
throw new Error('instanceCount values in TransformData and LocationIterator differ');
|
||||
}
|
||||
const positionIt = createPositionIterator(text, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
|
||||
344
src/mol-geo/geometry/texture-mesh/color-smoothing.ts
Normal file
344
src/mol-geo/geometry/texture-mesh/color-smoothing.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../../mol-gl/renderable';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../../mol-gl/webgl/texture';
|
||||
import { ShaderCode } from '../../../mol-gl/shader-code';
|
||||
import { createComputeRenderItem } from '../../../mol-gl/webgl/render-item';
|
||||
import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, Values, DefineSpec } from '../../../mol-gl/renderable/schema';
|
||||
import { quad_vert } from '../../../mol-gl/shader/quad.vert';
|
||||
import { normalize_frag } from '../../../mol-gl/shader/compute/color-smoothing/normalize.frag';
|
||||
import { QuadSchema, QuadValues } from '../../../mol-gl/compute/util';
|
||||
import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
|
||||
import { accumulate_frag } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.frag';
|
||||
import { accumulate_vert } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.vert';
|
||||
import { TextureImage } from '../../../mol-gl/renderable/util';
|
||||
|
||||
export const ColorAccumulateSchema = {
|
||||
drawCount: ValueSpec('number'),
|
||||
instanceCount: ValueSpec('number'),
|
||||
stride: ValueSpec('number'),
|
||||
|
||||
uTotalCount: UniformSpec('i'),
|
||||
uInstanceCount: UniformSpec('i'),
|
||||
uGroupCount: UniformSpec('i'),
|
||||
|
||||
aTransform: AttributeSpec('float32', 16, 1),
|
||||
aInstance: AttributeSpec('float32', 1, 1),
|
||||
aSample: AttributeSpec('float32', 1, 0),
|
||||
|
||||
uGeoTexDim: UniformSpec('v2', 'buffered'),
|
||||
tPosition: TextureSpec('texture', 'rgba', 'float', 'nearest'),
|
||||
tGroup: TextureSpec('texture', 'rgba', 'float', 'nearest'),
|
||||
|
||||
uColorTexDim: UniformSpec('v2'),
|
||||
tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
|
||||
dColorType: DefineSpec('string', ['group', 'groupInstance', 'vertex', 'vertexInstance']),
|
||||
|
||||
uCurrentSlice: UniformSpec('f'),
|
||||
uCurrentX: UniformSpec('f'),
|
||||
uCurrentY: UniformSpec('f'),
|
||||
uBboxMin: UniformSpec('v3', 'material'),
|
||||
uBboxSize: UniformSpec('v3', 'material'),
|
||||
uResolution: UniformSpec('f', 'material'),
|
||||
};
|
||||
type ColorAccumulateValues = Values<typeof ColorAccumulateSchema>
|
||||
const ColorAccumulateName = 'color-accumulate';
|
||||
|
||||
interface AccumulateInput {
|
||||
vertexCount: number
|
||||
instanceCount: number
|
||||
groupCount: number
|
||||
transformBuffer: Float32Array
|
||||
instanceBuffer: Float32Array
|
||||
positionTexture: Texture
|
||||
groupTexture: Texture
|
||||
colorData: TextureImage<Uint8Array>
|
||||
colorType: 'group' | 'groupInstance'
|
||||
}
|
||||
|
||||
function getSampleBuffer(sampleCount: number, stride: number) {
|
||||
const sampleBuffer = new Float32Array(sampleCount);
|
||||
for (let i = 0; i < sampleCount; ++i) {
|
||||
sampleBuffer[i] = i * stride;
|
||||
}
|
||||
return sampleBuffer;
|
||||
}
|
||||
|
||||
function getAccumulateRenderable(ctx: WebGLContext, input: AccumulateInput, box: Box3D, resolution: number, stride: number): ComputeRenderable<ColorAccumulateValues> {
|
||||
if (ctx.namedComputeRenderables[ColorAccumulateName]) {
|
||||
const extent = Vec3.sub(Vec3(), box.max, box.min);
|
||||
const v = ctx.namedComputeRenderables[ColorAccumulateName].values as ColorAccumulateValues;
|
||||
|
||||
const sampleCount = Math.round(input.vertexCount / stride);
|
||||
if (sampleCount > v.drawCount.ref.value || stride !== v.stride.ref.value) {
|
||||
ValueCell.update(v.aSample, getSampleBuffer(sampleCount, stride));
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(v.drawCount, sampleCount);
|
||||
ValueCell.updateIfChanged(v.instanceCount, input.instanceCount);
|
||||
ValueCell.updateIfChanged(v.stride, stride);
|
||||
|
||||
ValueCell.updateIfChanged(v.uTotalCount, input.vertexCount);
|
||||
ValueCell.updateIfChanged(v.uInstanceCount, input.instanceCount);
|
||||
ValueCell.updateIfChanged(v.uGroupCount, input.groupCount);
|
||||
|
||||
ValueCell.update(v.aTransform, input.transformBuffer);
|
||||
ValueCell.update(v.aInstance, input.instanceBuffer);
|
||||
|
||||
ValueCell.update(v.uGeoTexDim, Vec2.set(v.uGeoTexDim.ref.value, input.positionTexture.getWidth(), input.positionTexture.getHeight()));
|
||||
ValueCell.update(v.tPosition, input.positionTexture);
|
||||
ValueCell.update(v.tGroup, input.groupTexture);
|
||||
|
||||
ValueCell.update(v.uColorTexDim, Vec2.set(v.uColorTexDim.ref.value, input.colorData.width, input.colorData.height));
|
||||
ValueCell.update(v.tColor, input.colorData);
|
||||
ValueCell.updateIfChanged(v.dColorType, input.colorType);
|
||||
|
||||
ValueCell.updateIfChanged(v.uCurrentSlice, 0);
|
||||
ValueCell.updateIfChanged(v.uCurrentX, 0);
|
||||
ValueCell.updateIfChanged(v.uCurrentY, 0);
|
||||
ValueCell.update(v.uBboxMin, box.min);
|
||||
ValueCell.update(v.uBboxSize, extent);
|
||||
ValueCell.updateIfChanged(v.uResolution, resolution);
|
||||
|
||||
ctx.namedComputeRenderables[ColorAccumulateName].update();
|
||||
} else {
|
||||
ctx.namedComputeRenderables[ColorAccumulateName] = createAccumulateRenderable(ctx, input, box, resolution, stride);
|
||||
}
|
||||
return ctx.namedComputeRenderables[ColorAccumulateName];
|
||||
}
|
||||
|
||||
function createAccumulateRenderable(ctx: WebGLContext, input: AccumulateInput, box: Box3D, resolution: number, stride: number) {
|
||||
const extent = Vec3.sub(Vec3(), box.max, box.min);
|
||||
const sampleCount = Math.round(input.vertexCount / stride);
|
||||
|
||||
const values: ColorAccumulateValues = {
|
||||
drawCount: ValueCell.create(sampleCount),
|
||||
instanceCount: ValueCell.create(input.instanceCount),
|
||||
stride: ValueCell.create(stride),
|
||||
|
||||
uTotalCount: ValueCell.create(input.vertexCount),
|
||||
uInstanceCount: ValueCell.create(input.instanceCount),
|
||||
uGroupCount: ValueCell.create(input.groupCount),
|
||||
|
||||
aTransform: ValueCell.create(input.transformBuffer),
|
||||
aInstance: ValueCell.create(input.instanceBuffer),
|
||||
aSample: ValueCell.create(getSampleBuffer(sampleCount, stride)),
|
||||
|
||||
uGeoTexDim: ValueCell.create(Vec2.create(input.positionTexture.getWidth(), input.positionTexture.getHeight())),
|
||||
tPosition: ValueCell.create(input.positionTexture),
|
||||
tGroup: ValueCell.create(input.groupTexture),
|
||||
|
||||
uColorTexDim: ValueCell.create(Vec2.create(input.colorData.width, input.colorData.height)),
|
||||
tColor: ValueCell.create(input.colorData),
|
||||
dColorType: ValueCell.create(input.colorType),
|
||||
|
||||
uCurrentSlice: ValueCell.create(0),
|
||||
uCurrentX: ValueCell.create(0),
|
||||
uCurrentY: ValueCell.create(0),
|
||||
uBboxMin: ValueCell.create(box.min),
|
||||
uBboxSize: ValueCell.create(extent),
|
||||
uResolution: ValueCell.create(resolution),
|
||||
};
|
||||
|
||||
const schema = { ...ColorAccumulateSchema };
|
||||
const shaderCode = ShaderCode('accumulate', accumulate_vert, accumulate_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'points', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
function setAccumulateDefaults(ctx: WebGLContext) {
|
||||
const { gl, state } = ctx;
|
||||
state.disable(gl.CULL_FACE);
|
||||
state.enable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.enable(gl.SCISSOR_TEST);
|
||||
state.depthMask(false);
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
state.blendFunc(gl.ONE, gl.ONE);
|
||||
state.blendEquation(gl.FUNC_ADD);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export const ColorNormalizeSchema = {
|
||||
...QuadSchema,
|
||||
|
||||
tColor: TextureSpec('texture', 'rgba', 'float', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
};
|
||||
type ColorNormalizeValues = Values<typeof ColorNormalizeSchema>
|
||||
const ColorNormalizeName = 'color-normalize';
|
||||
|
||||
function getNormalizeRenderable(ctx: WebGLContext, color: Texture): ComputeRenderable<ColorNormalizeValues> {
|
||||
if (ctx.namedComputeRenderables[ColorNormalizeName]) {
|
||||
const v = ctx.namedComputeRenderables[ColorNormalizeName].values as ColorNormalizeValues;
|
||||
|
||||
ValueCell.update(v.tColor, color);
|
||||
ValueCell.update(v.uTexSize, Vec2.set(v.uTexSize.ref.value, color.getWidth(), color.getHeight()));
|
||||
|
||||
ctx.namedComputeRenderables[ColorNormalizeName].update();
|
||||
} else {
|
||||
ctx.namedComputeRenderables[ColorNormalizeName] = createColorNormalizeRenderable(ctx, color);
|
||||
}
|
||||
return ctx.namedComputeRenderables[ColorNormalizeName];
|
||||
}
|
||||
|
||||
function createColorNormalizeRenderable(ctx: WebGLContext, color: Texture) {
|
||||
const values: ColorNormalizeValues = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(color),
|
||||
uTexSize: ValueCell.create(Vec2.create(color.getWidth(), color.getHeight())),
|
||||
};
|
||||
|
||||
const schema = { ...ColorNormalizeSchema };
|
||||
const shaderCode = ShaderCode('normalize', quad_vert, normalize_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
function setNormalizeDefaults(ctx: WebGLContext) {
|
||||
const { gl, state } = ctx;
|
||||
state.disable(gl.CULL_FACE);
|
||||
state.enable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.enable(gl.SCISSOR_TEST);
|
||||
state.depthMask(false);
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
state.blendFunc(gl.ONE, gl.ONE);
|
||||
state.blendEquation(gl.FUNC_ADD);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function getTexture2dSize(gridDim: Vec3) {
|
||||
const area = gridDim[0] * gridDim[1] * gridDim[2];
|
||||
const squareDim = Math.sqrt(area);
|
||||
const powerOfTwoSize = Math.pow(2, Math.ceil(Math.log(squareDim) / Math.log(2)));
|
||||
|
||||
let texDimX = 0;
|
||||
let texDimY = gridDim[1];
|
||||
let texRows = 1;
|
||||
let texCols = gridDim[2];
|
||||
if (powerOfTwoSize < gridDim[0] * gridDim[2]) {
|
||||
texCols = Math.floor(powerOfTwoSize / gridDim[0]);
|
||||
texRows = Math.ceil(gridDim[2] / texCols);
|
||||
texDimX = texCols * gridDim[0];
|
||||
texDimY *= texRows;
|
||||
} else {
|
||||
texDimX = gridDim[0] * gridDim[2];
|
||||
}
|
||||
// console.log(texDimX, texDimY, texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2);
|
||||
return { texDimX, texDimY, texRows, texCols, powerOfTwoSize: texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
|
||||
}
|
||||
|
||||
interface ColorSmoothingInput extends AccumulateInput {
|
||||
boundingSphere: Sphere3D
|
||||
invariantBoundingSphere: Sphere3D
|
||||
}
|
||||
|
||||
export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl: WebGLContext, texture?: Texture) {
|
||||
const { gl, resources, state, extensions: { colorBufferHalfFloat, textureHalfFloat } } = webgl;
|
||||
|
||||
const isInstanceType = input.colorType.endsWith('Instance');
|
||||
const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
|
||||
|
||||
const scaleFactor = 1 / resolution;
|
||||
const scaledBox = Box3D.scale(Box3D(), box, scaleFactor);
|
||||
const gridDim = Box3D.size(Vec3(), scaledBox);
|
||||
Vec3.ceil(gridDim, gridDim);
|
||||
Vec3.add(gridDim, gridDim, Vec3.create(2, 2, 2));
|
||||
const { min } = box;
|
||||
|
||||
const [ dx, dy, dz ] = gridDim;
|
||||
const { texDimX: width, texDimY: height, texCols } = getTexture2dSize(gridDim);
|
||||
// console.log({ width, height, texCols, dim, resolution });
|
||||
|
||||
if (!webgl.namedTextures[ColorAccumulateName]) {
|
||||
webgl.namedTextures[ColorAccumulateName] = colorBufferHalfFloat && textureHalfFloat
|
||||
? resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
|
||||
: resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
}
|
||||
const accumulateTexture = webgl.namedTextures[ColorAccumulateName];
|
||||
accumulateTexture.define(width, height);
|
||||
|
||||
const accumulateRenderable = getAccumulateRenderable(webgl, input, box, resolution, stride);
|
||||
|
||||
//
|
||||
|
||||
const { uCurrentSlice, uCurrentX, uCurrentY } = accumulateRenderable.values;
|
||||
|
||||
if (!webgl.namedFramebuffers[ColorAccumulateName]) {
|
||||
webgl.namedFramebuffers[ColorAccumulateName] = webgl.resources.framebuffer();
|
||||
}
|
||||
const framebuffer = webgl.namedFramebuffers[ColorAccumulateName];
|
||||
framebuffer.bind();
|
||||
|
||||
setAccumulateDefaults(webgl);
|
||||
state.currentRenderItemId = -1;
|
||||
accumulateTexture.attachFramebuffer(framebuffer, 0);
|
||||
gl.viewport(0, 0, width, height);
|
||||
gl.scissor(0, 0, width, height);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
ValueCell.update(uCurrentY, 0);
|
||||
let currCol = 0;
|
||||
let currY = 0;
|
||||
let currX = 0;
|
||||
for (let i = 0; i < dz; ++i) {
|
||||
if (currCol >= texCols) {
|
||||
currCol -= texCols;
|
||||
currY += dy;
|
||||
currX = 0;
|
||||
ValueCell.update(uCurrentY, currY);
|
||||
}
|
||||
// console.log({ i, currX, currY });
|
||||
ValueCell.update(uCurrentX, currX);
|
||||
ValueCell.update(uCurrentSlice, i);
|
||||
gl.viewport(currX, currY, dx, dy);
|
||||
gl.scissor(currX, currY, dx, dy);
|
||||
accumulateRenderable.render();
|
||||
++currCol;
|
||||
currX += dx;
|
||||
}
|
||||
|
||||
// const accImage = new Float32Array(width * height * 4);
|
||||
// accumulateTexture.attachFramebuffer(framebuffer, 0);
|
||||
// webgl.readPixels(0, 0, width, height, accImage);
|
||||
// console.log(accImage);
|
||||
// printTextureImage({ array: accImage, width, height }, 1 / 4);
|
||||
|
||||
// normalize
|
||||
|
||||
if (!texture) texture = resources.texture('image-uint8', 'rgb', 'ubyte', 'linear');
|
||||
texture.define(width, height);
|
||||
|
||||
const normalizeRenderable = getNormalizeRenderable(webgl, accumulateTexture);
|
||||
|
||||
setNormalizeDefaults(webgl);
|
||||
state.currentRenderItemId = -1;
|
||||
texture.attachFramebuffer(framebuffer, 0);
|
||||
gl.viewport(0, 0, width, height);
|
||||
gl.scissor(0, 0, width, height);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
normalizeRenderable.render();
|
||||
|
||||
// const normImage = new Uint8Array(width * height * 4);
|
||||
// texture.attachFramebuffer(framebuffer, 0);
|
||||
// webgl.readPixels(0, 0, width, height, normImage);
|
||||
// console.log(normImage);
|
||||
// printTextureImage({ array: normImage, width, height }, 1 / 4);
|
||||
|
||||
const gridTransform = Vec4.create(min[0], min[1], min[2], scaleFactor);
|
||||
const type = isInstanceType ? 'volumeInstance' : 'volume';
|
||||
|
||||
return { texture, gridDim, gridTexDim: Vec2.create(width, height), gridTransform, type };
|
||||
}
|
||||
@@ -39,6 +39,8 @@ export interface TextureMesh {
|
||||
readonly doubleBuffer: TextureMesh.DoubleBuffer
|
||||
|
||||
readonly boundingSphere: Sphere3D
|
||||
|
||||
meta?: unknown
|
||||
}
|
||||
|
||||
export namespace TextureMesh {
|
||||
|
||||
78
src/mol-geo/primitive/torus.ts
Normal file
78
src/mol-geo/primitive/torus.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
// adapted from three.js, MIT License Copyright 2010-2021 three.js authors
|
||||
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Primitive } from './primitive';
|
||||
|
||||
export const DefaultTorusProps = {
|
||||
radius: 1,
|
||||
tube: 0.4,
|
||||
radialSegments: 8,
|
||||
tubularSegments: 6,
|
||||
arc: Math.PI * 2,
|
||||
};
|
||||
export type TorusProps = Partial<typeof DefaultTorusProps>
|
||||
|
||||
export function Torus(props?: TorusProps): Primitive {
|
||||
const { radius, tube, radialSegments, tubularSegments, arc } = { ...DefaultTorusProps, ...props };
|
||||
|
||||
// buffers
|
||||
const indices: number[] = [];
|
||||
const vertices: number[] = [];
|
||||
const normals: number[] = [];
|
||||
|
||||
// helper variables
|
||||
const center = Vec3();
|
||||
const vertex = Vec3();
|
||||
const normal = Vec3();
|
||||
|
||||
// generate vertices and normals
|
||||
for (let j = 0; j <= radialSegments; ++j) {
|
||||
for (let i = 0; i <= tubularSegments; ++i) {
|
||||
const u = i / tubularSegments * arc;
|
||||
const v = j / radialSegments * Math.PI * 2;
|
||||
|
||||
// vertex
|
||||
Vec3.set(
|
||||
vertex,
|
||||
(radius + tube * Math.cos(v)) * Math.cos(u),
|
||||
(radius + tube * Math.cos(v)) * Math.sin(u),
|
||||
tube * Math.sin(v)
|
||||
);
|
||||
vertices.push(...vertex);
|
||||
|
||||
// normal
|
||||
Vec3.set(center, radius * Math.cos(u), radius * Math.sin(u), 0 );
|
||||
Vec3.sub(normal, vertex, center);
|
||||
Vec3.normalize(normal, normal);
|
||||
normals.push(...normal);
|
||||
}
|
||||
}
|
||||
|
||||
// generate indices
|
||||
for (let j = 1; j <= radialSegments; ++j) {
|
||||
for (let i = 1; i <= tubularSegments; ++i) {
|
||||
|
||||
// indices
|
||||
const a = (tubularSegments + 1) * j + i - 1;
|
||||
const b = (tubularSegments + 1) * (j - 1) + i - 1;
|
||||
const c = (tubularSegments + 1) * (j - 1) + i;
|
||||
const d = (tubularSegments + 1) * j + i;
|
||||
|
||||
// faces
|
||||
indices.push(a, b, d);
|
||||
indices.push(b, c, d);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vertices: new Float32Array(vertices),
|
||||
normals: new Float32Array(normals),
|
||||
indices: new Uint32Array(indices)
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema, ValueSpec } from './schema';
|
||||
import { MeshShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -22,6 +22,7 @@ export const MeshSchema = {
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('boolean'),
|
||||
meta: ValueSpec('unknown')
|
||||
} as const;
|
||||
export type MeshSchema = typeof MeshSchema
|
||||
export type MeshValues = Values<MeshSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -17,6 +17,7 @@ export type ValueKindType = {
|
||||
'string': string
|
||||
'boolean': boolean
|
||||
'any': any
|
||||
'unknown': unknown
|
||||
|
||||
'm4': Mat4,
|
||||
'float32': Float32Array
|
||||
@@ -184,9 +185,12 @@ export const ColorSchema = {
|
||||
// aColor: AttributeSpec('float32', 3, 0), // TODO
|
||||
uColor: UniformSpec('v3', 'material'),
|
||||
uColorTexDim: UniformSpec('v2'),
|
||||
uColorGridDim: UniformSpec('v3'),
|
||||
uColorGridTransform: UniformSpec('v4'),
|
||||
tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
|
||||
tPalette: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
|
||||
dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'groupInstance', 'vertex', 'vertexInstance']),
|
||||
tColorGrid: TextureSpec('texture', 'rgb', 'ubyte', 'linear'),
|
||||
dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'groupInstance', 'vertex', 'vertexInstance', 'volume', 'volumeInstance']),
|
||||
dUsePalette: DefineSpec('boolean'),
|
||||
} as const;
|
||||
export type ColorSchema = typeof ColorSchema
|
||||
|
||||
@@ -74,7 +74,7 @@ export function printImageData(imageData: ImageData, scale = 1, pixelated = fals
|
||||
// not supported in Firefox and IE
|
||||
img.style.imageRendering = 'pixelated';
|
||||
}
|
||||
img.style.position = 'absolute';
|
||||
img.style.position = 'relative';
|
||||
img.style.top = '0px';
|
||||
img.style.left = '0px';
|
||||
img.style.border = 'solid grey';
|
||||
|
||||
@@ -110,10 +110,18 @@ export const RendererParams = {
|
||||
};
|
||||
export type RendererProps = PD.Values<typeof RendererParams>
|
||||
|
||||
function getStyle(props: RendererProps['style']) {
|
||||
export type Style = {
|
||||
lightIntensity: number
|
||||
ambientIntensity: number
|
||||
metalness: number
|
||||
roughness: number
|
||||
reflectivity: number
|
||||
}
|
||||
|
||||
export function getStyle(props: RendererProps['style']): Style {
|
||||
switch (props.name) {
|
||||
case 'custom':
|
||||
return props.params;
|
||||
return props.params as Style;
|
||||
case 'flat':
|
||||
return {
|
||||
lightIntensity: 0, ambientIntensity: 1,
|
||||
@@ -190,7 +198,7 @@ namespace Renderer {
|
||||
|
||||
let transparentBackground = false;
|
||||
|
||||
const nullDepthTexture = createNullTexture(gl, 'image-depth');
|
||||
const nullDepthTexture = createNullTexture(gl);
|
||||
const sharedTexturesList: Textures = [
|
||||
['tDepth', nullDepthTexture]
|
||||
];
|
||||
|
||||
@@ -12,6 +12,12 @@ export const assign_color_varying = `
|
||||
vColor.rgb = readFromTexture(tColor, VertexID, uColorTexDim).rgb;
|
||||
#elif defined(dColorType_vertexInstance)
|
||||
vColor.rgb = readFromTexture(tColor, int(aInstance) * uVertexCount + VertexID, uColorTexDim).rgb;
|
||||
#elif defined(dColorType_volume)
|
||||
vec3 gridPos = (uColorGridTransform.w * (position - uColorGridTransform.xyz)) / uColorGridDim;
|
||||
vColor.rgb = texture3dFrom2dLinear(tColorGrid, gridPos, uColorGridDim, uColorTexDim).rgb;
|
||||
#elif defined(dColorType_volumeInstance)
|
||||
vec3 gridPos = (uColorGridTransform.w * (vModelPosition - uColorGridTransform.xyz)) / uColorGridDim;
|
||||
vColor.rgb = texture3dFrom2dLinear(tColorGrid, gridPos, uColorGridDim, uColorTexDim).rgb;
|
||||
#endif
|
||||
|
||||
#ifdef dUsePalette
|
||||
|
||||
@@ -7,7 +7,8 @@ mat4 modelView = uView * model;
|
||||
vec3 position = aPosition;
|
||||
#endif
|
||||
vec4 position4 = vec4(position, 1.0);
|
||||
vModelPosition = (model * position4).xyz; // for clipping in frag shader
|
||||
// for accessing tColorGrid in vert shader and for clipping in frag shader
|
||||
vModelPosition = (model * position4).xyz;
|
||||
vec4 mvPosition = modelView * position4;
|
||||
vViewPosition = mvPosition.xyz;
|
||||
gl_Position = uProjection * mvPosition;
|
||||
|
||||
@@ -9,6 +9,12 @@ export const color_vert_params = `
|
||||
varying vec4 vColor;
|
||||
uniform vec2 uColorTexDim;
|
||||
uniform sampler2D tColor;
|
||||
#elif defined(dColorType_grid)
|
||||
varying vec4 vColor;
|
||||
uniform vec2 uColorTexDim;
|
||||
uniform vec3 uColorGridDim;
|
||||
uniform vec4 uColorGridTransform;
|
||||
uniform sampler2D tColorGrid;
|
||||
#endif
|
||||
|
||||
#ifdef dOverpaint
|
||||
|
||||
@@ -13,7 +13,11 @@ export const common = `
|
||||
#define dColorType_texture
|
||||
#endif
|
||||
|
||||
#if defined(dColorType_attribute) || defined(dColorType_texture)
|
||||
#if defined(dColorType_volume) || defined(dColorType_volumeInstance)
|
||||
#define dColorType_grid
|
||||
#endif
|
||||
|
||||
#if defined(dColorType_attribute) || defined(dColorType_texture) || defined(dColorType_grid)
|
||||
#define dColorType_varying
|
||||
#endif
|
||||
|
||||
|
||||
28
src/mol-gl/shader/compute/color-smoothing/accumulate.frag.ts
Normal file
28
src/mol-gl/shader/compute/color-smoothing/accumulate.frag.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const accumulate_frag = `
|
||||
precision highp float;
|
||||
|
||||
varying vec3 vPosition;
|
||||
varying vec3 vColor;
|
||||
|
||||
uniform float uCurrentSlice;
|
||||
uniform float uCurrentX;
|
||||
uniform float uCurrentY;
|
||||
uniform float uResolution;
|
||||
|
||||
const float p = 2.0;
|
||||
|
||||
void main() {
|
||||
vec2 v = gl_FragCoord.xy - vec2(uCurrentX, uCurrentY) - 0.5;
|
||||
vec3 fragPos = vec3(v.x, v.y, uCurrentSlice);
|
||||
float dist = distance(fragPos, vPosition);
|
||||
if (dist > p) discard;
|
||||
|
||||
gl_FragColor = vec4(vColor, 1.0) * (p - dist);
|
||||
}
|
||||
`;
|
||||
51
src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts
Normal file
51
src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const accumulate_vert = `
|
||||
precision highp float;
|
||||
|
||||
#include common
|
||||
#include read_from_texture
|
||||
|
||||
uniform int uTotalCount;
|
||||
uniform int uGroupCount;
|
||||
|
||||
attribute float aSample;
|
||||
#define SampleID int(aSample)
|
||||
|
||||
attribute mat4 aTransform;
|
||||
attribute float aInstance;
|
||||
|
||||
uniform vec2 uGeoTexDim;
|
||||
uniform sampler2D tPosition;
|
||||
uniform sampler2D tGroup;
|
||||
|
||||
uniform vec2 uColorTexDim;
|
||||
uniform sampler2D tColor;
|
||||
|
||||
varying vec3 vPosition;
|
||||
varying vec3 vColor;
|
||||
|
||||
uniform vec3 uBboxSize;
|
||||
uniform vec3 uBboxMin;
|
||||
uniform float uResolution;
|
||||
|
||||
void main() {
|
||||
vec3 position = readFromTexture(tPosition, SampleID, uGeoTexDim).xyz;
|
||||
float group = decodeFloatRGB(readFromTexture(tGroup, SampleID, uGeoTexDim).rgb);
|
||||
|
||||
position = (aTransform * vec4(position, 1.0)).xyz;
|
||||
gl_PointSize = 7.0;
|
||||
vPosition = (position - uBboxMin) / uResolution;
|
||||
gl_Position = vec4(((position - uBboxMin) / uBboxSize) * 2.0 - 1.0, 1.0);
|
||||
|
||||
#if defined(dColorType_group)
|
||||
vColor = readFromTexture(tColor, group, uColorTexDim).rgb;
|
||||
#elif defined(dColorType_groupInstance)
|
||||
vColor = readFromTexture(tColor, aInstance * float(uGroupCount) + group, uColorTexDim).rgb;
|
||||
#endif
|
||||
}
|
||||
`;
|
||||
20
src/mol-gl/shader/compute/color-smoothing/normalize.frag.ts
Normal file
20
src/mol-gl/shader/compute/color-smoothing/normalize.frag.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const normalize_frag = `
|
||||
precision highp float;
|
||||
precision highp sampler2D;
|
||||
|
||||
uniform sampler2D tColor;
|
||||
uniform vec2 uTexSize;
|
||||
|
||||
void main(void) {
|
||||
vec2 coords = gl_FragCoord.xy / uTexSize;
|
||||
vec4 color = texture2D(tColor, coords);
|
||||
|
||||
gl_FragColor.rgb = color.rgb / color.a;
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -15,6 +15,10 @@ precision highp sampler2D;
|
||||
#include color_vert_params
|
||||
#include common_clip
|
||||
|
||||
#if defined(dColorType_grid)
|
||||
#include texture3d_from_2d_linear
|
||||
#endif
|
||||
|
||||
#ifdef dGeoTexture
|
||||
uniform vec2 uGeoTexDim;
|
||||
uniform sampler2D tPosition;
|
||||
@@ -32,10 +36,10 @@ varying vec3 vNormal;
|
||||
|
||||
void main(){
|
||||
#include assign_group
|
||||
#include assign_color_varying
|
||||
#include assign_marker_varying
|
||||
#include assign_clipping_varying
|
||||
#include assign_position
|
||||
#include assign_color_varying
|
||||
#include clip_instance
|
||||
|
||||
#ifdef dGeoTexture
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -26,9 +26,6 @@ uniform bool uTransparentBackground;
|
||||
uniform float uOcclusionBias;
|
||||
uniform float uOcclusionRadius;
|
||||
|
||||
uniform float uOutlineScale;
|
||||
uniform float uOutlineThreshold;
|
||||
|
||||
uniform float uMaxPossibleViewZDiff;
|
||||
|
||||
const vec3 occlusionColor = vec3(0.0);
|
||||
@@ -88,7 +85,8 @@ float getSsao(vec2 coords) {
|
||||
} else if (rawSsao > 0.001) {
|
||||
return rawSsao;
|
||||
}
|
||||
return 0.0;
|
||||
// treat values close to 0.0 as errors and return no occlusion
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const ssaoBlur_frag = `
|
||||
@@ -11,6 +12,7 @@ precision highp sampler2D;
|
||||
|
||||
uniform sampler2D tSsaoDepth;
|
||||
uniform vec2 uTexSize;
|
||||
uniform vec4 uBounds;
|
||||
|
||||
uniform float uKernel[dOcclusionKernelSize];
|
||||
|
||||
@@ -36,16 +38,25 @@ bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
bool outsideBounds(const in vec2 p) {
|
||||
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
vec2 coords = gl_FragCoord.xy / uTexSize;
|
||||
|
||||
vec2 packedDepth = texture2D(tSsaoDepth, coords).zw;
|
||||
|
||||
if (outsideBounds(coords)) {
|
||||
gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
float selfDepth = unpackRGToUnitInterval(packedDepth);
|
||||
// if background and if second pass
|
||||
if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
|
||||
gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
|
||||
return;
|
||||
gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
float selfViewZ = getViewZ(selfDepth);
|
||||
@@ -57,6 +68,9 @@ void main(void) {
|
||||
// only if kernelSize is odd
|
||||
for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
|
||||
vec2 sampleCoords = coords + float(i) * offset;
|
||||
if (outsideBounds(sampleCoords)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -13,14 +13,14 @@ precision highp sampler2D;
|
||||
#include common
|
||||
|
||||
uniform sampler2D tDepth;
|
||||
uniform vec2 uTexSize;
|
||||
uniform vec4 uBounds;
|
||||
|
||||
uniform vec3 uSamples[dNSamples];
|
||||
|
||||
uniform mat4 uProjection;
|
||||
uniform mat4 uInvProjection;
|
||||
|
||||
uniform vec2 uTexSize;
|
||||
|
||||
uniform float uRadius;
|
||||
uniform float uBias;
|
||||
|
||||
@@ -46,8 +46,12 @@ bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
bool outsideBounds(const in vec2 p) {
|
||||
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
|
||||
}
|
||||
|
||||
float getDepth(const in vec2 coords) {
|
||||
return unpackRGBAToDepth(texture2D(tDepth, coords));
|
||||
return outsideBounds(coords) ? 1.0 : unpackRGBAToDepth(texture2D(tDepth, coords));
|
||||
}
|
||||
|
||||
vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {
|
||||
|
||||
@@ -88,7 +88,7 @@ export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResou
|
||||
export function createNullRenderTarget(gl: GLRenderingContext): RenderTarget {
|
||||
return {
|
||||
id: getNextRenderTargetId(),
|
||||
texture: createNullTexture(gl, 'image-uint8'),
|
||||
texture: createNullTexture(gl),
|
||||
framebuffer: createNullFramebuffer(),
|
||||
|
||||
getWidth: () => 0,
|
||||
|
||||
@@ -169,7 +169,7 @@ export function getAttachment(gl: GLRenderingContext, extensions: WebGLExtension
|
||||
}
|
||||
|
||||
function isImage(x: TextureImage<any> | TextureVolume<any> | HTMLImageElement): x is HTMLImageElement {
|
||||
return typeof HTMLImageElement !== undefined && (x instanceof HTMLImageElement);
|
||||
return typeof HTMLImageElement !== 'undefined' && (x instanceof HTMLImageElement);
|
||||
}
|
||||
|
||||
function isTexture2d(x: TextureImage<any> | TextureVolume<any>, target: number, gl: GLRenderingContext): x is TextureImage<any> {
|
||||
@@ -186,6 +186,7 @@ export interface Texture {
|
||||
readonly format: number
|
||||
readonly internalFormat: number
|
||||
readonly type: number
|
||||
readonly filter: number
|
||||
|
||||
getWidth: () => number
|
||||
getHeight: () => number
|
||||
@@ -326,6 +327,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
|
||||
format,
|
||||
internalFormat,
|
||||
type,
|
||||
filter,
|
||||
|
||||
getWidth: () => width,
|
||||
getHeight: () => height,
|
||||
@@ -394,7 +396,7 @@ export function createTextures(ctx: WebGLContext, schema: RenderableSchema, valu
|
||||
|
||||
/**
|
||||
* Loads an image from a url to a textures and triggers update asynchronously.
|
||||
* This will not work on node.js with a polyfill for HTMLImageElement.
|
||||
* This will not work on node.js without a polyfill for `HTMLImageElement`.
|
||||
*/
|
||||
export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture: Texture) {
|
||||
const img = new Image();
|
||||
@@ -407,14 +409,15 @@ export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture:
|
||||
|
||||
//
|
||||
|
||||
export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Texture {
|
||||
const target = getTarget(gl, kind);
|
||||
export function createNullTexture(gl?: GLRenderingContext): Texture {
|
||||
const target = gl?.TEXTURE_2D ?? 3553;
|
||||
return {
|
||||
id: getNextTextureId(),
|
||||
target,
|
||||
format: 0,
|
||||
internalFormat: 0,
|
||||
type: 0,
|
||||
filter: 0,
|
||||
|
||||
getWidth: () => 0,
|
||||
getHeight: () => 0,
|
||||
@@ -424,12 +427,16 @@ export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Te
|
||||
define: () => {},
|
||||
load: () => {},
|
||||
bind: (id: TextureId) => {
|
||||
gl.activeTexture(gl.TEXTURE0 + id);
|
||||
gl.bindTexture(target, null);
|
||||
if (gl) {
|
||||
gl.activeTexture(gl.TEXTURE0 + id);
|
||||
gl.bindTexture(target, null);
|
||||
}
|
||||
},
|
||||
unbind: (id: TextureId) => {
|
||||
gl.activeTexture(gl.TEXTURE0 + id);
|
||||
gl.bindTexture(target, null);
|
||||
if (gl) {
|
||||
gl.activeTexture(gl.TEXTURE0 + id);
|
||||
gl.bindTexture(target, null);
|
||||
}
|
||||
},
|
||||
attachFramebuffer: () => {},
|
||||
detachFramebuffer: () => {},
|
||||
@@ -437,4 +444,4 @@ export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Te
|
||||
reset: () => {},
|
||||
destroy: () => {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
11
src/mol-io/common/ascii.ts
Normal file
11
src/mol-io/common/ascii.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
|
||||
export function asciiWrite(data: Uint8Array, str: string) {
|
||||
for (let i = 0, il = str.length; i < il; ++i) {
|
||||
data[i] = str.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
import { defaults, noop } from '../../mol-util';
|
||||
import { SimpleBuffer } from './simple-buffer';
|
||||
// only import 'fs' in node.js
|
||||
const fs = typeof document === 'undefined' ? require('fs') as typeof import('fs') : void 0;
|
||||
|
||||
export interface FileHandle {
|
||||
name: string
|
||||
@@ -83,55 +81,4 @@ export namespace FileHandle {
|
||||
close: noop
|
||||
};
|
||||
}
|
||||
|
||||
export function fromDescriptor(file: number, name: string): FileHandle {
|
||||
if (fs === undefined) throw new Error('fs module not available');
|
||||
return {
|
||||
name,
|
||||
readBuffer: (position: number, sizeOrBuffer: SimpleBuffer | number, length?: number, byteOffset?: number) => {
|
||||
return new Promise((res, rej) => {
|
||||
let outBuffer: SimpleBuffer;
|
||||
if (typeof sizeOrBuffer === 'number') {
|
||||
byteOffset = defaults(byteOffset, 0);
|
||||
length = defaults(length, sizeOrBuffer);
|
||||
outBuffer = SimpleBuffer.fromArrayBuffer(new ArrayBuffer(sizeOrBuffer));
|
||||
} else {
|
||||
byteOffset = defaults(byteOffset, 0);
|
||||
length = defaults(length, sizeOrBuffer.length);
|
||||
outBuffer = sizeOrBuffer;
|
||||
}
|
||||
fs.read(file, outBuffer, byteOffset, length, position, (err, bytesRead, buffer) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
return;
|
||||
}
|
||||
if (length !== bytesRead) {
|
||||
console.warn(`byteCount ${length} and bytesRead ${bytesRead} differ`);
|
||||
}
|
||||
res({ bytesRead, buffer });
|
||||
});
|
||||
});
|
||||
},
|
||||
writeBuffer: (position: number, buffer: SimpleBuffer, length?: number) => {
|
||||
length = defaults(length, buffer.length);
|
||||
return new Promise<number>((res, rej) => {
|
||||
fs.write(file, buffer, 0, length, position, (err, written) => {
|
||||
if (err) rej(err);
|
||||
else res(written);
|
||||
});
|
||||
});
|
||||
},
|
||||
writeBufferSync: (position: number, buffer: Uint8Array, length?: number) => {
|
||||
length = defaults(length, buffer.length);
|
||||
return fs.writeSync(file, buffer, 0, length, position);
|
||||
},
|
||||
close: () => {
|
||||
try {
|
||||
if (file !== void 0) fs.close(file, noop);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ M END
|
||||
> <DATABASE_NAME>
|
||||
drugbank
|
||||
|
||||
> <SMILES>
|
||||
[O-]P([O-])([O-])=O
|
||||
> 5225 <TEST_FIELD>
|
||||
whatever
|
||||
|
||||
> <INCHI_IDENTIFIER>
|
||||
InChI=1S/H3O4P/c1-5(2,3)4/h(H3,1,2,3,4)/p-3
|
||||
@@ -362,22 +362,25 @@ describe('sdf reader', () => {
|
||||
expect(bonds.atomIdxB.value(3)).toBe(5);
|
||||
expect(bonds.order.value(3)).toBe(1);
|
||||
|
||||
expect(dataItems.dataHeader.value(0)).toBe('DATABASE_ID');
|
||||
expect(dataItems.dataHeader.value(0)).toBe('<DATABASE_ID>');
|
||||
expect(dataItems.data.value(0)).toBe('0');
|
||||
|
||||
expect(dataItems.dataHeader.value(1)).toBe('DATABASE_NAME');
|
||||
expect(dataItems.dataHeader.value(1)).toBe('<DATABASE_NAME>');
|
||||
expect(dataItems.data.value(1)).toBe('drugbank');
|
||||
|
||||
expect(dataItems.dataHeader.value(31)).toBe('SYNONYMS');
|
||||
expect(dataItems.dataHeader.value(2)).toBe('5225 <TEST_FIELD>');
|
||||
expect(dataItems.data.value(2)).toBe('whatever');
|
||||
|
||||
expect(dataItems.dataHeader.value(31)).toBe('<SYNONYMS>');
|
||||
expect(dataItems.data.value(31)).toBe('Orthophosphate; Phosphate');
|
||||
|
||||
expect(compound1.dataItems.data.value(0)).toBe('0');
|
||||
expect(compound2.dataItems.data.value(0)).toBe('1');
|
||||
|
||||
expect(compound3.dataItems.dataHeader.value(2)).toBe('PUBCHEM_CONFORMER_DIVERSEORDER');
|
||||
expect(compound3.dataItems.dataHeader.value(2)).toBe('<PUBCHEM_CONFORMER_DIVERSEORDER>');
|
||||
expect(compound3.dataItems.data.value(2)).toBe('1\n11\n10\n3\n15\n17\n13\n5\n16\n7\n14\n9\n8\n4\n18\n6\n12\n2');
|
||||
|
||||
expect(compound3.dataItems.dataHeader.value(21)).toBe('PUBCHEM_COORDINATE_TYPE');
|
||||
expect(compound3.dataItems.dataHeader.value(21)).toBe('<PUBCHEM_COORDINATE_TYPE>');
|
||||
expect(compound3.dataItems.data.value(21)).toBe('2\n5\n10');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,16 +13,20 @@ import { Tokenizer, TokenBuilder } from '../common/text/tokenizer';
|
||||
import { TokenColumnProvider as TokenColumn } from '../common/text/column/token';
|
||||
|
||||
/** http://c4.cabrillo.edu/404/ctfile.pdf - page 41 */
|
||||
export interface SdfFile {
|
||||
readonly compounds: {
|
||||
readonly molFile: MolFile,
|
||||
readonly dataItems: {
|
||||
readonly dataHeader: Column<string>,
|
||||
readonly data: Column<string>
|
||||
}
|
||||
}[]
|
||||
|
||||
export interface SdfFileCompound {
|
||||
readonly molFile: MolFile,
|
||||
readonly dataItems: {
|
||||
readonly dataHeader: Column<string>,
|
||||
readonly data: Column<string>
|
||||
}
|
||||
}
|
||||
|
||||
export interface SdfFile {
|
||||
readonly compounds: SdfFileCompound[]
|
||||
}
|
||||
|
||||
|
||||
const delimiter = '$$$$';
|
||||
function handleDataItems(tokenizer: Tokenizer): { dataHeader: Column<string>, data: Column<string> } {
|
||||
const dataHeader = TokenBuilder.create(tokenizer.data, 32);
|
||||
@@ -33,8 +37,8 @@ function handleDataItems(tokenizer: Tokenizer): { dataHeader: Column<string>, da
|
||||
if (line.startsWith(delimiter)) break;
|
||||
if (!line) continue;
|
||||
|
||||
if (line.startsWith('> <')) {
|
||||
TokenBuilder.add(dataHeader, tokenizer.tokenStart + 3, tokenizer.tokenEnd - 1);
|
||||
if (line.startsWith('> ')) {
|
||||
TokenBuilder.add(dataHeader, tokenizer.tokenStart + 2, tokenizer.tokenEnd);
|
||||
|
||||
Tokenizer.markLine(tokenizer);
|
||||
const start = tokenizer.tokenStart;
|
||||
@@ -42,7 +46,7 @@ function handleDataItems(tokenizer: Tokenizer): { dataHeader: Column<string>, da
|
||||
let added = false;
|
||||
while (tokenizer.position < tokenizer.length) {
|
||||
const line2 = Tokenizer.readLine(tokenizer);
|
||||
if (!line2 || line2.startsWith(delimiter) || line2.startsWith('> <')) {
|
||||
if (!line2 || line2.startsWith(delimiter) || line2.startsWith('> ')) {
|
||||
TokenBuilder.add(data, start, end);
|
||||
added = true;
|
||||
break;
|
||||
|
||||
@@ -3,7 +3,10 @@ import { CifWriter } from '../cif';
|
||||
import { decodeMsgPack } from '../../common/msgpack/decode';
|
||||
import { EncodedFile, EncodedCategory } from '../../common/binary-cif';
|
||||
import { Field } from '../../reader/cif/binary/field';
|
||||
import { TextEncoder } from '../cif/encoder/text';
|
||||
import * as C from '../cif/encoder';
|
||||
import { Column, Database, Table } from '../../../mol-data/db';
|
||||
import { parseCifText } from '../../reader/cif/text/parser';
|
||||
|
||||
const cartn_x = Data.CifField.ofNumbers([1.001, 1.002, 1.003, 1.004, 1.005, 1.006, 1.007, 1.008, 1.009]);
|
||||
const cartn_y = Data.CifField.ofNumbers([-3.0, -2.666, -2.3333, -2.0, -1.666, -1.333, -1.0, -0.666, -0.333]);
|
||||
@@ -38,6 +41,30 @@ const encoding_aware_encoder = CifWriter.createEncoder({
|
||||
])
|
||||
});
|
||||
|
||||
test('cif writer value escaping', async () => {
|
||||
const values = ['1', ' ', ' ', ` ' `, `a'`, `b"`, `"`, ' a ', `"'"`, `'"`, `\na`];
|
||||
const table = Table.ofArrays({ values: Column.Schema.str }, { values });
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
C.Encoder.writeDatabase(encoder, 'test', Database.ofTables('test', { test: table._schema }, { test: table }));
|
||||
encoder.encode();
|
||||
const data = encoder.getData();
|
||||
|
||||
const result = await parseCifText(data).run();
|
||||
if (result.isError) {
|
||||
expect(false).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const cat = result.result.blocks[0].categories['test'];
|
||||
const parsed = cat.getField('values')?.toStringArray();
|
||||
|
||||
expect(values.length).toBe(parsed?.length);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
expect(values[i]).toBe(parsed?.[i]);
|
||||
}
|
||||
});
|
||||
|
||||
describe('encoding-config', () => {
|
||||
const decoded = process(encoding_aware_encoder);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Adapted from CIFTools.js (https://github.com/dsehnal/CIFTools.js)
|
||||
*
|
||||
@@ -208,55 +208,61 @@ function writeChecked(builder: StringBuilder, val: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let escape = val.charCodeAt(0) === 95 /* _ */, escapeCharStart = '\'', escapeCharEnd = '\' ';
|
||||
let hasWhitespace = false;
|
||||
let hasSingle = false;
|
||||
let hasDouble = false;
|
||||
for (let i = 0, _l = val.length - 1; i < _l; i++) {
|
||||
const fst = val.charCodeAt(0);
|
||||
let escape = false;
|
||||
let escapeKind = 0; // 0 => ', 1 => "
|
||||
let hasSingleQuote = false, hasDoubleQuote = false;
|
||||
for (let i = 0, _l = val.length - 1; i <= _l; i++) {
|
||||
const c = val.charCodeAt(i);
|
||||
|
||||
switch (c) {
|
||||
case 9: hasWhitespace = true; break; // \t
|
||||
case 9: // \t
|
||||
escape = true;
|
||||
break;
|
||||
case 10: // \n
|
||||
writeMultiline(builder, val);
|
||||
return true;
|
||||
case 32: hasWhitespace = true; break; // ' '
|
||||
case 32: // ' '
|
||||
escape = true;
|
||||
break;
|
||||
case 34: // "
|
||||
if (hasSingle) {
|
||||
// no need to escape quote if it's the last char and the length is > 1
|
||||
if (i && i === _l) break;
|
||||
|
||||
if (hasSingleQuote) {
|
||||
// the string already has a " => use multiline value
|
||||
writeMultiline(builder, val);
|
||||
return true;
|
||||
}
|
||||
|
||||
hasDouble = true;
|
||||
hasDoubleQuote = true;
|
||||
escape = true;
|
||||
escapeCharStart = '\'';
|
||||
escapeCharEnd = '\' ';
|
||||
escapeKind = 0;
|
||||
break;
|
||||
case 39: // '
|
||||
if (hasDouble) {
|
||||
// no need to escape quote if it's the last char and the length is > 1
|
||||
if (i && i === _l) break;
|
||||
|
||||
if (hasDoubleQuote) {
|
||||
writeMultiline(builder, val);
|
||||
return true;
|
||||
}
|
||||
|
||||
hasSingleQuote = true;
|
||||
escape = true;
|
||||
hasSingle = true;
|
||||
escapeCharStart = '"';
|
||||
escapeCharEnd = '" ';
|
||||
escapeKind = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const fst = val.charCodeAt(0);
|
||||
if (!escape && (fst === 35 /* # */|| fst === 36 /* $ */ || fst === 59 /* ; */ || fst === 91 /* [ */ || fst === 93 /* ] */ || hasWhitespace)) {
|
||||
escapeCharStart = '\'';
|
||||
escapeCharEnd = '\' ';
|
||||
if (!escape && (fst === 35 /* # */ || fst === 36 /* $ */ || fst === 59 /* ; */ || fst === 91 /* [ */ || fst === 93 /* ] */ || fst === 95 /* _ */)) {
|
||||
escape = true;
|
||||
}
|
||||
|
||||
if (escape) {
|
||||
StringBuilder.writeSafe(builder, escapeCharStart);
|
||||
StringBuilder.writeSafe(builder, escapeKind ? '"' : '\'');
|
||||
StringBuilder.writeSafe(builder, val);
|
||||
StringBuilder.writeSafe(builder, escapeCharEnd);
|
||||
StringBuilder.writeSafe(builder, escapeKind ? '" ' : '\' ');
|
||||
} else {
|
||||
StringBuilder.writeSafe(builder, val);
|
||||
StringBuilder.writeSafe(builder, ' ');
|
||||
|
||||
@@ -26,6 +26,7 @@ export type DensityData = {
|
||||
transform: Mat4,
|
||||
field: Tensor,
|
||||
idField: Tensor,
|
||||
resolution: number
|
||||
}
|
||||
|
||||
export type DensityTextureData = {
|
||||
|
||||
@@ -21,10 +21,12 @@ export type GaussianDensityProps = typeof DefaultGaussianDensityProps
|
||||
|
||||
export type GaussianDensityData = {
|
||||
radiusFactor: number
|
||||
resolution: number
|
||||
} & DensityData
|
||||
|
||||
export type GaussianDensityTextureData = {
|
||||
radiusFactor: number
|
||||
resolution: number
|
||||
} & DensityTextureData
|
||||
|
||||
export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps) {
|
||||
|
||||
@@ -129,5 +129,5 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position
|
||||
Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
|
||||
Mat4.setTranslation(transform, expandedBox.min);
|
||||
|
||||
return { field, idField, transform, radiusFactor: 1 };
|
||||
return { field, idField, transform, radiusFactor: 1, resolution };
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import { gaussianDensity_vert } from '../../../mol-gl/shader/gaussian-density.ve
|
||||
import { gaussianDensity_frag } from '../../../mol-gl/shader/gaussian-density.frag';
|
||||
import { Framebuffer } from '../../../mol-gl/webgl/framebuffer';
|
||||
|
||||
export const GaussianDensitySchema = {
|
||||
const GaussianDensitySchema = {
|
||||
drawCount: ValueSpec('number'),
|
||||
instanceCount: ValueSpec('number'),
|
||||
|
||||
@@ -49,9 +49,6 @@ export const GaussianDensitySchema = {
|
||||
type GaussianDensityValues = Values<typeof GaussianDensitySchema>
|
||||
type GaussianDensityRenderable = ComputeRenderable<GaussianDensityValues>
|
||||
const GaussianDensityName = 'gaussian-density';
|
||||
const GaussianDensityShaderCode = ShaderCode(
|
||||
GaussianDensityName, gaussianDensity_vert, gaussianDensity_frag
|
||||
);
|
||||
|
||||
function getFramebuffer(webgl: WebGLContext): Framebuffer {
|
||||
if (!webgl.namedFramebuffers[GaussianDensityName]) {
|
||||
@@ -73,12 +70,12 @@ export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (
|
||||
// it's faster than texture3d
|
||||
// console.time('GaussianDensityTexture2d')
|
||||
const tmpTexture = getTexture('tmp', webgl, 'image-uint8', 'rgba', 'ubyte', 'linear');
|
||||
const { scale, bbox, texture, gridDim, gridTexDim, radiusFactor } = calcGaussianDensityTexture2d(webgl, position, box, radius, false, props, tmpTexture);
|
||||
const { scale, bbox, texture, gridDim, gridTexDim, radiusFactor, resolution } = calcGaussianDensityTexture2d(webgl, position, box, radius, false, props, tmpTexture);
|
||||
// webgl.waitForGpuCommandsCompleteSync()
|
||||
// console.timeEnd('GaussianDensityTexture2d')
|
||||
const { field, idField } = fieldFromTexture2d(webgl, texture, gridDim, gridTexDim);
|
||||
|
||||
return { field, idField, transform: getTransform(scale, bbox), radiusFactor };
|
||||
return { field, idField, transform: getTransform(scale, bbox), radiusFactor, resolution };
|
||||
}
|
||||
|
||||
export function GaussianDensityTexture(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, oldTexture?: Texture): GaussianDensityTextureData {
|
||||
@@ -95,8 +92,8 @@ export function GaussianDensityTexture3d(webgl: WebGLContext, position: Position
|
||||
return finalizeGaussianDensityTexture(calcGaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture));
|
||||
}
|
||||
|
||||
function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor }: _GaussianDensityTextureData): GaussianDensityTextureData {
|
||||
return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor };
|
||||
function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor, resolution }: _GaussianDensityTextureData): GaussianDensityTextureData {
|
||||
return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor, resolution };
|
||||
}
|
||||
|
||||
function getTransform(scale: Vec3, bbox: Box3D) {
|
||||
@@ -116,6 +113,7 @@ type _GaussianDensityTextureData = {
|
||||
gridTexDim: Vec3
|
||||
gridTexScale: Vec2
|
||||
radiusFactor: number
|
||||
resolution: number
|
||||
}
|
||||
|
||||
function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
|
||||
@@ -200,7 +198,7 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
|
||||
|
||||
// printTexture(webgl, minDistTex, 0.75);
|
||||
|
||||
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor };
|
||||
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor, resolution };
|
||||
}
|
||||
|
||||
function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
|
||||
@@ -256,7 +254,7 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
|
||||
setupGroupIdRendering(webgl, renderable);
|
||||
render(texture, false);
|
||||
|
||||
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridTexScale, radiusFactor };
|
||||
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridTexScale, radiusFactor, resolution };
|
||||
}
|
||||
|
||||
//
|
||||
@@ -362,7 +360,8 @@ function createGaussianDensityRenderable(webgl: WebGLContext, drawCount: number,
|
||||
};
|
||||
|
||||
const schema = { ...GaussianDensitySchema };
|
||||
const renderItem = createComputeRenderItem(webgl, 'points', GaussianDensityShaderCode, schema, values);
|
||||
const shaderCode = ShaderCode(GaussianDensityName, gaussianDensity_vert, gaussianDensity_frag);
|
||||
const renderItem = createComputeRenderItem(webgl, 'points', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -430,7 +429,7 @@ function getTexture2dSize(gridDim: Vec3) {
|
||||
return { texDimX, texDimY, texRows, texCols, powerOfTwoSize: texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
|
||||
}
|
||||
|
||||
export function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3, texDim: Vec3) {
|
||||
function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3, texDim: Vec3) {
|
||||
// console.time('fieldFromTexture2d')
|
||||
const [ dx, dy, dz ] = dim;
|
||||
const [ width, height ] = texDim;
|
||||
|
||||
@@ -40,7 +40,7 @@ export namespace Result {
|
||||
|
||||
export interface Lookup3D<T = number> {
|
||||
// The result is mutated with each call to find.
|
||||
find(x: number, y: number, z: number, radius: number): Result<T>,
|
||||
find(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T>,
|
||||
check(x: number, y: number, z: number, radius: number): boolean,
|
||||
readonly boundary: { readonly box: Box3D, readonly sphere: Sphere3D }
|
||||
/** transient result */
|
||||
|
||||
@@ -24,19 +24,20 @@ function GridLookup3D<T extends number = number>(data: PositionData, boundary: B
|
||||
export { GridLookup3D };
|
||||
|
||||
class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
|
||||
private ctx: QueryContext<T>;
|
||||
private ctx: QueryContext;
|
||||
boundary: Lookup3D['boundary'];
|
||||
buckets: GridLookup3D['buckets'];
|
||||
result: Result<T>
|
||||
|
||||
find(x: number, y: number, z: number, radius: number): Result<T> {
|
||||
find(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T> {
|
||||
this.ctx.x = x;
|
||||
this.ctx.y = y;
|
||||
this.ctx.z = z;
|
||||
this.ctx.radius = radius;
|
||||
this.ctx.isCheck = false;
|
||||
query(this.ctx);
|
||||
return this.ctx.result;
|
||||
const ret = result ?? this.result;
|
||||
query(this.ctx, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
check(x: number, y: number, z: number, radius: number): boolean {
|
||||
@@ -45,15 +46,15 @@ class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
|
||||
this.ctx.z = z;
|
||||
this.ctx.radius = radius;
|
||||
this.ctx.isCheck = true;
|
||||
return query(this.ctx);
|
||||
return query(this.ctx, this.result);
|
||||
}
|
||||
|
||||
constructor(data: PositionData, boundary: Boundary, cellSizeOrCount?: Vec3 | number) {
|
||||
const structure = build(data, boundary, cellSizeOrCount);
|
||||
this.ctx = createContext<T>(structure);
|
||||
this.ctx = createContext(structure);
|
||||
this.boundary = { box: structure.boundingBox, sphere: structure.boundingSphere };
|
||||
this.buckets = { offset: structure.bucketOffset, count: structure.bucketCounts, array: structure.bucketArray };
|
||||
this.result = this.ctx.result;
|
||||
this.result = Result.create();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,23 +216,22 @@ function build(data: PositionData, boundary: Boundary, cellSizeOrCount?: Vec3 |
|
||||
return _build(state);
|
||||
}
|
||||
|
||||
interface QueryContext<T extends number = number> {
|
||||
interface QueryContext {
|
||||
grid: Grid3D,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
radius: number,
|
||||
result: Result<T>,
|
||||
isCheck: boolean
|
||||
}
|
||||
|
||||
function createContext<T extends number = number>(grid: Grid3D): QueryContext<T> {
|
||||
return { grid, x: 0.1, y: 0.1, z: 0.1, radius: 0.1, result: Result.create(), isCheck: false };
|
||||
function createContext(grid: Grid3D): QueryContext {
|
||||
return { grid, x: 0.1, y: 0.1, z: 0.1, radius: 0.1, isCheck: false };
|
||||
}
|
||||
|
||||
function query<T extends number = number>(ctx: QueryContext<T>): boolean {
|
||||
function query<T extends number = number>(ctx: QueryContext, result: Result<T>): boolean {
|
||||
const { min, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices, radius }, delta, maxRadius } = ctx.grid;
|
||||
const { radius: inputRadius, isCheck, x, y, z, result } = ctx;
|
||||
const { radius: inputRadius, isCheck, x, y, z } = ctx;
|
||||
|
||||
const r = inputRadius + maxRadius;
|
||||
const rSq = r * r;
|
||||
|
||||
@@ -49,7 +49,7 @@ function getAngleTables (probePositions: number): AnglesTables {
|
||||
export const MolecularSurfaceCalculationParams = {
|
||||
probeRadius: PD.Numeric(1.4, { min: 0, max: 10, step: 0.1 }, { description: 'Radius of the probe tracing the molecular surface.' }),
|
||||
resolution: PD.Numeric(0.5, { min: 0.01, max: 20, step: 0.01 }, { description: 'Grid resolution/cell spacing.', ...BaseGeometry.CustomQualityParamInfo }),
|
||||
probePositions: PD.Numeric(30, { min: 12, max: 90, step: 1 }, { description: 'Number of positions tested for probe target intersection.', ...BaseGeometry.CustomQualityParamInfo }),
|
||||
probePositions: PD.Numeric(36, { min: 12, max: 90, step: 1 }, { description: 'Number of positions tested for probe target intersection.', ...BaseGeometry.CustomQualityParamInfo }),
|
||||
};
|
||||
export const DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(MolecularSurfaceCalculationParams);
|
||||
export type MolecularSurfaceCalculationProps = typeof DefaultMolecularSurfaceCalculationProps
|
||||
@@ -370,5 +370,5 @@ export async function calcMolecularSurface(ctx: RuntimeContext, position: Requir
|
||||
Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
|
||||
Mat4.setTranslation(transform, expandedBox.min);
|
||||
// console.log({ field, idField, transform, updateChunk })
|
||||
return { field, idField, transform };
|
||||
return { field, idField, transform, resolution };
|
||||
}
|
||||
@@ -333,7 +333,7 @@ namespace Mat4 {
|
||||
return out;
|
||||
}
|
||||
|
||||
export function invert(out: Mat4, a: Mat4) {
|
||||
export function tryInvert(out: Mat4, a: Mat4) {
|
||||
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
|
||||
a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
|
||||
a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
|
||||
@@ -356,8 +356,7 @@ namespace Mat4 {
|
||||
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
|
||||
|
||||
if (!det) {
|
||||
console.warn('non-invertible matrix.', a);
|
||||
return out;
|
||||
return false;
|
||||
}
|
||||
det = 1.0 / det;
|
||||
|
||||
@@ -378,6 +377,13 @@ namespace Mat4 {
|
||||
out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
|
||||
out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function invert(out: Mat4, a: Mat4) {
|
||||
if (!tryInvert(out, a)) {
|
||||
console.warn('non-invertible matrix.', a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -474,7 +474,9 @@ namespace Vec3 {
|
||||
|
||||
/** Computes the angle between 2 vectors, reports in radians. */
|
||||
export function angle(a: Vec3, b: Vec3) {
|
||||
const theta = dot(a, b) / Math.sqrt(squaredMagnitude(a) * squaredMagnitude(b));
|
||||
const denominator = Math.sqrt(squaredMagnitude(a) * squaredMagnitude(b));
|
||||
if (denominator === 0) return Math.PI / 2;
|
||||
const theta = dot(a, b) / denominator;
|
||||
return Math.acos(clamp(theta, -1, 1)); // clamp to avoid numerical problems
|
||||
}
|
||||
|
||||
|
||||
@@ -13,21 +13,26 @@ import { mmCIF_chemComp_schema } from '../../../mol-io/reader/cif/schema/mmcif-e
|
||||
type Component = Table.Row<Pick<mmCIF_chemComp_schema, 'id' | 'name' | 'type'>>
|
||||
|
||||
const ProteinAtomIdsList = [
|
||||
new Set([ 'CA' ]),
|
||||
new Set([ 'C' ]),
|
||||
new Set([ 'N' ])
|
||||
new Set(['CA']),
|
||||
new Set(['C']),
|
||||
new Set(['N'])
|
||||
];
|
||||
const RnaAtomIdsList = [
|
||||
new Set([ 'P', 'O3\'', 'O3*' ]),
|
||||
new Set([ 'C4\'', 'C4*' ]),
|
||||
new Set([ 'O2\'', 'O2*', 'F2\'', 'F2*' ])
|
||||
new Set(['P', 'O3\'', 'O3*']),
|
||||
new Set(['C4\'', 'C4*']),
|
||||
new Set(['O2\'', 'O2*', 'F2\'', 'F2*'])
|
||||
];
|
||||
const DnaAtomIdsList = [
|
||||
new Set([ 'P', 'O3\'', 'O3*' ]),
|
||||
new Set([ 'C3\'', 'C3*' ]),
|
||||
new Set([ 'O2\'', 'O2*', 'F2\'', 'F2*' ])
|
||||
new Set(['P', 'O3\'', 'O3*']),
|
||||
new Set(['C3\'', 'C3*']),
|
||||
new Set(['O2\'', 'O2*', 'F2\'', 'F2*'])
|
||||
];
|
||||
|
||||
/** Used to reduce false positives for atom name-based type guessing */
|
||||
const NonPolymerNames = new Set([
|
||||
'FMN', 'NCN', 'FNS', 'FMA' // Mononucleotides
|
||||
]);
|
||||
|
||||
const StandardComponents = (function() {
|
||||
const map = new Map<string, Component>();
|
||||
const components: Component[] = [
|
||||
@@ -151,9 +156,11 @@ export class ComponentBuilder {
|
||||
this.set(StandardComponents.get(compId)!);
|
||||
} else if (WaterNames.has(compId)) {
|
||||
this.set({ id: compId, name: 'WATER', type: 'non-polymer' });
|
||||
} else if (NonPolymerNames.has(compId)) {
|
||||
this.set({ id: compId, name: this.namesMap.get(compId) || compId, type: 'non-polymer' });
|
||||
} else {
|
||||
const atomIds = this.getAtomIds(index);
|
||||
if (CharmmIonComponents.has(compId) && atomIds.size === 1) {
|
||||
if (atomIds.size === 1 && CharmmIonComponents.has(compId)) {
|
||||
this.set(CharmmIonComponents.get(compId)!);
|
||||
} else {
|
||||
const type = this.getType(atomIds);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { degToRad, halfPI } from '../../mol-math/misc';
|
||||
import { Cell } from '../../mol-math/geometry/spacegroup/cell';
|
||||
import { Mutable } from '../../mol-util/type-helpers';
|
||||
import { EPSILON, equalEps } from '../../mol-math/linear-algebra/3d/common';
|
||||
|
||||
const charmmTimeUnitFactor = 20.45482949774598;
|
||||
|
||||
@@ -38,6 +39,8 @@ export function coordinatesFromDcd(dcdFile: DcdFile): Task<Coordinates> {
|
||||
x: dcdFrame.x,
|
||||
y: dcdFrame.y,
|
||||
z: dcdFrame.z,
|
||||
|
||||
xyzOrdering: { isIdentity: true }
|
||||
};
|
||||
|
||||
if (dcdFrame.cell) {
|
||||
@@ -64,7 +67,12 @@ export function coordinatesFromDcd(dcdFile: DcdFile): Task<Coordinates> {
|
||||
} else {
|
||||
frame.cell = Cell.create(
|
||||
Vec3.create(c[0], c[2], c[5]),
|
||||
Vec3.create(degToRad(c[1]), degToRad(c[3]), degToRad(c[4]))
|
||||
// interpret angles very close to 0 as 90 deg
|
||||
Vec3.create(
|
||||
degToRad(equalEps(c[1], 0, EPSILON) ? 90 : c[1]),
|
||||
degToRad(equalEps(c[3], 0, EPSILON) ? 90 : c[3]),
|
||||
degToRad(equalEps(c[4], 0, EPSILON) ? 90 : c[4])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ModelFormat } from '../format';
|
||||
import { IndexPairBonds } from './property/bonds/index-pair';
|
||||
import { Trajectory } from '../../mol-model/structure';
|
||||
|
||||
async function getModels(mol: MolFile, ctx: RuntimeContext) {
|
||||
export async function getMolModels(mol: MolFile, format: ModelFormat<any> | undefined, ctx: RuntimeContext) {
|
||||
const { atoms, bonds } = mol;
|
||||
|
||||
const MOL = Column.ofConst('MOL', mol.atoms.count, Column.Schema.str);
|
||||
@@ -61,7 +61,7 @@ async function getModels(mol: MolFile, ctx: RuntimeContext) {
|
||||
atom_site
|
||||
});
|
||||
|
||||
const models = await createModels(basics, MolFormat.create(mol), ctx);
|
||||
const models = await createModels(basics, format ?? MolFormat.create(mol), ctx);
|
||||
|
||||
if (models.frameCount > 0) {
|
||||
const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdxA, x => x - 1, Int32Array));
|
||||
@@ -91,5 +91,5 @@ namespace MolFormat {
|
||||
}
|
||||
|
||||
export function trajectoryFromMol(mol: MolFile): Task<Trajectory> {
|
||||
return Task.create('Parse MOL', ctx => getModels(mol, ctx));
|
||||
return Task.create('Parse MOL', ctx => getMolModels(mol, void 0, ctx));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Column, Table } from '../../mol-data/db';
|
||||
import { Model } from '../../mol-model/structure/model';
|
||||
import { MoleculeType } from '../../mol-model/structure/model/types';
|
||||
import { BondType, MoleculeType } from '../../mol-model/structure/model/types';
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { createModels } from './basic/parser';
|
||||
import { BasicSchema, createBasic } from './basic/schema';
|
||||
@@ -74,8 +74,33 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) {
|
||||
if (_models.frameCount > 0) {
|
||||
const indexA = Column.ofIntArray(Column.mapToArray(bonds.origin_atom_id, x => x - 1, Int32Array));
|
||||
const indexB = Column.ofIntArray(Column.mapToArray(bonds.target_atom_id, x => x - 1, Int32Array));
|
||||
const order = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => x === 'ar' ? 1 : parseInt(x), Int8Array));
|
||||
const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: atoms.count });
|
||||
const order = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => {
|
||||
switch (x) {
|
||||
case 'ar': // aromatic
|
||||
case 'am': // amide
|
||||
case 'un': // unknown
|
||||
return 1;
|
||||
case 'du': // dummy
|
||||
case 'nc': // not connected
|
||||
return 0;
|
||||
default:
|
||||
return parseInt(x);
|
||||
}
|
||||
}, Int8Array));
|
||||
const flag = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => {
|
||||
switch (x) {
|
||||
case 'ar': // aromatic
|
||||
return BondType.Flag.Aromatic | BondType.Flag.Covalent;
|
||||
case 'du': // dummy
|
||||
case 'nc': // not connected
|
||||
return BondType.Flag.None;
|
||||
case 'am': // amide
|
||||
case 'un': // unknown
|
||||
default:
|
||||
return BondType.Flag.Covalent;
|
||||
}
|
||||
}, Int8Array));
|
||||
const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order, flag }, count: atoms.count });
|
||||
|
||||
const first = _models.representative;
|
||||
IndexPairBonds.Provider.set(first, pairBonds);
|
||||
|
||||
@@ -19,6 +19,7 @@ export { ModelSecondaryStructure };
|
||||
|
||||
type StructConf = Table<mmCIF_Schema['struct_conf']>
|
||||
type StructSheetRange = Table<mmCIF_Schema['struct_sheet_range']>
|
||||
type CoordinateType = 'label' | 'auth';
|
||||
|
||||
namespace ModelSecondaryStructure {
|
||||
export const Descriptor: CustomPropertyDescriptor = {
|
||||
@@ -30,9 +31,12 @@ namespace ModelSecondaryStructure {
|
||||
export function fromStruct(conf: StructConf, sheetRange: StructSheetRange, hierarchy: AtomicHierarchy): SecondaryStructure {
|
||||
const map: SecondaryStructureMap = new Map();
|
||||
const elements: SecondaryStructure.Element[] = [{ kind: 'none' }];
|
||||
addHelices(conf, map, elements);
|
||||
|
||||
const coordinates = getCoordinateType(conf, sheetRange);
|
||||
|
||||
addHelices(conf, coordinates, map, elements);
|
||||
// must add Helices 1st because of 'key' value assignment.
|
||||
addSheets(sheetRange, map, conf._rowCount, elements);
|
||||
addSheets(sheetRange, coordinates, map, conf._rowCount, elements);
|
||||
|
||||
const n = hierarchy.residues._rowCount;
|
||||
const getIndex = (rI: ResidueIndex) => rI;
|
||||
@@ -43,15 +47,24 @@ namespace ModelSecondaryStructure {
|
||||
elements
|
||||
};
|
||||
|
||||
if (map.size > 0) assignSecondaryStructureRanges(hierarchy, map, secStruct);
|
||||
if (map.size > 0) assignSecondaryStructureRanges(hierarchy, coordinates, map, secStruct);
|
||||
return SecondaryStructure(secStruct.type, secStruct.key, secStruct.elements, getIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function getCoordinateType(conf: StructConf, sheetRange: StructSheetRange): CoordinateType {
|
||||
if (conf._rowCount > 0) {
|
||||
if (conf.beg_label_seq_id.valueKind(0) !== Column.ValueKind.Present || conf.end_label_seq_id.valueKind(0) !== Column.ValueKind.Present) return 'auth';
|
||||
} else if (sheetRange) {
|
||||
if (sheetRange.beg_label_seq_id.valueKind(0) !== Column.ValueKind.Present || sheetRange.end_label_seq_id.valueKind(0) !== Column.ValueKind.Present) return 'auth';
|
||||
}
|
||||
return 'label';
|
||||
}
|
||||
|
||||
type SecondaryStructureEntry = {
|
||||
startSeqNumber: number,
|
||||
startSeqId: number,
|
||||
startInsCode: string | null,
|
||||
endSeqNumber: number,
|
||||
endSeqId: number,
|
||||
endInsCode: string | null,
|
||||
type: SecondaryStructureType,
|
||||
key: number
|
||||
@@ -59,13 +72,16 @@ type SecondaryStructureEntry = {
|
||||
type SecondaryStructureMap = Map<string, Map<number, SecondaryStructureEntry[]>>
|
||||
type SecondaryStructureData = { type: SecondaryStructureType[], key: number[], elements: SecondaryStructure.Element[] }
|
||||
|
||||
function addHelices(cat: StructConf, map: SecondaryStructureMap, elements: SecondaryStructure.Element[]) {
|
||||
function addHelices(cat: StructConf, coordinates: CoordinateType, map: SecondaryStructureMap, elements: SecondaryStructure.Element[]) {
|
||||
if (!cat._rowCount) return;
|
||||
|
||||
const { beg_label_asym_id, beg_auth_seq_id, pdbx_beg_PDB_ins_code } = cat;
|
||||
const { end_auth_seq_id, pdbx_end_PDB_ins_code } = cat;
|
||||
const { beg_label_asym_id, beg_label_seq_id, beg_auth_seq_id, pdbx_beg_PDB_ins_code } = cat;
|
||||
const { end_label_seq_id, end_auth_seq_id, pdbx_end_PDB_ins_code } = cat;
|
||||
const { pdbx_PDB_helix_class, conf_type_id, details } = cat;
|
||||
|
||||
const beg_seq_id = coordinates === 'label' ? beg_label_seq_id : beg_auth_seq_id;
|
||||
const end_seq_id = coordinates === 'label' ? end_label_seq_id : end_auth_seq_id;
|
||||
|
||||
for (let i = 0, _i = cat._rowCount; i < _i; i++) {
|
||||
const type = SecondaryStructureType.create(pdbx_PDB_helix_class.valueKind(i) === Column.ValueKind.Present
|
||||
? SecondaryStructureType.SecondaryStructurePdb[pdbx_PDB_helix_class.value(i)]
|
||||
@@ -81,9 +97,9 @@ function addHelices(cat: StructConf, map: SecondaryStructureMap, elements: Secon
|
||||
details: details.valueKind(i) === Column.ValueKind.Present ? details.value(i) : void 0
|
||||
};
|
||||
const entry: SecondaryStructureEntry = {
|
||||
startSeqNumber: beg_auth_seq_id.value(i),
|
||||
startSeqId: beg_seq_id.value(i),
|
||||
startInsCode: pdbx_beg_PDB_ins_code.value(i),
|
||||
endSeqNumber: end_auth_seq_id.value(i),
|
||||
endSeqId: end_seq_id.value(i),
|
||||
endInsCode: pdbx_end_PDB_ins_code.value(i),
|
||||
type,
|
||||
key: elements.length
|
||||
@@ -94,24 +110,27 @@ function addHelices(cat: StructConf, map: SecondaryStructureMap, elements: Secon
|
||||
const asymId = beg_label_asym_id.value(i)!;
|
||||
if (map.has(asymId)) {
|
||||
const entries = map.get(asymId)!;
|
||||
if (entries.has(entry.startSeqNumber)) {
|
||||
entries.get(entry.startSeqNumber)!.push(entry);
|
||||
if (entries.has(entry.startSeqId)) {
|
||||
entries.get(entry.startSeqId)!.push(entry);
|
||||
} else {
|
||||
entries.set(entry.startSeqNumber, [entry]);
|
||||
entries.set(entry.startSeqId, [entry]);
|
||||
}
|
||||
} else {
|
||||
map.set(asymId, new Map([[entry.startSeqNumber, [entry]]]));
|
||||
map.set(asymId, new Map([[entry.startSeqId, [entry]]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSheets(cat: StructSheetRange, map: SecondaryStructureMap, sheetCount: number, elements: SecondaryStructure.Element[]) {
|
||||
function addSheets(cat: StructSheetRange, coordinates: CoordinateType, map: SecondaryStructureMap, sheetCount: number, elements: SecondaryStructure.Element[]) {
|
||||
if (!cat._rowCount) return;
|
||||
|
||||
const { beg_label_asym_id, beg_auth_seq_id, pdbx_beg_PDB_ins_code } = cat;
|
||||
const { end_auth_seq_id, pdbx_end_PDB_ins_code } = cat;
|
||||
const { beg_label_asym_id, beg_label_seq_id, beg_auth_seq_id, pdbx_beg_PDB_ins_code } = cat;
|
||||
const { end_label_seq_id, end_auth_seq_id, pdbx_end_PDB_ins_code } = cat;
|
||||
const { sheet_id } = cat;
|
||||
|
||||
const beg_seq_id = coordinates === 'label' ? beg_label_seq_id : beg_auth_seq_id;
|
||||
const end_seq_id = coordinates === 'label' ? end_label_seq_id : end_auth_seq_id;
|
||||
|
||||
const sheet_id_key = new Map<string, number>();
|
||||
let currentKey = sheetCount + 1;
|
||||
|
||||
@@ -132,9 +151,9 @@ function addSheets(cat: StructSheetRange, map: SecondaryStructureMap, sheetCount
|
||||
symmetry: void 0
|
||||
};
|
||||
const entry: SecondaryStructureEntry = {
|
||||
startSeqNumber: beg_auth_seq_id.value(i),
|
||||
startSeqId: beg_seq_id.value(i),
|
||||
startInsCode: pdbx_beg_PDB_ins_code.value(i),
|
||||
endSeqNumber: end_auth_seq_id.value(i),
|
||||
endSeqId: end_seq_id.value(i),
|
||||
endInsCode: pdbx_end_PDB_ins_code.value(i),
|
||||
type,
|
||||
key: elements.length
|
||||
@@ -145,31 +164,33 @@ function addSheets(cat: StructSheetRange, map: SecondaryStructureMap, sheetCount
|
||||
const asymId = beg_label_asym_id.value(i)!;
|
||||
if (map.has(asymId)) {
|
||||
const entries = map.get(asymId)!;
|
||||
if (entries.has(entry.startSeqNumber)) {
|
||||
entries.get(entry.startSeqNumber)!.push(entry);
|
||||
if (entries.has(entry.startSeqId)) {
|
||||
entries.get(entry.startSeqId)!.push(entry);
|
||||
} else {
|
||||
entries.set(entry.startSeqNumber, [entry]);
|
||||
entries.set(entry.startSeqId, [entry]);
|
||||
}
|
||||
} else {
|
||||
map.set(asymId, new Map([[entry.startSeqNumber, [entry]]]));
|
||||
map.set(asymId, new Map([[entry.startSeqId, [entry]]]));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function assignSecondaryStructureEntry(hierarchy: AtomicHierarchy, entry: SecondaryStructureEntry, resStart: ResidueIndex, resEnd: ResidueIndex, data: SecondaryStructureData) {
|
||||
const { auth_seq_id, pdbx_PDB_ins_code } = hierarchy.residues;
|
||||
const { endSeqNumber, endInsCode, key, type } = entry;
|
||||
function assignSecondaryStructureEntry(hierarchy: AtomicHierarchy, coordinates: CoordinateType, entry: SecondaryStructureEntry, resStart: ResidueIndex, resEnd: ResidueIndex, data: SecondaryStructureData) {
|
||||
const { auth_seq_id, label_seq_id, pdbx_PDB_ins_code } = hierarchy.residues;
|
||||
const { endSeqId, endInsCode, key, type } = entry;
|
||||
|
||||
const seq_id = coordinates === 'label' ? label_seq_id : auth_seq_id;
|
||||
|
||||
let rI = resStart;
|
||||
while (rI < resEnd) {
|
||||
const seqNumber = auth_seq_id.value(rI);
|
||||
const seqNumber = seq_id.value(rI);
|
||||
data.type[rI] = type;
|
||||
data.key[rI] = key;
|
||||
|
||||
if ((seqNumber > endSeqNumber) ||
|
||||
(seqNumber === endSeqNumber && pdbx_PDB_ins_code.value(rI) === endInsCode)) {
|
||||
if ((seqNumber > endSeqId) ||
|
||||
(seqNumber === endSeqId && pdbx_PDB_ins_code.value(rI) === endInsCode)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -177,10 +198,11 @@ function assignSecondaryStructureEntry(hierarchy: AtomicHierarchy, entry: Second
|
||||
}
|
||||
}
|
||||
|
||||
function assignSecondaryStructureRanges(hierarchy: AtomicHierarchy, map: SecondaryStructureMap, data: SecondaryStructureData) {
|
||||
function assignSecondaryStructureRanges(hierarchy: AtomicHierarchy, coordinates: CoordinateType, map: SecondaryStructureMap, data: SecondaryStructureData) {
|
||||
const { count: chainCount } = hierarchy.chainAtomSegments;
|
||||
const { label_asym_id } = hierarchy.chains;
|
||||
const { auth_seq_id, pdbx_PDB_ins_code } = hierarchy.residues;
|
||||
const { auth_seq_id, label_seq_id, pdbx_PDB_ins_code } = hierarchy.residues;
|
||||
const seq_id = coordinates === 'label' ? label_seq_id : auth_seq_id;
|
||||
|
||||
for (let cI = 0 as ChainIndex; cI < chainCount; cI++) {
|
||||
const resStart = AtomicHierarchy.chainStartResidueIndex(hierarchy, cI), resEnd = AtomicHierarchy.chainEndResidueIndexExcl(hierarchy, cI);
|
||||
@@ -189,13 +211,13 @@ function assignSecondaryStructureRanges(hierarchy: AtomicHierarchy, map: Seconda
|
||||
const entries = map.get(asymId)!;
|
||||
|
||||
for (let rI = resStart; rI < resEnd; rI++) {
|
||||
const seqNumber = auth_seq_id.value(rI);
|
||||
if (entries.has(seqNumber)) {
|
||||
const entryList = entries.get(seqNumber)!;
|
||||
const seqId = seq_id.value(rI);
|
||||
if (entries.has(seqId)) {
|
||||
const entryList = entries.get(seqId)!;
|
||||
for (const entry of entryList) {
|
||||
const insCode = pdbx_PDB_ins_code.value(rI);
|
||||
if (entry.startInsCode !== insCode) continue;
|
||||
assignSecondaryStructureEntry(hierarchy, entry, rI, resEnd, data);
|
||||
assignSecondaryStructureEntry(hierarchy, coordinates, entry, rI, resEnd, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
src/mol-model-formats/structure/sdf.ts
Normal file
29
src/mol-model-formats/structure/sdf.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { SdfFileCompound } from '../../mol-io/reader/sdf/parser';
|
||||
import { Trajectory } from '../../mol-model/structure';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ModelFormat } from '../format';
|
||||
import { getMolModels } from './mol';
|
||||
|
||||
export { SdfFormat };
|
||||
|
||||
type SdfFormat = ModelFormat<SdfFileCompound>
|
||||
|
||||
namespace SdfFormat {
|
||||
export function is(x?: ModelFormat): x is SdfFormat {
|
||||
return x?.kind === 'sdf';
|
||||
}
|
||||
|
||||
export function create(mol: SdfFileCompound): SdfFormat {
|
||||
return { kind: 'sdf', name: mol.molFile.title, data: mol };
|
||||
}
|
||||
}
|
||||
|
||||
export function trajectoryFromSdf(mol: SdfFileCompound): Task<Trajectory> {
|
||||
return Task.create('Parse SDF', ctx => getMolModels(mol.molFile, SdfFormat.create(mol), ctx));
|
||||
}
|
||||
@@ -61,6 +61,14 @@ export function guessElementSymbolString(str: string) {
|
||||
) return str;
|
||||
}
|
||||
|
||||
if (l === 3) { // three chars
|
||||
if (str === 'SOD') return 'NA';
|
||||
if (str === 'POT') return 'K';
|
||||
if (str === 'CES') return 'CS';
|
||||
if (str === 'CAL') return 'CA';
|
||||
if (str === 'CLA') return 'CL';
|
||||
}
|
||||
|
||||
const c = str[0];
|
||||
if (c === 'C' || c === 'H' || c === 'N' || c === 'O' || c === 'P' || c === 'S') return c;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export function coordinatesFromXtc(file: XtcFile): Task<Coordinates> {
|
||||
x: file.frames[i].x,
|
||||
y: file.frames[i].y,
|
||||
z: file.frames[i].z,
|
||||
xyzOrdering: { isIdentity: true },
|
||||
time: Time(offsetTime.value + deltaTime.value * i, deltaTime.unit)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import { Structure, StructureElement, StructureProperties } from '../../../mol-m
|
||||
import { assignRadiusForHeavyAtoms } from './shrake-rupley/radii';
|
||||
import { ShrakeRupleyContext, VdWLookup, MaxAsa, DefaultMaxAsa } from './shrake-rupley/common';
|
||||
import { computeArea } from './shrake-rupley/area';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
|
||||
export const ShrakeRupleyComputationParams = {
|
||||
numberOfSpherePoints: PD.Numeric(92, { min: 12, max: 360, step: 1 }, { description: 'Number of sphere points to sample per atom: 92 (original paper), 960 (BioJava), 3000 (EPPIC) - see Shrake A, Rupley JA: Environment and exposure to solvent of protein atoms. Lysozyme and insulin. J Mol Biol 1973.' }),
|
||||
probeSize: PD.Numeric(1.4, { min: 0.1, max: 4, step: 0.01 }, { description: 'Corresponds to the size of a water molecule: 1.4 (original paper), 1.5 (occassionally used)' }),
|
||||
// buriedRasaThreshold: PD.Numeric(0.16, { min: 0.0, max: 1.0 }, { description: 'below this cutoff of relative accessible surface area a residue will be considered buried - see: Rost B, Sander C: Conservation and prediction of solvent accessibility in protein families. Proteins 1994.' }),
|
||||
nonPolymer: PD.Boolean(false, { description: 'Include non-polymer atoms as occluders.' })
|
||||
nonPolymer: PD.Boolean(false, { description: 'Include non-polymer atoms as occluders.' }),
|
||||
traceOnly: PD.Boolean(false, { description: 'Compute only using alpha-carbons, if true increase probeSize accordingly (e.g., 4 A). Considers only canonical amino acids.' })
|
||||
};
|
||||
export type ShrakeRupleyComputationParams = typeof ShrakeRupleyComputationParams
|
||||
export type ShrakeRupleyComputationProps = PD.Values<ShrakeRupleyComputationParams>
|
||||
@@ -57,12 +59,13 @@ namespace AccessibleSurfaceArea {
|
||||
|
||||
function initialize(structure: Structure, props: ShrakeRupleyComputationProps): ShrakeRupleyContext {
|
||||
const { elementCount, atomicResidueCount } = structure;
|
||||
const { probeSize, nonPolymer, numberOfSpherePoints } = props;
|
||||
const { probeSize, nonPolymer, traceOnly, numberOfSpherePoints } = props;
|
||||
|
||||
return {
|
||||
structure,
|
||||
probeSize,
|
||||
nonPolymer,
|
||||
traceOnly,
|
||||
spherePoints: generateSpherePoints(numberOfSpherePoints),
|
||||
scalingConstant: 4.0 * Math.PI / numberOfSpherePoints,
|
||||
maxLookupRadius: 2 * props.probeSize + 2 * VdWLookup[2], // 2x probe size + 2x largest VdW
|
||||
@@ -99,9 +102,8 @@ namespace AccessibleSurfaceArea {
|
||||
}
|
||||
|
||||
export function getValue(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) {
|
||||
const { getSerialIndex } = location.structure.root.serialMapping;
|
||||
const { area, serialResidueIndex } = accessibleSurfaceArea;
|
||||
const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)];
|
||||
const rSI = serialResidueIndex[SortedArray.indexOf(SortedArray.ofSortedArray(location.structure.root.serialMapping.elementIndices), location.element)];
|
||||
if (rSI === -1) return -1;
|
||||
return area[rSI];
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
*/
|
||||
|
||||
import { ShrakeRupleyContext, VdWLookup } from './common';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../../mol-task';
|
||||
import { StructureProperties, StructureElement, Structure } from '../../../../mol-model/structure/structure';
|
||||
|
||||
// TODO
|
||||
// - iterate over units and elements
|
||||
@@ -28,77 +26,65 @@ export async function computeArea(runtime: RuntimeContext, ctx: ShrakeRupleyCont
|
||||
}
|
||||
}
|
||||
|
||||
const aPos = Vec3();
|
||||
const bPos = Vec3();
|
||||
const testPoint = Vec3();
|
||||
|
||||
function setLocation(l: StructureElement.Location, structure: Structure, serialIndex: number) {
|
||||
l.structure = structure;
|
||||
l.unit = structure.units[structure.serialMapping.unitIndices[serialIndex]];
|
||||
l.element = structure.serialMapping.elementIndices[serialIndex];
|
||||
return l;
|
||||
}
|
||||
|
||||
function computeRange(ctx: ShrakeRupleyContext, begin: number, end: number) {
|
||||
const { structure, atomRadiusType, serialResidueIndex, area, spherePoints, scalingConstant, maxLookupRadius, probeSize } = ctx;
|
||||
const aLoc = StructureElement.Location.create(structure);
|
||||
const bLoc = StructureElement.Location.create(structure);
|
||||
const { x, y, z } = StructureProperties.atom;
|
||||
const { lookup3d, serialMapping, unitIndexMap } = structure;
|
||||
const { cumulativeUnitElementCount } = serialMapping;
|
||||
const { lookup3d, serialMapping, unitIndexMap, units } = structure;
|
||||
const { cumulativeUnitElementCount, elementIndices, unitIndices } = serialMapping;
|
||||
|
||||
for (let aI = begin; aI < end; ++aI) {
|
||||
const radius1 = VdWLookup[atomRadiusType[aI]];
|
||||
if (radius1 === VdWLookup[0]) continue;
|
||||
const vdw1 = VdWLookup[atomRadiusType[aI]];
|
||||
if (vdw1 === VdWLookup[0]) continue;
|
||||
|
||||
setLocation(aLoc, structure, aI);
|
||||
const aX = x(aLoc);
|
||||
const aY = y(aLoc);
|
||||
const aZ = z(aLoc);
|
||||
const aUnit = units[unitIndices[aI]];
|
||||
const aElementIndex = elementIndices[aI];
|
||||
const aX = aUnit.conformation.x(aElementIndex);
|
||||
const aY = aUnit.conformation.y(aElementIndex);
|
||||
const aZ = aUnit.conformation.z(aElementIndex);
|
||||
|
||||
// pre-filter by lookup3d (provides >10x speed-up compared to naive evaluation)
|
||||
const { count, units, indices, squaredDistances } = lookup3d.find(aX, aY, aZ, maxLookupRadius);
|
||||
const { count, units: lUnits, indices, squaredDistances } = lookup3d.find(aX, aY, aZ, maxLookupRadius);
|
||||
|
||||
// see optimizations proposed in Eisenhaber et al., 1995 (https://doi.org/10.1002/jcc.540160303)
|
||||
// collect neighbors for each atom
|
||||
const cutoff1 = probeSize + probeSize + radius1;
|
||||
const radius1 = probeSize + vdw1;
|
||||
const cutoff1 = probeSize + radius1;
|
||||
const neighbors = []; // TODO reuse
|
||||
for (let iI = 0; iI < count; ++iI) {
|
||||
const bUnit = units[iI];
|
||||
StructureElement.Location.set(bLoc, ctx.structure, bUnit, bUnit.elements[indices[iI]]);
|
||||
const bUnit = lUnits[iI];
|
||||
const bI = cumulativeUnitElementCount[unitIndexMap.get(bUnit.id)] + indices[iI];
|
||||
const bElementIndex = elementIndices[bI];
|
||||
|
||||
const radius2 = VdWLookup[atomRadiusType[bI]];
|
||||
if (StructureElement.Location.areEqual(aLoc, bLoc) || radius2 === VdWLookup[0]) continue;
|
||||
const vdw2 = VdWLookup[atomRadiusType[bI]];
|
||||
if ((aUnit === bUnit && aElementIndex === bElementIndex) || vdw2 === VdWLookup[0]) continue;
|
||||
|
||||
const cutoff2 = (cutoff1 + radius2) * (cutoff1 + radius2);
|
||||
if (squaredDistances[iI] < cutoff2) {
|
||||
neighbors[neighbors.length] = bI;
|
||||
const radius2 = probeSize + vdw2;
|
||||
if (squaredDistances[iI] < (cutoff1 + vdw2) * (cutoff1 + vdw2)) {
|
||||
const bElementIndex = elementIndices[bI];
|
||||
// while here: compute values for later lookup
|
||||
neighbors[neighbors.length] = [squaredDistances[iI],
|
||||
(squaredDistances[iI] + radius1 * radius1 - radius2 * radius2) / (2 * radius1),
|
||||
bUnit.conformation.x(bElementIndex) - aX,
|
||||
bUnit.conformation.y(bElementIndex) - aY,
|
||||
bUnit.conformation.z(bElementIndex) - aZ];
|
||||
}
|
||||
}
|
||||
|
||||
// for all neighbors: test all sphere points
|
||||
Vec3.set(aPos, aX, aY, aZ);
|
||||
const scale = probeSize + radius1;
|
||||
let accessiblePointCount = 0;
|
||||
for (let sI = 0; sI < spherePoints.length; ++sI) {
|
||||
Vec3.scaleAndAdd(testPoint, aPos, spherePoints[sI], scale);
|
||||
let accessible = true;
|
||||
// sort ascendingly by distance for improved downstream performance
|
||||
neighbors.sort((a, b) => a[0] - b[0]);
|
||||
|
||||
for (let _nI = 0; _nI < neighbors.length; ++_nI) {
|
||||
const nI = neighbors[_nI];
|
||||
setLocation(bLoc, structure, nI);
|
||||
Vec3.set(bPos, x(bLoc), y(bLoc), z(bLoc));
|
||||
const radius3 = VdWLookup[atomRadiusType[nI]];
|
||||
const cutoff3 = (radius3 + probeSize) * (radius3 + probeSize);
|
||||
if (Vec3.squaredDistance(testPoint, bPos) < cutoff3) {
|
||||
accessible = false;
|
||||
break;
|
||||
let accessiblePointCount = 0;
|
||||
sl: for (let sI = 0; sI < spherePoints.length; ++sI) {
|
||||
const [sX, sY, sZ] = spherePoints[sI];
|
||||
for (let nI = 0; nI < neighbors.length; ++nI) {
|
||||
const [, sqRadius, nX, nY, nZ] = neighbors[nI];
|
||||
const dot = sX * nX + sY * nY + sZ * nZ;
|
||||
if (dot > sqRadius) {
|
||||
continue sl;
|
||||
}
|
||||
}
|
||||
|
||||
if (accessible) ++accessiblePointCount;
|
||||
++accessiblePointCount;
|
||||
}
|
||||
|
||||
area[serialResidueIndex[aI]] += scalingConstant * accessiblePointCount * scale * scale;
|
||||
area[serialResidueIndex[aI]] += scalingConstant * accessiblePointCount * radius1 * radius1;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export interface ShrakeRupleyContext {
|
||||
spherePoints: Vec3[],
|
||||
probeSize: number,
|
||||
nonPolymer: boolean,
|
||||
traceOnly: boolean,
|
||||
scalingConstant: number,
|
||||
maxLookupRadius: number,
|
||||
atomRadiusType: Int8Array,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ShrakeRupleyContext, VdWLookup } from './common';
|
||||
import { MaxAsa, ShrakeRupleyContext, VdWLookup } from './common';
|
||||
import { getElementIdx, isHydrogen } from '../../../../mol-model/structure/structure/unit/bonds/common';
|
||||
import { isPolymer, isNucleic, MoleculeType, ElementSymbol } from '../../../../mol-model/structure/model/types';
|
||||
import { VdwRadius } from '../../../../mol-model/structure/model/properties/atomic';
|
||||
@@ -45,21 +45,26 @@ export function assignRadiusForHeavyAtoms(ctx: ShrakeRupleyContext) {
|
||||
|
||||
// skip hydrogen atoms
|
||||
if (isHydrogen(elementIdx)) {
|
||||
atomRadiusType[mj] = VdWLookup[0];
|
||||
serialResidueIndex[mj] = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const moleculeType = getElementMoleculeType(unit, eI);
|
||||
// skip water and optionally non-polymer groups
|
||||
if (moleculeType === MoleculeType.Water || (!ctx.nonPolymer && !isPolymer(moleculeType))) {
|
||||
atomRadiusType[mj] = VdWLookup[0];
|
||||
atomRadiusType[mj] = 0;
|
||||
serialResidueIndex[mj] = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const atomId = label_atom_id(l);
|
||||
const moleculeType = getElementMoleculeType(unit, eI);
|
||||
// skip water and optionally non-polymer groups
|
||||
if (moleculeType === MoleculeType.Water || (!ctx.nonPolymer && !isPolymer(moleculeType))) {
|
||||
atomRadiusType[mj] = 0;
|
||||
serialResidueIndex[mj] = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const compId = label_comp_id(l);
|
||||
if (ctx.traceOnly && ((atomId !== 'CA' && atomId !== 'BB') || !MaxAsa[compId])) {
|
||||
atomRadiusType[mj] = 0;
|
||||
serialResidueIndex[mj] = serialResidueIdx;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isNucleic(moleculeType)) {
|
||||
atomRadiusType[mj] = determineRadiusNucl(atomId, element, compId);
|
||||
|
||||
31
src/mol-model-props/computed/helix-orientation.ts
Normal file
31
src/mol-model-props/computed/helix-orientation.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2021 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 { Model } from '../../mol-model/structure';
|
||||
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { CustomModelProperty } from '../common/custom-model-property';
|
||||
import { calcHelixOrientation, HelixOrientation } from './helix-orientation/helix-orientation';
|
||||
|
||||
export const HelixOrientationParams = { };
|
||||
export type HelixOrientationParams = typeof HelixOrientationParams
|
||||
export type HelixOrientationProps = PD.Values<HelixOrientationParams>
|
||||
|
||||
export type HelixOrientationValue = HelixOrientation;
|
||||
|
||||
export const HelixOrientationProvider: CustomModelProperty.Provider<HelixOrientationParams, HelixOrientationValue> = CustomModelProperty.createProvider({
|
||||
label: 'Helix Orientation',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'molstar_helix_orientation'
|
||||
}),
|
||||
type: 'dynamic',
|
||||
defaultParams: {},
|
||||
getParams: () => ({}),
|
||||
isApplicable: (data: Model) => true,
|
||||
obtain: async (ctx, data) => {
|
||||
return { value: calcHelixOrientation(data) };
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ElementIndex } from '../../../mol-model/structure';
|
||||
import { Segmentation } from '../../../mol-data/int/segmentation';
|
||||
import { SortedRanges } from '../../../mol-data/int/sorted-ranges';
|
||||
import { OrderedSet } from '../../../mol-data/int';
|
||||
import { Model } from '../../../mol-model/structure/model';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
export interface HelixOrientation {
|
||||
centers: ArrayLike<number>
|
||||
}
|
||||
|
||||
/** Usees same definition as GROMACS' helixorient */
|
||||
export function calcHelixOrientation(model: Model): HelixOrientation {
|
||||
const { x, y, z } = model.atomicConformation;
|
||||
const { polymerType, traceElementIndex } = model.atomicHierarchy.derived.residue;
|
||||
const n = polymerType.length;
|
||||
|
||||
const elements = OrderedSet.ofBounds(0, model.atomicConformation.atomId.rowCount) as OrderedSet<ElementIndex>;
|
||||
const polymerIt = SortedRanges.transientSegments(model.atomicRanges.polymerRanges, elements);
|
||||
const residueIt = Segmentation.transientSegments(model.atomicHierarchy.residueAtomSegments, elements);
|
||||
|
||||
const centers = new Float32Array(n * 3);
|
||||
const axes = new Float32Array(n * 3);
|
||||
|
||||
let i = 0;
|
||||
let j = -1;
|
||||
let s = -1;
|
||||
|
||||
const a1 = Vec3();
|
||||
const a2 = Vec3();
|
||||
const a3 = Vec3();
|
||||
const a4 = Vec3();
|
||||
|
||||
const r12 = Vec3();
|
||||
const r23 = Vec3();
|
||||
const r34 = Vec3();
|
||||
|
||||
const v1 = Vec3();
|
||||
const v2 = Vec3();
|
||||
const vt = Vec3();
|
||||
|
||||
const diff13 = Vec3();
|
||||
const diff24 = Vec3();
|
||||
|
||||
const axis = Vec3();
|
||||
const prevAxis = Vec3();
|
||||
|
||||
while (polymerIt.hasNext) {
|
||||
const ps = polymerIt.move();
|
||||
residueIt.setSegment(ps);
|
||||
i = -1;
|
||||
s = -1;
|
||||
while (residueIt.hasNext) {
|
||||
i += 1;
|
||||
const { index } = residueIt.move();
|
||||
if (i === 0) s = index;
|
||||
|
||||
j = (index - 2);
|
||||
const j3 = j * 3;
|
||||
|
||||
Vec3.copy(a1, a2);
|
||||
Vec3.copy(a2, a3);
|
||||
Vec3.copy(a3, a4);
|
||||
|
||||
const eI = traceElementIndex[index];
|
||||
Vec3.set(a4, x[eI], y[eI], z[eI]);
|
||||
|
||||
if (i < 3) continue;
|
||||
|
||||
Vec3.sub(r12, a2, a1);
|
||||
Vec3.sub(r23, a3, a2);
|
||||
Vec3.sub(r34, a4, a3);
|
||||
|
||||
Vec3.sub(diff13, r12, r23);
|
||||
Vec3.sub(diff24, r23, r34);
|
||||
|
||||
Vec3.cross(axis, diff13, diff24);
|
||||
Vec3.normalize(axis, axis);
|
||||
Vec3.toArray(axis, axes, j3);
|
||||
|
||||
const tmp = Math.cos(Vec3.angle(diff13, diff24));
|
||||
|
||||
const diff13Length = Vec3.magnitude(diff13);
|
||||
const diff24Length = Vec3.magnitude(diff24);
|
||||
|
||||
const r = (
|
||||
Math.sqrt(diff24Length * diff13Length) /
|
||||
// clamp, to avoid numerical instabilities for when
|
||||
// angle between diff13 and diff24 is close to 0
|
||||
Math.max(2.0, 2.0 * (1.0 - tmp))
|
||||
);
|
||||
|
||||
Vec3.scale(v1, diff13, r / diff13Length);
|
||||
Vec3.sub(v1, a2, v1);
|
||||
Vec3.toArray(v1, centers, j3);
|
||||
|
||||
Vec3.scale(v2, diff24, r / diff24Length);
|
||||
Vec3.sub(v2, a3, v2);
|
||||
Vec3.toArray(v2, centers, j3 + 3);
|
||||
|
||||
Vec3.copy(prevAxis, axis);
|
||||
}
|
||||
|
||||
// calc axis as dir of second and third center pos
|
||||
// project first trace atom onto axis to get first center pos
|
||||
const s3 = s * 3;
|
||||
Vec3.fromArray(v1, centers, s3 + 3);
|
||||
Vec3.fromArray(v2, centers, s3 + 6);
|
||||
Vec3.normalize(axis, Vec3.sub(axis, v1, v2));
|
||||
const sI = traceElementIndex[s];
|
||||
Vec3.set(a1, x[sI], y[sI], z[sI]);
|
||||
Vec3.copy(vt, a1);
|
||||
Vec3.projectPointOnVector(vt, vt, axis, v1);
|
||||
Vec3.toArray(vt, centers, s3);
|
||||
|
||||
// calc axis as dir of n-1 and n-2 center pos
|
||||
// project last traceAtom onto axis to get last center pos
|
||||
const e = j + 2;
|
||||
const e3 = e * 3;
|
||||
Vec3.fromArray(v1, centers, e3 - 3);
|
||||
Vec3.fromArray(v2, centers, e3 - 6);
|
||||
Vec3.normalize(axis, Vec3.sub(axis, v1, v2));
|
||||
const eI = traceElementIndex[e];
|
||||
Vec3.set(a1, x[eI], y[eI], z[eI]);Vec3.copy(vt, a1);
|
||||
Vec3.projectPointOnVector(vt, vt, axis, v1);
|
||||
Vec3.toArray(vt, centers, e3);
|
||||
}
|
||||
|
||||
return {
|
||||
centers
|
||||
};
|
||||
}
|
||||
@@ -4,16 +4,17 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
|
||||
import { Features } from './features';
|
||||
import { InteractionType, FeatureType } from './common';
|
||||
import { IntraContactsBuilder, InterContactsBuilder } from './contacts-builder';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { altLoc, connectedTo, typeSymbol } from '../chemistry/util';
|
||||
import { OrderedSet } from '../../../mol-data/int';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
|
||||
import { VdwRadius } from '../../../mol-model/structure/model/properties/atomic';
|
||||
import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
|
||||
import { StructureLookup3DResultContext } from '../../../mol-model/structure/structure/util/lookup3d';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { altLoc, connectedTo, typeSymbol } from '../chemistry/util';
|
||||
import { FeatureType, InteractionType } from './common';
|
||||
import { InterContactsBuilder, IntraContactsBuilder } from './contacts-builder';
|
||||
import { Features } from './features';
|
||||
|
||||
export const ContactsParams = {
|
||||
lineOfSightDistFactor: PD.Numeric(1.0, { min: 0, max: 3, step: 0.1 }),
|
||||
@@ -53,7 +54,7 @@ function validPair(structure: Structure, infoA: Features.Info, infoB: Features.I
|
||||
|
||||
//
|
||||
|
||||
function invalidAltLoc (unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
|
||||
function invalidAltLoc(unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
|
||||
const altA = altLoc(unitA, indexA);
|
||||
const altB = altLoc(unitB, indexB);
|
||||
return altA && altB && altA !== altB;
|
||||
@@ -71,6 +72,9 @@ const tmpVec = Vec3();
|
||||
const tmpVecA = Vec3();
|
||||
const tmpVecB = Vec3();
|
||||
|
||||
// need to use a separate context for structure.lookup3d.find because of nested queries
|
||||
const lineOfSightLookupCtx = StructureLookup3DResultContext();
|
||||
|
||||
function checkLineOfSight(structure: Structure, infoA: Features.Info, infoB: Features.Info, distFactor: number) {
|
||||
const featureA = infoA.feature;
|
||||
const featureB = infoB.feature;
|
||||
@@ -83,7 +87,7 @@ function checkLineOfSight(structure: Structure, infoA: Features.Info, infoB: Fea
|
||||
|
||||
const distMax = distFactor * MAX_LINE_OF_SIGHT_DISTANCE;
|
||||
|
||||
const { count, indices, units, squaredDistances } = structure.lookup3d.find(tmpVec[0], tmpVec[1], tmpVec[2], distMax);
|
||||
const { count, indices, units, squaredDistances } = structure.lookup3d.find(tmpVec[0], tmpVec[1], tmpVec[2], distMax, lineOfSightLookupCtx);
|
||||
if (count === 0) return true;
|
||||
|
||||
for (let r = 0; r < count; ++r) {
|
||||
|
||||
@@ -15,12 +15,13 @@ import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
|
||||
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
|
||||
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr/structure/units-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { Interactions } from '../interactions/interactions';
|
||||
import { InteractionFlag } from '../interactions/common';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
|
||||
|
||||
async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
|
||||
if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
|
||||
|
||||
@@ -64,6 +64,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
|
||||
return {
|
||||
factory: AccessibleSurfaceAreaColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color,
|
||||
props,
|
||||
contextHash,
|
||||
@@ -82,6 +83,6 @@ export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<Access
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? AccessibleSurfaceAreaProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(AccessibleSurfaceAreaProvider.descriptor, false)
|
||||
detach: (data) => data.structure && AccessibleSurfaceAreaProvider.ref(data.structure, false)
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user