mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 14:04:36 +08:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
698f7e16bd | ||
|
|
d653a96b25 | ||
|
|
e0a594121b |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -3,9 +3,27 @@ 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]
|
||||
|
||||
- [empty]
|
||||
|
||||
## [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
|
||||
|
||||
@@ -19,7 +37,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- 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 })``
|
||||
- ``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
|
||||
|
||||
@@ -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.5",
|
||||
"version": "2.0.7",
|
||||
"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": {
|
||||
|
||||
@@ -38,19 +38,6 @@
|
||||
viewer.loadPdb('7bv2');
|
||||
viewer.loadEmdb('EMD-30210', { detail: 6 });
|
||||
|
||||
// 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
|
||||
// });
|
||||
|
||||
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -243,7 +243,46 @@ export class Viewer {
|
||||
}));
|
||||
}
|
||||
|
||||
async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string, isLazy?: boolean }) {
|
||||
/**
|
||||
* @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)) {
|
||||
@@ -257,26 +296,28 @@ export class Viewer {
|
||||
format,
|
||||
entryId: options?.entryId,
|
||||
isBinary,
|
||||
isovalues: isovalues.map(v => ({ alpha: 1, ...v }))
|
||||
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: options?.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: options?.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 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();
|
||||
@@ -296,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)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,17 +4,33 @@
|
||||
* @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 { ObjExporter } from './obj-exporter';
|
||||
import { GlbExporter } from './glb-exporter';
|
||||
import { StlExporter } from './stl-exporter';
|
||||
|
||||
export const GeometryParams = {
|
||||
format: PD.Select('glb', [
|
||||
['glb', 'glTF 2.0 Binary (.glb)'],
|
||||
['stl', 'Stl (.stl)'],
|
||||
['obj', 'Wavefront (.obj)']
|
||||
])
|
||||
};
|
||||
|
||||
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 +38,37 @@ 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 boundingBox = Box3D.fromSphere3D(Box3D(), this.plugin.canvas3d?.boundingSphereVisible!);
|
||||
let renderObjectExporter: GlbExporter | ObjExporter | StlExporter;
|
||||
switch (this.behaviors.params.value.format) {
|
||||
case 'glb':
|
||||
const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
|
||||
renderObjectExporter = new GlbExporter(style, boundingBox);
|
||||
break;
|
||||
case 'obj':
|
||||
renderObjectExporter = new ObjExporter(filename, boundingBox);
|
||||
break;
|
||||
case 'stl':
|
||||
renderObjectExporter = new StlExporter(boundingBox);
|
||||
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], this.plugin.canvas3d?.webgl!, 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,369 +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 { 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 { 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, webgl: WebGLContext, 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 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);
|
||||
}
|
||||
|
||||
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 | undefined, groups: Float32Array | Uint8Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, geoTexture: boolean, ctx: RuntimeContext) {
|
||||
const obj = this.obj;
|
||||
const t = Mat4();
|
||||
const n = Mat3();
|
||||
const tmpV = Vec3();
|
||||
const stride = geoTexture ? 4 : 3;
|
||||
|
||||
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 * 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) {
|
||||
if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + 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) {
|
||||
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': {
|
||||
const group = geoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
|
||||
color = Color.fromArray(tColor, group * 3);
|
||||
break;
|
||||
}
|
||||
case 'groupInstance': {
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const group = geoTexture ? ObjExporter.getGroup(groups, i) : 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 + (geoTexture ? i : indices![i]) + 1;
|
||||
const v2 = this.vertexOffset + (geoTexture ? i + 1 : indices![i + 1]) + 1;
|
||||
const v3 = this.vertexOffset + (geoTexture ? 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;
|
||||
}
|
||||
|
||||
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, false, 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, false, 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, false, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const GeoExportName = 'geo-export';
|
||||
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 instanceCount = values.instanceCount.ref.value;
|
||||
const drawCount = values.drawCount.ref.value;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
await this.addMeshWithColors(vertices, normals, undefined, groups, vertexCount, drawCount, values, instanceIndex, true, 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, 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);
|
||||
case 'texture-mesh':
|
||||
return this.addTextureMesh(renderObject.values as TextureMeshValues, webgl, 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);
|
||||
}
|
||||
}
|
||||
|
||||
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([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);
|
||||
}
|
||||
}
|
||||
284
src/extensions/geo-export/mesh-exporter.ts
Normal file
284
src/extensions/geo-export/mesh-exporter.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 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 { 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 { 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 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(): D;
|
||||
|
||||
abstract getBlob(ctx: RuntimeContext): Promise<Blob>;
|
||||
}
|
||||
287
src/extensions/geo-export/obj-exporter.ts
Normal file
287
src/extensions/geo-export/obj-exporter.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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 { 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);
|
||||
}
|
||||
}
|
||||
|
||||
private 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
getData() {
|
||||
return {
|
||||
obj: StringBuilder.getString(this.obj),
|
||||
mtl: StringBuilder.getString(this.mtl)
|
||||
};
|
||||
}
|
||||
|
||||
async getBlob(ctx: RuntimeContext) {
|
||||
const { obj, mtl } = 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 | undefined
|
||||
}
|
||||
|
||||
export interface RenderObjectExporter<D extends RenderObjectExportData> {
|
||||
readonly fileExtension: string
|
||||
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext): Promise<void> | undefined
|
||||
getData(): 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;
|
||||
}
|
||||
}
|
||||
|
||||
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([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 { 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
|
||||
@@ -23,24 +25,36 @@ 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 {
|
||||
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>
|
||||
</>;
|
||||
}
|
||||
|
||||
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 +64,13 @@ 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 });
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
@@ -242,6 +242,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[]
|
||||
@@ -717,6 +718,7 @@ namespace Canvas3D {
|
||||
},
|
||||
camera,
|
||||
boundingSphere: scene.boundingSphere,
|
||||
boundingSphereVisible: scene.boundingSphereVisible,
|
||||
get notifyDidDraw() { return notifyDidDraw; },
|
||||
set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
|
||||
didDraw,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,12 @@ 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);
|
||||
Mat4.fromScaling(tmpCylinderMatScale, Vec3.set(tmpCylinderScale, 1, length, 1));
|
||||
Vec3.makeRotation(tmpCylinderMatRot, tmpUp, tmpCylinderMatDir);
|
||||
Mat4.mul(m, tmpCylinderMatRot, tmpCylinderMatScale);
|
||||
@@ -69,10 +69,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 +89,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 +116,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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -406,6 +424,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);
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
@@ -394,7 +394,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,8 +407,8 @@ 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,
|
||||
@@ -424,12 +424,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 +441,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) {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -38,6 +38,8 @@ export function coordinatesFromDcd(dcdFile: DcdFile): Task<Coordinates> {
|
||||
x: dcdFrame.x,
|
||||
y: dcdFrame.y,
|
||||
z: dcdFrame.z,
|
||||
|
||||
xyzOrdering: { isIdentity: true }
|
||||
};
|
||||
|
||||
if (dcdFrame.cell) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
@@ -117,6 +117,6 @@ export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionT
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? InteractionsProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(InteractionsProvider.descriptor, false)
|
||||
detach: (data) => data.structure && InteractionsProvider.ref(data.structure, false)
|
||||
}
|
||||
};
|
||||
@@ -70,6 +70,6 @@ export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThem
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && CrossLinkRestraint.isApplicable(ctx.structure),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? CrossLinkRestraintProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(CrossLinkRestraintProvider.descriptor, false)
|
||||
detach: (data) => data.structure && CrossLinkRestraintProvider.ref(data.structure, false)
|
||||
}
|
||||
};
|
||||
95
src/mol-model-props/sequence/best-database-mapping.ts
Normal file
95
src/mol-model-props/sequence/best-database-mapping.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column } from '../../mol-data/db';
|
||||
import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
|
||||
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { Model } from '../../mol-model/structure';
|
||||
import { StructureElement } from '../../mol-model/structure/structure';
|
||||
import { CustomModelProperty } from '../common/custom-model-property';
|
||||
|
||||
export { BestDatabaseSequenceMapping };
|
||||
|
||||
interface BestDatabaseSequenceMapping {
|
||||
readonly dbName: string[],
|
||||
readonly accession: string[],
|
||||
readonly num: number[],
|
||||
readonly residue: string[]
|
||||
}
|
||||
|
||||
namespace BestDatabaseSequenceMapping {
|
||||
export const Provider: CustomModelProperty.Provider<{}, BestDatabaseSequenceMapping> = CustomModelProperty.createProvider({
|
||||
label: 'Best Database Sequence Mapping',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'molstar_best_database_sequence_mapping'
|
||||
}),
|
||||
type: 'static',
|
||||
defaultParams: {},
|
||||
getParams: () => ({}),
|
||||
isApplicable: (data: Model) => MmcifFormat.is(data.sourceData) && data.sourceData.data.frame.categories?.atom_site?.fieldNames.indexOf('db_name') >= 0,
|
||||
obtain: async (ctx, data) => {
|
||||
return { value: fromCif(data) };
|
||||
}
|
||||
});
|
||||
|
||||
export function getKey(loc: StructureElement.Location) {
|
||||
const model = loc.unit.model;
|
||||
const data = Provider.get(model).value;
|
||||
if (!data) return '';
|
||||
const eI = loc.unit.elements[loc.element];
|
||||
const rI = model.atomicHierarchy.residueAtomSegments.offsets[eI];
|
||||
return data.accession[rI];
|
||||
}
|
||||
|
||||
export function getLabel(loc: StructureElement.Location) {
|
||||
const model = loc.unit.model;
|
||||
const data = Provider.get(model).value;
|
||||
if (!data) return;
|
||||
const eI = loc.unit.elements[loc.element];
|
||||
const rI = model.atomicHierarchy.residueAtomSegments.offsets[eI];
|
||||
const dbName = data.dbName[rI];
|
||||
if (!dbName) return;
|
||||
return `${dbName} ${data.accession[rI]} ${data.num[rI]} ${data.residue[rI]}`;
|
||||
}
|
||||
|
||||
function fromCif(model: Model): BestDatabaseSequenceMapping | undefined {
|
||||
if (!MmcifFormat.is(model.sourceData)) return;
|
||||
|
||||
const { atom_site } = model.sourceData.data.frame.categories;
|
||||
const db_name = atom_site.getField('db_name');
|
||||
const db_acc = atom_site.getField('db_acc');
|
||||
const db_num = atom_site.getField('db_num');
|
||||
const db_res = atom_site.getField('db_res');
|
||||
|
||||
if (!db_name || !db_acc || !db_num || !db_res) return;
|
||||
|
||||
const { atomSourceIndex } = model.atomicHierarchy;
|
||||
const { count, offsets: residueOffsets } = model.atomicHierarchy.residueAtomSegments;
|
||||
const dbName = new Array<string>(count);
|
||||
const accession = new Array<string>(count);
|
||||
const num = new Array<number>(count);
|
||||
const residue = new Array<string>(count);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = atomSourceIndex.value(residueOffsets[i]);
|
||||
|
||||
if (db_name.valueKind(row) !== Column.ValueKind.Present) {
|
||||
dbName[row] = '';
|
||||
accession[row] = '';
|
||||
num[row] = 0;
|
||||
residue[row] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
dbName[row] = db_name.str(row);
|
||||
accession[row] = db_acc.str(row);
|
||||
num[row] = db_num.int(row);
|
||||
residue[row] = db_res.str(row);
|
||||
}
|
||||
|
||||
return { dbName, accession, num, residue };
|
||||
}
|
||||
}
|
||||
105
src/mol-model-props/sequence/themes/best-database-mapping.ts
Normal file
105
src/mol-model-props/sequence/themes/best-database-mapping.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { Bond, StructureElement, Unit } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { getPalette, getPaletteParams } from '../../../mol-util/color/palette';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { BestDatabaseSequenceMapping } from '../best-database-mapping';
|
||||
|
||||
const DefaultColor = Color(0xFAFAFA);
|
||||
const Description = 'Assigns a color based on best dababase sequence mapping.';
|
||||
|
||||
// same colors for same accessions
|
||||
const globalAccessionMap = new Map<string, number>();
|
||||
|
||||
export const BestDatabaseSequenceMappingColorThemeParams = {
|
||||
...getPaletteParams({ type: 'colors', colorList: 'set-1' }),
|
||||
};
|
||||
export type BestDatabaseSequenceMappingColorThemeParams = typeof BestDatabaseSequenceMappingColorThemeParams
|
||||
export function getBestDatabaseSequenceMappingColorThemeParams(ctx: ThemeDataContext) {
|
||||
return BestDatabaseSequenceMappingColorThemeParams; // TODO return copy
|
||||
}
|
||||
export function BestDatabaseSequenceMappingColorTheme(ctx: ThemeDataContext, props: PD.Values<BestDatabaseSequenceMappingColorThemeParams>): ColorTheme<BestDatabaseSequenceMappingColorThemeParams> {
|
||||
let color: LocationColor;
|
||||
|
||||
if (ctx.structure) {
|
||||
for (const m of ctx.structure.models) {
|
||||
const mapping = BestDatabaseSequenceMapping.Provider.get(m).value;
|
||||
if (!mapping) continue;
|
||||
for (const acc of mapping.accession) {
|
||||
if (!acc || globalAccessionMap.has(acc)) continue;
|
||||
globalAccessionMap.set(acc, globalAccessionMap.size);
|
||||
}
|
||||
}
|
||||
|
||||
const l = StructureElement.Location.create(ctx.structure);
|
||||
const palette = getPalette(globalAccessionMap.size + 1, props, { valueLabel: i => `${i}` });
|
||||
const colorMap = new Map<string, Color>();
|
||||
|
||||
const getColor = (location: StructureElement.Location) => {
|
||||
const key = BestDatabaseSequenceMapping.getKey(location);
|
||||
if (!key) return DefaultColor;
|
||||
|
||||
if (colorMap.has(key)) return colorMap.get(key)!;
|
||||
|
||||
const color = palette.color(globalAccessionMap.get(key)!);
|
||||
colorMap.set(key, color);
|
||||
return color;
|
||||
};
|
||||
|
||||
color = (location: Location): Color => {
|
||||
if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) {
|
||||
return getColor(location);
|
||||
} else if (Bond.isLocation(location)) {
|
||||
l.unit = location.aUnit;
|
||||
l.element = location.aUnit.elements[location.aIndex];
|
||||
return getColor(l);
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
} else {
|
||||
color = () => DefaultColor;
|
||||
}
|
||||
|
||||
return {
|
||||
factory: BestDatabaseSequenceMappingColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color,
|
||||
props,
|
||||
description: Description,
|
||||
};
|
||||
}
|
||||
|
||||
export const BestDatabaseSequenceMappingColorThemeProvider: ColorTheme.Provider<BestDatabaseSequenceMappingColorThemeParams, 'best-sequence-database-mapping'> = {
|
||||
name: 'best-sequence-database-mapping',
|
||||
label: 'Best Database Sequence Mapping',
|
||||
category: ColorTheme.Category.Residue,
|
||||
factory: BestDatabaseSequenceMappingColorTheme,
|
||||
getParams: getBestDatabaseSequenceMappingColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(BestDatabaseSequenceMappingColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => BestDatabaseSequenceMapping.Provider.isApplicable(m)),
|
||||
ensureCustomProperties: {
|
||||
attach: async (ctx: CustomProperty.Context, data: ThemeDataContext) => {
|
||||
if (!data.structure) return;
|
||||
|
||||
for (const m of data.structure.models) {
|
||||
await BestDatabaseSequenceMapping.Provider.attach(ctx, m, void 0, true);
|
||||
}
|
||||
},
|
||||
detach: (data) => {
|
||||
if (!data.structure) return;
|
||||
for (const m of data.structure.models) {
|
||||
BestDatabaseSequenceMapping.Provider.ref(m, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* 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 David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { arrayEqual, UUID } from '../../../mol-util';
|
||||
import { Cell } from '../../../mol-math/geometry/spacegroup/cell';
|
||||
import { AtomicConformation } from '../model/properties/atomic';
|
||||
import { Column } from '../../../mol-data/db';
|
||||
@@ -34,6 +35,12 @@ export interface Frame {
|
||||
readonly fy: ArrayLike<number>
|
||||
readonly fz: ArrayLike<number>
|
||||
}
|
||||
|
||||
readonly xyzOrdering: {
|
||||
isIdentity: boolean,
|
||||
frozen?: boolean,
|
||||
index?: ArrayLike<number>,
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -85,41 +92,106 @@ namespace Coordinates {
|
||||
hasVelocities,
|
||||
hasForces,
|
||||
deltaTime,
|
||||
timeOffset
|
||||
timeOffset,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAtomicConformation(frame: Frame, atomId: Column<number>): AtomicConformation {
|
||||
/**
|
||||
* Only use ordering if it's not identity.
|
||||
*/
|
||||
export function getAtomicConformation(frame: Frame, atomId: Column<number>, ordering?: ArrayLike<number>): AtomicConformation {
|
||||
let { x, y, z } = frame;
|
||||
|
||||
if (frame.xyzOrdering.frozen) {
|
||||
if (ordering) {
|
||||
if (frame.xyzOrdering.isIdentity) {
|
||||
// simple list reordering
|
||||
x = getOrderedCoords(x, ordering);
|
||||
y = getOrderedCoords(y, ordering);
|
||||
z = getOrderedCoords(z, ordering);
|
||||
} else if (!arrayEqual(frame.xyzOrdering.index! as any, ordering as any)) {
|
||||
x = getSourceOrderedCoords(x, frame.xyzOrdering.index!, ordering);
|
||||
y = getSourceOrderedCoords(y, frame.xyzOrdering.index!, ordering);
|
||||
z = getSourceOrderedCoords(z, frame.xyzOrdering.index!, ordering);
|
||||
}
|
||||
} else if (!frame.xyzOrdering.isIdentity) {
|
||||
x = getInvertedCoords(x, frame.xyzOrdering.index!);
|
||||
y = getInvertedCoords(y, frame.xyzOrdering.index!);
|
||||
z = getInvertedCoords(z, frame.xyzOrdering.index!);
|
||||
}
|
||||
} else if (ordering) {
|
||||
if (frame.xyzOrdering.isIdentity) {
|
||||
frame.xyzOrdering.isIdentity = false;
|
||||
frame.xyzOrdering.index = ordering;
|
||||
reorderCoordsInPlace(x as unknown as number[], ordering);
|
||||
reorderCoordsInPlace(y as unknown as number[], ordering);
|
||||
reorderCoordsInPlace(z as unknown as number[], ordering);
|
||||
} else {
|
||||
// is current ordering is not the same as requested?
|
||||
// => copy the conformations into a new array
|
||||
if (!arrayEqual(frame.xyzOrdering.index! as any, ordering as any)) {
|
||||
x = getSourceOrderedCoords(x, frame.xyzOrdering.index!, ordering);
|
||||
y = getSourceOrderedCoords(y, frame.xyzOrdering.index!, ordering);
|
||||
z = getSourceOrderedCoords(z, frame.xyzOrdering.index!, ordering);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// once the conformation has been accessed at least once, freeze it.
|
||||
// => any other request to the frame with different ordering will result in a copy.
|
||||
frame.xyzOrdering.frozen = true;
|
||||
|
||||
return {
|
||||
id: UUID.create22(),
|
||||
atomId,
|
||||
occupancy: Column.ofConst(1, frame.elementCount, Column.Schema.int),
|
||||
B_iso_or_equiv: Column.ofConst(0, frame.elementCount, Column.Schema.float),
|
||||
xyzDefined: true,
|
||||
x: frame.x,
|
||||
y: frame.y,
|
||||
z: frame.z,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
};
|
||||
}
|
||||
|
||||
function reorderCoords(xs: ArrayLike<number>, index: ArrayLike<number>) {
|
||||
const ret = new Float32Array(xs.length);
|
||||
const _reorderBuffer = [0.123];
|
||||
function reorderCoordsInPlace(xs: number[], index: ArrayLike<number>) {
|
||||
const buffer = _reorderBuffer;
|
||||
|
||||
for (let i = 0, _i = xs.length; i < _i; i++) {
|
||||
ret[i] = xs[index[i]];
|
||||
buffer[i] = xs[index[i]];
|
||||
}
|
||||
for (let i = 0, _i = xs.length; i < _i; i++) {
|
||||
xs[i] = buffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
function getSourceOrderedCoords(xs: ArrayLike<number>, srcIndex: ArrayLike<number>, index: ArrayLike<number>) {
|
||||
const ret = new Float32Array(xs.length);
|
||||
|
||||
for (let i = 0, _i = xs.length; i < _i; i++) {
|
||||
ret[i] = xs[srcIndex[index[i]]];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getAtomicConformationReordered(frame: Frame, atomId: Column<number>, srcIndex: ArrayLike<number>): AtomicConformation {
|
||||
return {
|
||||
id: UUID.create22(),
|
||||
atomId,
|
||||
occupancy: Column.ofConst(1, frame.elementCount, Column.Schema.int),
|
||||
B_iso_or_equiv: Column.ofConst(0, frame.elementCount, Column.Schema.float),
|
||||
xyzDefined: true,
|
||||
x: reorderCoords(frame.x, srcIndex),
|
||||
y: reorderCoords(frame.y, srcIndex),
|
||||
z: reorderCoords(frame.z, srcIndex)
|
||||
};
|
||||
function getOrderedCoords(xs: ArrayLike<number>, index: ArrayLike<number>) {
|
||||
const ret = new Float32Array(xs.length);
|
||||
|
||||
for (let i = 0, _i = xs.length; i < _i; i++) {
|
||||
ret[i] = xs[index[i]];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getInvertedCoords(xs: ArrayLike<number>, index: ArrayLike<number>) {
|
||||
const ret = new Float32Array(xs.length);
|
||||
|
||||
for (let i = 0, _i = xs.length; i < _i; i++) {
|
||||
ret[index[i]] = xs[i];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -105,9 +105,7 @@ export namespace Model {
|
||||
...model,
|
||||
id: UUID.create22(),
|
||||
modelNum: i,
|
||||
atomicConformation: isIdentity
|
||||
? Coordinates.getAtomicConformation(f, model.atomicConformation.atomId)
|
||||
: Coordinates.getAtomicConformationReordered(f, model.atomicConformation.atomId, srcIndexArray!),
|
||||
atomicConformation: Coordinates.getAtomicConformation(f, model.atomicConformation.atomId, srcIndexArray),
|
||||
// TODO: add support for supplying sphere and gaussian coordinates in addition to atomic coordinates?
|
||||
// coarseConformation: coarse.conformation,
|
||||
customProperties: new CustomProperties(),
|
||||
@@ -131,32 +129,14 @@ export namespace Model {
|
||||
return new ArrayTrajectory(_trajectoryFromModelAndCoordinates(model, coordinates).trajectory);
|
||||
}
|
||||
|
||||
export function invertIndex(xs: ArrayLike<number>) {
|
||||
const ret = new Int32Array(xs.length);
|
||||
for (let i = 0, _i = xs.length; i < _i; i++) {
|
||||
ret[xs[i]] = i;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function trajectoryFromTopologyAndCoordinates(topology: Topology, coordinates: Coordinates): Task<Trajectory> {
|
||||
return Task.create('Create Trajectory', async ctx => {
|
||||
const models = await createModels(topology.basic, topology.sourceData, ctx);
|
||||
if (models.frameCount === 0) throw new Error('found no model');
|
||||
const model = models.representative;
|
||||
const { trajectory, srcIndexArray } = _trajectoryFromModelAndCoordinates(model, coordinates);
|
||||
const { trajectory } = _trajectoryFromModelAndCoordinates(model, coordinates);
|
||||
|
||||
// TODO: cache the inverted index somewhere?
|
||||
const invertedIndex = srcIndexArray ? invertIndex(srcIndexArray) : void 0;
|
||||
const pairs = srcIndexArray
|
||||
? {
|
||||
indexA: Column.ofIntArray(Column.mapToArray(topology.bonds.indexA, i => invertedIndex![i], Int32Array)),
|
||||
indexB: Column.ofIntArray(Column.mapToArray(topology.bonds.indexB, i => invertedIndex![i], Int32Array)),
|
||||
order: topology.bonds.order
|
||||
}
|
||||
: topology.bonds;
|
||||
|
||||
const bondData = { pairs, count: model.atomicHierarchy.atoms._rowCount };
|
||||
const bondData = { pairs: topology.bonds, count: model.atomicHierarchy.atoms._rowCount };
|
||||
const indexPairBonds = IndexPairBonds.fromData(bondData);
|
||||
|
||||
let index = 0;
|
||||
@@ -176,6 +156,25 @@ export namespace Model {
|
||||
return center;
|
||||
}
|
||||
|
||||
function invertIndex(xs: Column<number>) {
|
||||
const invertedIndex = new Int32Array(xs.rowCount);
|
||||
let isIdentity = false;
|
||||
for (let i = 0, _i = xs.rowCount; i < _i; i++) {
|
||||
const x = xs.value(i);
|
||||
if (x !== i) isIdentity = false;
|
||||
invertedIndex[x] = i;
|
||||
}
|
||||
return { isIdentity, invertedIndex: invertedIndex as unknown as ArrayLike<ElementIndex> };
|
||||
}
|
||||
|
||||
const InvertedAtomSrcIndexProp = '__InvertedAtomSrcIndex__';
|
||||
export function getInvertedAtomSourceIndex(model: Model): { isIdentity: boolean, invertedIndex: ArrayLike<ElementIndex> } {
|
||||
if (model._staticPropertyData[InvertedAtomSrcIndexProp]) return model._staticPropertyData[InvertedAtomSrcIndexProp];
|
||||
const index = invertIndex(model.atomicHierarchy.atomSourceIndex);
|
||||
model._staticPropertyData[InvertedAtomSrcIndexProp] = index;
|
||||
return index;
|
||||
}
|
||||
|
||||
const TrajectoryInfoProp = '__TrajectoryInfo__';
|
||||
export type TrajectoryInfo = { readonly index: number, readonly size: number }
|
||||
export const TrajectoryInfo = {
|
||||
|
||||
@@ -278,7 +278,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
|
||||
|
||||
for (let li = 0; li < buCount; li++) {
|
||||
const lu = bondedUnits[li];
|
||||
const bUnit = structure.unitMap.get(lu.unitB) as Unit.Atomic;
|
||||
const bUnit = input.unitMap.get(lu.unitB) as Unit.Atomic;
|
||||
const bElements = bUnit.elements;
|
||||
const bonds = lu.getEdges(inputIndex);
|
||||
for (let bi = 0, _bi = bonds.length; bi < _bi; bi++) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { CustomStructureProperty } from '../../../mol-model-props/common/custom-
|
||||
import { Trajectory } from '../trajectory';
|
||||
import { RuntimeContext, Task } from '../../../mol-task';
|
||||
import { computeStructureBoundary } from './util/boundary';
|
||||
import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
|
||||
|
||||
/** Internal structure state */
|
||||
type State = {
|
||||
@@ -1290,6 +1291,14 @@ namespace Structure {
|
||||
|
||||
export type Index = number;
|
||||
export const Index = CustomStructureProperty.createSimple<Index>('index', 'root');
|
||||
|
||||
const PrincipalAxesProp = '__PrincipalAxes__';
|
||||
export function getPrincipalAxes(structure: Structure): PrincipalAxes {
|
||||
if (structure.currentPropertyData[PrincipalAxesProp]) return structure.currentPropertyData[PrincipalAxesProp];
|
||||
const principalAxes = StructureElement.Loci.getPrincipalAxes(Structure.toStructureElementLoci(structure));
|
||||
structure.currentPropertyData[PrincipalAxesProp] = principalAxes;
|
||||
return principalAxes;
|
||||
}
|
||||
}
|
||||
|
||||
export { Structure };
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2020 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.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -23,6 +23,10 @@ const __ElementIndex: { [e: string]: number | undefined } = { 'H': 0, 'h': 0, 'D
|
||||
const __ElementBondThresholds: { [e: number]: number | undefined } = { 0: 1.42, 1: 1.42, 3: 2.7, 4: 2.7, 6: 1.75, 7: 1.6, 8: 1.52, 11: 2.7, 12: 2.7, 13: 2.7, 14: 1.9, 15: 2.0, 16: 1.9, 17: 1.8, 19: 2.7, 20: 2.7, 21: 2.7, 22: 2.7, 23: 2.7, 24: 2.7, 25: 2.7, 26: 2.7, 27: 2.7, 28: 2.7, 29: 2.7, 30: 2.7, 31: 2.7, 33: 2.68, 37: 2.7, 38: 2.7, 39: 2.7, 40: 2.7, 41: 2.7, 42: 2.7, 43: 2.7, 44: 2.7, 45: 2.7, 46: 2.7, 47: 2.7, 48: 2.7, 49: 2.7, 50: 2.7, 55: 2.7, 56: 2.7, 57: 2.7, 58: 2.7, 59: 2.7, 60: 2.7, 61: 2.7, 62: 2.7, 63: 2.7, 64: 2.7, 65: 2.7, 66: 2.7, 67: 2.7, 68: 2.7, 69: 2.7, 70: 2.7, 71: 2.7, 72: 2.7, 73: 2.7, 74: 2.7, 75: 2.7, 76: 2.7, 77: 2.7, 78: 2.7, 79: 2.7, 80: 2.7, 81: 2.7, 82: 2.7, 83: 2.7, 87: 2.7, 88: 2.7, 89: 2.7, 90: 2.7, 91: 2.7, 92: 2.7, 93: 2.7, 94: 2.7, 95: 2.7, 96: 2.7, 97: 2.7, 98: 2.7, 99: 2.7, 100: 2.7, 101: 2.7, 102: 2.7, 103: 2.7, 104: 2.7, 105: 2.7, 106: 2.7, 107: 2.7, 108: 2.7, 109: 2.88 };
|
||||
|
||||
/**
|
||||
* Added O-S (316) as 1.8
|
||||
* N-O-S bridge (e.g. LYS-CSO in 7B0L, 6ZWJ, 6ZWH)
|
||||
* (https://www.nature.com/articles/s41586-021-03513-3?s=09)
|
||||
*
|
||||
* Increased N-N (112) threshold from 1.55 to 1.6 (e.g. for 0QH in 1BMA)
|
||||
*
|
||||
* More experimentally observed bond length here (https://cccbdb.nist.gov/expbondlengths1x.asp)
|
||||
@@ -51,8 +55,8 @@ const __ElementBondThresholds: { [e: number]: number | undefined } = { 0: 1.42,
|
||||
* Added P-H (135) as 1.47
|
||||
* P-H (https://cccbdb.nist.gov/expbondlengths2x.asp?descript=rPH)
|
||||
* - Average 1.423 ((+/- 0.007)
|
||||
* - Min 0.912
|
||||
* - Max 1.033
|
||||
* - Min 1.414
|
||||
* - Max 1.435
|
||||
*
|
||||
* Added S-H (152) as 1.45
|
||||
* S-H (https://cccbdb.nist.gov/expbondlengths2x.asp?descript=rSH)
|
||||
@@ -63,7 +67,7 @@ const __ElementBondThresholds: { [e: number]: number | undefined } = { 0: 1.42,
|
||||
* Added Si-Si (420) as 2.37
|
||||
* (https://cccbdb.nist.gov/expbondlengths2x.asp?descript=rSiSi)
|
||||
*/
|
||||
const __ElementPairThresholds: { [e: number]: number | undefined } = { 0: 0.8, 20: 1.31, 27: 1.2, 35: 1.15, 44: 1.1, 54: 1, 60: 1.84, 72: 1.88, 84: 1.75, 85: 1.56, 86: 1.76, 98: 1.6, 99: 1.68, 100: 1.63, 112: 1.6, 113: 1.59, 114: 1.36, 129: 1.45, 135: 1.47, 144: 1.6, 152: 1.45, 170: 1.4, 180: 1.55, 202: 2.4, 222: 2.24, 224: 1.91, 225: 1.98, 243: 2.02, 269: 2, 293: 1.9, 420: 2.37, 480: 2.3, 512: 2.3, 544: 2.3, 612: 2.1, 629: 1.54, 665: 1, 813: 2.6, 854: 2.27, 894: 1.93, 896: 2.1, 937: 2.05, 938: 2.06, 981: 1.62, 1258: 2.68, 1309: 2.33, 1484: 1, 1763: 2.14, 1823: 2.48, 1882: 2.1, 1944: 1.72, 2380: 2.34, 3367: 2.44, 3733: 2.11, 3819: 2.6, 3821: 2.36, 4736: 2.75, 5724: 2.73, 5959: 2.63, 6519: 2.84, 6750: 2.87, 8991: 2.81 };
|
||||
const __ElementPairThresholds: { [e: number]: number | undefined } = { 0: 0.8, 20: 1.31, 27: 1.2, 35: 1.15, 44: 1.1, 54: 1, 60: 1.84, 72: 1.88, 84: 1.75, 85: 1.56, 86: 1.76, 98: 1.6, 99: 1.68, 100: 1.63, 112: 1.6, 113: 1.59, 114: 1.36, 129: 1.45, 135: 1.47, 144: 1.6, 152: 1.45, 170: 1.4, 180: 1.55, 202: 2.4, 222: 2.24, 224: 1.91, 225: 1.98, 243: 2.02, 269: 2, 293: 1.9, 316: 1.8, 420: 2.37, 480: 2.3, 512: 2.3, 544: 2.3, 612: 2.1, 629: 1.54, 665: 1, 813: 2.6, 854: 2.27, 894: 1.93, 896: 2.1, 937: 2.05, 938: 2.06, 981: 1.62, 1258: 2.68, 1309: 2.33, 1484: 1, 1763: 2.14, 1823: 2.48, 1882: 2.1, 1944: 1.72, 2380: 2.34, 3367: 2.44, 3733: 2.11, 3819: 2.6, 3821: 2.36, 4736: 2.75, 5724: 2.73, 5959: 2.63, 6519: 2.84, 6750: 2.87, 8991: 2.81 };
|
||||
|
||||
const __DefaultBondingRadius = 2.001;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { IndexPairBonds } from '../../../../../mol-model-formats/structure/prope
|
||||
import { InterUnitGraph } from '../../../../../mol-math/graph/inter-unit-graph';
|
||||
import { StructConn } from '../../../../../mol-model-formats/structure/property/bonds/struct_conn';
|
||||
import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
|
||||
import { Model } from '../../../model';
|
||||
|
||||
const MAX_RADIUS = 4;
|
||||
|
||||
@@ -48,7 +49,10 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
|
||||
const hasOccupancy = occupancyA.isDefined && occupancyB.isDefined;
|
||||
|
||||
const structConn = unitA.model === unitB.model && StructConn.Provider.get(unitA.model);
|
||||
const indexPairs = unitA.model === unitB.model && IndexPairBonds.Provider.get(unitA.model);
|
||||
const indexPairs = !props.forceCompute && unitA.model === unitB.model && IndexPairBonds.Provider.get(unitA.model);
|
||||
|
||||
const { atomSourceIndex: sourceIndex } = unitA.model.atomicHierarchy;
|
||||
const { invertedIndex } = indexPairs ? Model.getInvertedAtomSourceIndex(unitB.model) : { invertedIndex: void 0 };
|
||||
|
||||
const structConnExhaustive = unitA.model === unitB.model && StructConn.isExhaustive(unitA.model);
|
||||
|
||||
@@ -70,8 +74,10 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
|
||||
|
||||
if (!props.forceCompute && indexPairs) {
|
||||
const { order, distance, flag } = indexPairs.edgeProps;
|
||||
for (let i = indexPairs.offset[aI], il = indexPairs.offset[aI + 1]; i < il; ++i) {
|
||||
const bI = indexPairs.b[i];
|
||||
|
||||
const srcA = sourceIndex.value(aI);
|
||||
for (let i = indexPairs.offset[srcA], il = indexPairs.offset[srcA + 1]; i < il; ++i) {
|
||||
const bI = invertedIndex![indexPairs.b[i]];
|
||||
|
||||
const _bI = SortedArray.indexOf(unitB.elements, bI) as StructureElement.UnitIndex;
|
||||
if (_bI < 0) continue;
|
||||
|
||||
@@ -51,6 +51,9 @@ function findIndexPairBonds(unit: Unit.Atomic) {
|
||||
const atomCount = unit.elements.length;
|
||||
const { edgeProps } = indexPairs;
|
||||
|
||||
const { atomSourceIndex: sourceIndex } = unit.model.atomicHierarchy;
|
||||
const { invertedIndex } = Model.getInvertedAtomSourceIndex(unit.model);
|
||||
|
||||
const atomA: StructureElement.UnitIndex[] = [];
|
||||
const atomB: StructureElement.UnitIndex[] = [];
|
||||
const flags: number[] = [];
|
||||
@@ -60,8 +63,10 @@ function findIndexPairBonds(unit: Unit.Atomic) {
|
||||
const aI = atoms[_aI];
|
||||
const isHa = type_symbol.value(aI) === 'H';
|
||||
|
||||
for (let i = indexPairs.offset[aI], il = indexPairs.offset[aI + 1]; i < il; ++i) {
|
||||
const bI = indexPairs.b[i];
|
||||
const srcA = sourceIndex.value(aI);
|
||||
|
||||
for (let i = indexPairs.offset[srcA], il = indexPairs.offset[srcA + 1]; i < il; ++i) {
|
||||
const bI = invertedIndex[indexPairs.b[i]];
|
||||
if (aI >= bI) continue;
|
||||
|
||||
const _bI = SortedArray.indexOf(unit.elements, bI) as StructureElement.UnitIndex;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { OrderedSet } from '../../../../mol-data/int';
|
||||
import { StructureUniqueSubsetBuilder } from './unique-subset-builder';
|
||||
import { StructureElement } from '../element';
|
||||
import { Unit } from '../unit';
|
||||
import { UnitIndex } from '../element/util';
|
||||
|
||||
export interface StructureResult extends Result<StructureElement.UnitIndex> {
|
||||
units: Unit[]
|
||||
@@ -40,6 +41,16 @@ export namespace StructureResult {
|
||||
}
|
||||
}
|
||||
|
||||
export interface StructureLookup3DResultContext {
|
||||
result: StructureResult,
|
||||
closeUnitsResult: Result<number>,
|
||||
unitGroupResult: Result<UnitIndex>,
|
||||
}
|
||||
|
||||
export function StructureLookup3DResultContext(): StructureLookup3DResultContext {
|
||||
return { result: StructureResult.create(), closeUnitsResult: Result.create(), unitGroupResult: Result.create() };
|
||||
}
|
||||
|
||||
export class StructureLookup3D {
|
||||
private unitLookup: Lookup3D;
|
||||
private pivot = Vec3();
|
||||
@@ -48,12 +59,17 @@ export class StructureLookup3D {
|
||||
return this.unitLookup.find(x, y, z, radius);
|
||||
}
|
||||
|
||||
private result = StructureResult.create();
|
||||
find(x: number, y: number, z: number, radius: number): StructureResult {
|
||||
Result.reset(this.result);
|
||||
private findContext = StructureLookup3DResultContext();
|
||||
|
||||
find(x: number, y: number, z: number, radius: number, ctx?: StructureLookup3DResultContext): StructureResult {
|
||||
return this._find(x, y, z, radius, ctx ?? this.findContext);
|
||||
}
|
||||
|
||||
private _find(x: number, y: number, z: number, radius: number, ctx: StructureLookup3DResultContext): StructureResult {
|
||||
Result.reset(ctx.result);
|
||||
const { units } = this.structure;
|
||||
const closeUnits = this.unitLookup.find(x, y, z, radius);
|
||||
if (closeUnits.count === 0) return this.result;
|
||||
const closeUnits = this.unitLookup.find(x, y, z, radius, ctx.closeUnitsResult);
|
||||
if (closeUnits.count === 0) return ctx.result;
|
||||
|
||||
for (let t = 0, _t = closeUnits.count; t < _t; t++) {
|
||||
const unit = units[closeUnits.indices[t]];
|
||||
@@ -62,12 +78,12 @@ export class StructureLookup3D {
|
||||
Vec3.transformMat4(this.pivot, this.pivot, unit.conformation.operator.inverse);
|
||||
}
|
||||
const unitLookup = unit.lookup3d;
|
||||
const groupResult = unitLookup.find(this.pivot[0], this.pivot[1], this.pivot[2], radius);
|
||||
const groupResult = unitLookup.find(this.pivot[0], this.pivot[1], this.pivot[2], radius, ctx.unitGroupResult);
|
||||
for (let j = 0, _j = groupResult.count; j < _j; j++) {
|
||||
StructureResult.add(this.result, unit, groupResult.indices[j], groupResult.squaredDistances[j]);
|
||||
StructureResult.add(ctx.result, unit, groupResult.indices[j], groupResult.squaredDistances[j]);
|
||||
}
|
||||
}
|
||||
return this.result;
|
||||
return ctx.result;
|
||||
}
|
||||
|
||||
findIntoBuilder(x: number, y: number, z: number, radius: number, builder: StructureUniqueSubsetBuilder) {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Segmentation } from '../../../../mol-data/int';
|
||||
import { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
|
||||
import { BestDatabaseSequenceMapping } from '../../../../mol-model-props/sequence/best-database-mapping';
|
||||
import { ElementIndex } from '../../model/indexing';
|
||||
import { Structure } from '../structure';
|
||||
import { Unit } from '../unit';
|
||||
|
||||
export interface AlignmentResult {
|
||||
transform: MinimizeRmsd.Result,
|
||||
pivot: number,
|
||||
other: number
|
||||
}
|
||||
|
||||
export function alignAndSuperposeWithBestDatabaseMapping(structures: Structure[]): AlignmentResult[] {
|
||||
const indexMap = new Map<string, IndexEntry>();
|
||||
|
||||
for (let i = 0; i < structures.length; i++) {
|
||||
buildIndex(structures[i], indexMap, i);
|
||||
}
|
||||
|
||||
const index = Array.from(indexMap.values());
|
||||
|
||||
// TODO: support non-first structure pivots
|
||||
const pairs = findPairs(structures.length, index);
|
||||
|
||||
const ret: AlignmentResult[] = [];
|
||||
for (const p of pairs) {
|
||||
const [a, b] = getPositionTables(index, p.i, p.j, p.count);
|
||||
const transform = MinimizeRmsd.compute({ a, b });
|
||||
ret.push({ transform, pivot: p.i, other: p.j });
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getPositionTables(index: IndexEntry[], pivot: number, other: number, N: number) {
|
||||
const xs = MinimizeRmsd.Positions.empty(N);
|
||||
const ys = MinimizeRmsd.Positions.empty(N);
|
||||
|
||||
let o = 0;
|
||||
for (const { pivots } of index) {
|
||||
const a = pivots[pivot];
|
||||
const b = pivots[other];
|
||||
if (!a || !b) continue;
|
||||
|
||||
const l = Math.min(a[2] - a[1], b[2] - b[1]);
|
||||
|
||||
for (let i = 0; i < l; i++) {
|
||||
let eI = (a[1] + i) as ElementIndex;
|
||||
xs.x[o] = a[0].conformation.x(eI);
|
||||
xs.y[o] = a[0].conformation.y(eI);
|
||||
xs.z[o] = a[0].conformation.z(eI);
|
||||
|
||||
eI = (b[1] + i) as ElementIndex;
|
||||
ys.x[o] = b[0].conformation.x(eI);
|
||||
ys.y[o] = b[0].conformation.y(eI);
|
||||
ys.z[o] = b[0].conformation.z(eI);
|
||||
o++;
|
||||
}
|
||||
}
|
||||
|
||||
return [xs, ys];
|
||||
}
|
||||
|
||||
function findPairs(N: number, index: IndexEntry[]) {
|
||||
const pairwiseCounts: number[][] = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
pairwiseCounts[i] = [];
|
||||
for (let j = 0; j < N; j++) pairwiseCounts[i][j] = 0;
|
||||
}
|
||||
|
||||
for (const { pivots } of index) {
|
||||
for (let i = 0; i < N; i++) {
|
||||
if (!pivots[i]) continue;
|
||||
|
||||
const lI = pivots[i]![2] - pivots[i]![1];
|
||||
|
||||
for (let j = i + 1; j < N; j++) {
|
||||
if (!pivots[j]) continue;
|
||||
|
||||
const lJ = pivots[j]![2] - pivots[j]![1];
|
||||
pairwiseCounts[i][j] = pairwiseCounts[i][j] + Math.min(lI, lJ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ret: { i: number, j: number, count: number }[] = [];
|
||||
|
||||
for (let j = 1; j < N; j++) {
|
||||
ret[j - 1] = { i: 0, j, count: pairwiseCounts[0][j] };
|
||||
}
|
||||
|
||||
// TODO: support non-first structure pivots
|
||||
// for (let i = 0; i < N - 1; i++) {
|
||||
// let max = 0, maxJ = i;
|
||||
// for (let j = i + 1; j < N; j++) {
|
||||
// if (pairwiseCounts[i][j] > max) {
|
||||
// maxJ = j;
|
||||
// max = pairwiseCounts[i][j];
|
||||
// }
|
||||
// }
|
||||
|
||||
// ret[i] = { i, j: maxJ, count: max };
|
||||
// }
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
interface IndexEntry {
|
||||
key: string,
|
||||
pivots: { [i: number]: [unit: Unit.Atomic, start: ElementIndex, end: ElementIndex] | undefined }
|
||||
}
|
||||
|
||||
function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: number) {
|
||||
for (const unit of structure.units) {
|
||||
if (unit.kind !== Unit.Kind.Atomic) continue;
|
||||
|
||||
const { elements, model } = unit;
|
||||
const { offsets: residueOffset } = model.atomicHierarchy.residueAtomSegments;
|
||||
|
||||
const map = BestDatabaseSequenceMapping.Provider.get(model).value;
|
||||
if (!map) return;
|
||||
|
||||
const { dbName, accession, num } = map;
|
||||
|
||||
const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
|
||||
const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements);
|
||||
|
||||
while (chainsIt.hasNext) {
|
||||
const chainSegment = chainsIt.move();
|
||||
residuesIt.setSegment(chainSegment);
|
||||
while (residuesIt.hasNext) {
|
||||
const residueSegment = residuesIt.move();
|
||||
const eI = elements[residueSegment.start];
|
||||
const rI = residueOffset[eI];
|
||||
|
||||
if (!dbName[rI]) continue;
|
||||
|
||||
const key = `${dbName[rI]}-${accession[rI]}-${num[rI]}`;
|
||||
|
||||
if (!index.has(key)) {
|
||||
index.set(key, { key, pivots: { [sI]: [unit, eI, elements[residueSegment.end]] } });
|
||||
} else {
|
||||
const entry = index.get(key)!;
|
||||
|
||||
if (!entry.pivots[sI]) {
|
||||
entry.pivots[sI] = [unit, eI, elements[residueSegment.end]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/mol-plugin-state/animation/built-in/spin-structure.ts
Normal file
73
src/mol-plugin-state/animation/built-in/spin-structure.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginCommands } from '../../../mol-plugin/commands';
|
||||
import { StateSelection } from '../../../mol-state';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PluginStateObject } from '../../objects';
|
||||
import { StateTransforms } from '../../transforms';
|
||||
import { PluginStateAnimation } from '../model';
|
||||
|
||||
export const AnimateStructureSpin = PluginStateAnimation.create({
|
||||
name: 'built-in.animate-structure-spin',
|
||||
display: { name: 'Spin Structure' },
|
||||
isExportable: true,
|
||||
params: () => ({
|
||||
durationInMs: PD.Numeric(3000, { min: 100, max: 10000, step: 100})
|
||||
}),
|
||||
initialState: () => ({ t: 0 }),
|
||||
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
|
||||
async setup(_, __, plugin) {
|
||||
const state = plugin.state.data;
|
||||
const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3D));
|
||||
|
||||
const update = state.build();
|
||||
let changed = false;
|
||||
for (const r of reprs) {
|
||||
const spins = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.SpinStructureRepresentation3D, r.transform.ref));
|
||||
if (spins.length > 0) continue;
|
||||
|
||||
changed = true;
|
||||
update.to(r.transform.ref)
|
||||
.apply(StateTransforms.Representation.SpinStructureRepresentation3D, { t: 0 }, { tags: 'animate-structure-spin' });
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
return update.commit({ doNotUpdateCurrent: true });
|
||||
},
|
||||
teardown(_, __, plugin) {
|
||||
const state = plugin.state.data;
|
||||
const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
|
||||
.withTag('animate-structure-spin'));
|
||||
if (reprs.length === 0) return;
|
||||
|
||||
const update = state.build();
|
||||
for (const r of reprs) update.delete(r.transform.ref);
|
||||
return update.commit();
|
||||
},
|
||||
async apply(animState, t, ctx) {
|
||||
const state = ctx.plugin.state.data;
|
||||
const anims = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.SpinStructureRepresentation3D));
|
||||
|
||||
if (anims.length === 0) {
|
||||
return { kind: 'finished' };
|
||||
}
|
||||
|
||||
const update = state.build();
|
||||
|
||||
const d = (t.current - t.lastApplied) / ctx.params.durationInMs;
|
||||
const newTime = (animState.t + d) % 1;
|
||||
|
||||
for (const m of anims) {
|
||||
update.to(m).update({ ...m.params?.values, t: newTime });
|
||||
}
|
||||
|
||||
await PluginCommands.State.Update(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
|
||||
|
||||
return { kind: 'next', state: { t: newTime } };
|
||||
}
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { SymmetryOperator } from '../../mol-math/geometry';
|
||||
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
@@ -32,4 +34,60 @@ export function explodeStructure(structure: Structure, unitTransforms: Structure
|
||||
|
||||
unitTransforms.setTransform(_transMat, u);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export const SpinStructureParams = {
|
||||
axis: PD.MappedStatic('custom', {
|
||||
structure: PD.Group({
|
||||
principalAxis: PD.Select('dirA', [['dirA', 'A'], ['dirB', 'B'], ['dirC', 'C']] as const)
|
||||
}),
|
||||
custom: PD.Group({
|
||||
vector: PD.Vec3(Vec3.create(0, 0, 1))
|
||||
})
|
||||
}),
|
||||
origin: PD.MappedStatic('structure', {
|
||||
structure: PD.Group({}),
|
||||
custom: PD.Group({
|
||||
vector: PD.Vec3(Vec3.create(0, 0, 0))
|
||||
})
|
||||
}),
|
||||
};
|
||||
export type SpinStructureProps = PD.Values<typeof SpinStructureParams>
|
||||
|
||||
export function getSpinStructureAxisAndOrigin(structure: Structure, props: SpinStructureProps) {
|
||||
let axis: Vec3, origin: Vec3;
|
||||
|
||||
if (props.axis.name === 'custom') {
|
||||
axis = props.axis.params.vector;
|
||||
} else {
|
||||
const pa = Structure.getPrincipalAxes(structure);
|
||||
axis = pa.momentsAxes[props.axis.params.principalAxis];
|
||||
}
|
||||
|
||||
if (props.origin.name === 'custom') {
|
||||
origin = props.origin.params.vector;
|
||||
} else {
|
||||
const pa = Structure.getPrincipalAxes(structure);
|
||||
origin = pa.momentsAxes.origin;
|
||||
}
|
||||
|
||||
return { axis, origin };
|
||||
}
|
||||
|
||||
const _rotMat = Mat4();
|
||||
const _transMat2 = Mat4();
|
||||
const _t = Mat4();
|
||||
export function spinStructure(structure: Structure, unitTransforms: StructureUnitTransforms, t: number, axis: Vec3, origin: Vec3) {
|
||||
for (let i = 0, _i = structure.units.length; i < _i; i++) {
|
||||
const u = structure.units[i];
|
||||
Vec3.negate(_transVec, origin);
|
||||
Mat4.fromTranslation(_transMat, _transVec);
|
||||
Mat4.fromRotation(_rotMat, Math.PI * t * 2, axis);
|
||||
Mat4.fromTranslation(_transMat2, origin);
|
||||
Mat4.mul(_t, _rotMat, _transMat);
|
||||
Mat4.mul(_t, _transMat2, _t);
|
||||
unitTransforms.setTransform(_t, u);
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,9 @@ export const CubeProvider = DataFormatProvider({
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
type DsCifParams = { entryId?: string | string[] };
|
||||
|
||||
export const DscifProvider = DataFormatProvider({
|
||||
label: 'DensityServer CIF',
|
||||
description: 'DensityServer CIF',
|
||||
@@ -200,7 +203,7 @@ export const DscifProvider = DataFormatProvider({
|
||||
isApplicable: (info, data) => {
|
||||
return guessCifVariant(info, data) === 'dscif' ? true : false;
|
||||
},
|
||||
parse: async (plugin, data, params?: Params) => {
|
||||
parse: async (plugin, data, params?: DsCifParams) => {
|
||||
const cifCell = await plugin.build().to(data).apply(StateTransforms.Data.ParseCif).commit();
|
||||
const b = plugin.build().to(cifCell);
|
||||
const blocks = cifCell.obj!.data.blocks.slice(1); // zero block contains query meta-data
|
||||
@@ -208,8 +211,11 @@ export const DscifProvider = DataFormatProvider({
|
||||
if (blocks.length !== 1 && blocks.length !== 2) throw new Error('unknown number of blocks');
|
||||
|
||||
const volumes: StateObjectSelector<PluginStateObject.Volume.Data>[] = [];
|
||||
let i = 0;
|
||||
for (const block of blocks) {
|
||||
volumes.push(b.apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: block.header, entryId: params?.entryId }).selector);
|
||||
const entryId = Array.isArray(params?.entryId) ? params?.entryId[i] : params?.entryId;
|
||||
volumes.push(b.apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: block.header, entryId }).selector);
|
||||
i++;
|
||||
}
|
||||
|
||||
await b.commit();
|
||||
|
||||
@@ -374,18 +374,53 @@ const connectedOnly = StructureSelectionQuery('Connected to Ligand or Carbohydra
|
||||
]), { category: StructureSelectionCategory.Internal, isHidden: true });
|
||||
|
||||
const disulfideBridges = StructureSelectionQuery('Disulfide Bridges', MS.struct.modifier.union([
|
||||
MS.struct.modifier.wholeResidues([
|
||||
MS.struct.combinator.merge([
|
||||
MS.struct.modifier.union([
|
||||
MS.struct.generator.bondedAtomicPairs({
|
||||
0: MS.core.flags.hasAny([
|
||||
MS.struct.bondProperty.flags(),
|
||||
MS.core.type.bitflags([BondType.Flag.Disulfide])
|
||||
MS.struct.modifier.wholeResidues([
|
||||
MS.struct.filter.isConnectedTo({
|
||||
0: MS.struct.generator.atomGroups({
|
||||
'residue-test': MS.core.set.has([MS.set('CYS'), MS.ammp('auth_comp_id')]),
|
||||
'atom-test': MS.core.set.has([MS.set('SG'), MS.ammp('label_atom_id')])
|
||||
}),
|
||||
target: MS.struct.generator.atomGroups({
|
||||
'residue-test': MS.core.set.has([MS.set('CYS'), MS.ammp('auth_comp_id')]),
|
||||
'atom-test': MS.core.set.has([MS.set('SG'), MS.ammp('label_atom_id')])
|
||||
}),
|
||||
'bond-test': true
|
||||
})
|
||||
])
|
||||
]),
|
||||
MS.struct.modifier.union([
|
||||
MS.struct.modifier.wholeResidues([
|
||||
MS.struct.modifier.union([
|
||||
MS.struct.generator.bondedAtomicPairs({
|
||||
0: MS.core.flags.hasAny([
|
||||
MS.struct.bondProperty.flags(),
|
||||
MS.core.type.bitflags([BondType.Flag.Disulfide])
|
||||
])
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
])
|
||||
])
|
||||
]), { category: StructureSelectionCategory.Bond });
|
||||
|
||||
const nosBridges = StructureSelectionQuery('NOS Bridges', MS.struct.modifier.union([
|
||||
MS.struct.modifier.wholeResidues([
|
||||
MS.struct.filter.isConnectedTo({
|
||||
0: MS.struct.generator.atomGroups({
|
||||
'residue-test': MS.core.set.has([MS.set('CSO', 'LYS'), MS.ammp('auth_comp_id')]),
|
||||
'atom-test': MS.core.set.has([MS.set('OD', 'NZ'), MS.ammp('label_atom_id')])
|
||||
}),
|
||||
target: MS.struct.generator.atomGroups({
|
||||
'residue-test': MS.core.set.has([MS.set('CSO', 'LYS'), MS.ammp('auth_comp_id')]),
|
||||
'atom-test': MS.core.set.has([MS.set('OD', 'NZ'), MS.ammp('label_atom_id')])
|
||||
}),
|
||||
'bond-test': true
|
||||
})
|
||||
])
|
||||
]), { category: StructureSelectionCategory.Bond });
|
||||
|
||||
const nonStandardPolymer = StructureSelectionQuery('Non-standard Residues in Polymers', MS.struct.modifier.union([
|
||||
MS.struct.generator.atomGroups({
|
||||
'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
|
||||
@@ -652,6 +687,7 @@ export const StructureSelectionQueries = {
|
||||
ligandConnectedOnly,
|
||||
connectedOnly,
|
||||
disulfideBridges,
|
||||
nosBridges,
|
||||
nonStandardPolymer,
|
||||
coarse,
|
||||
ring,
|
||||
|
||||
@@ -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 David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -70,5 +70,5 @@ async function eachRepr(plugin: PluginContext, components: StructureComponentRef
|
||||
function getFilteredBundle(layers: Transparency.BundleLayer[], structure: Structure) {
|
||||
const transparency = Transparency.ofBundle(layers, structure.root);
|
||||
const merged = Transparency.merge(transparency);
|
||||
return Transparency.filter(merged, structure);
|
||||
return Transparency.filter(merged, structure) as Transparency<StructureElement.Loci>;
|
||||
}
|
||||
@@ -125,12 +125,13 @@ export namespace PluginStateObject {
|
||||
url: string | Asset.Url,
|
||||
isBinary: boolean,
|
||||
format: string,
|
||||
entryId?: string,
|
||||
entryId?: string | string[],
|
||||
isovalues: {
|
||||
type: 'absolute' | 'relative',
|
||||
value: number,
|
||||
color: Color,
|
||||
alpha?: number
|
||||
alpha?: number,
|
||||
volumeIndex?: number,
|
||||
}[]
|
||||
}
|
||||
|
||||
|
||||
@@ -455,18 +455,20 @@ const LazyVolume = PluginStateTransform.BuiltIn({
|
||||
url: PD.Url(''),
|
||||
isBinary: PD.Boolean(false),
|
||||
format: PD.Text('ccp4'), // TODO: use Select based on available formats
|
||||
entryId: PD.Text(''),
|
||||
entryId: PD.Value<string | string[]>('', { isHidden: true }),
|
||||
isovalues: PD.ObjectList({
|
||||
type: PD.Text<'absolute' | 'relative'>('relative'), // TODO: Select
|
||||
value: PD.Numeric(0),
|
||||
color: PD.Color(ColorNames.black),
|
||||
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 })
|
||||
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
volumeIndex: PD.Numeric(0),
|
||||
}, e => `${e.type} ${e.value}`)
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Lazy Volume', async ctx => {
|
||||
return new SO.Volume.Lazy(params, { label: `${params.entryId || params.url}`, description: 'Lazy Volume' });
|
||||
const entryId = Array.isArray(params.entryId) ? params.entryId.join(', ') : params.entryId;
|
||||
return new SO.Volume.Lazy(params, { label: `${entryId || params.url}`, description: 'Lazy Volume' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import { PluginStateObject as SO, PluginStateTransform } from '../objects';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { ShapeRepresentation } from '../../mol-repr/shape/representation';
|
||||
import { StructureUnitTransforms } from '../../mol-model/structure/structure/util/unit-transforms';
|
||||
import { unwindStructureAssembly, explodeStructure } from '../animation/helpers';
|
||||
import { unwindStructureAssembly, explodeStructure, spinStructure, SpinStructureParams, getSpinStructureAxisAndOrigin } from '../animation/helpers';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Overpaint } from '../../mol-theme/overpaint';
|
||||
import { Transparency } from '../../mol-theme/transparency';
|
||||
@@ -43,6 +43,7 @@ import { Box3D } from '../../mol-math/geometry';
|
||||
|
||||
export { StructureRepresentation3D };
|
||||
export { ExplodeStructureRepresentation3D };
|
||||
export { SpinStructureRepresentation3D };
|
||||
export { UnwindStructureAssemblyRepresentation3D };
|
||||
export { OverpaintStructureRepresentation3DFromScript };
|
||||
export { OverpaintStructureRepresentation3DFromBundle };
|
||||
@@ -222,7 +223,6 @@ const UnwindStructureAssemblyRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
type ExplodeStructureRepresentation3D = typeof ExplodeStructureRepresentation3D
|
||||
const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
name: 'explode-structure-representation-3d',
|
||||
@@ -259,6 +259,49 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
}
|
||||
});
|
||||
|
||||
type SpinStructureRepresentation3D = typeof SpinStructureRepresentation3D
|
||||
const SpinStructureRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
name: 'spin-structure-representation-3d',
|
||||
display: 'Spin 3D Representation',
|
||||
from: SO.Molecule.Structure.Representation3D,
|
||||
to: SO.Molecule.Structure.Representation3DState,
|
||||
params: {
|
||||
t: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
...SpinStructureParams
|
||||
}
|
||||
})({
|
||||
canAutoUpdate() {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params }) {
|
||||
const structure = a.data.sourceData;
|
||||
const unitTransforms = new StructureUnitTransforms(structure.root);
|
||||
|
||||
const { axis, origin } = getSpinStructureAxisAndOrigin(structure.root, params);
|
||||
spinStructure(structure, unitTransforms, params.t, axis, origin);
|
||||
return new SO.Molecule.Structure.Representation3DState({
|
||||
state: { unitTransforms },
|
||||
initialState: { unitTransforms: new StructureUnitTransforms(structure.root) },
|
||||
info: structure.root,
|
||||
repr: a.data.repr
|
||||
}, { label: `Spin T = ${params.t.toFixed(2)}` });
|
||||
},
|
||||
update({ a, b, newParams, oldParams }) {
|
||||
const structure = a.data.sourceData;
|
||||
if (b.data.info !== structure.root) return StateTransformer.UpdateResult.Recreate;
|
||||
if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
|
||||
|
||||
if (oldParams.t === newParams.t && oldParams.axis === newParams.axis && oldParams.origin === newParams.origin) return StateTransformer.UpdateResult.Unchanged;
|
||||
|
||||
const unitTransforms = b.data.state.unitTransforms!;
|
||||
const { axis, origin } = getSpinStructureAxisAndOrigin(structure.root, newParams);
|
||||
spinStructure(structure.root, unitTransforms, newParams.t, axis, origin);
|
||||
b.label = `Spin T = ${newParams.t.toFixed(2)}`;
|
||||
b.data.repr = a.data.repr;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
}
|
||||
});
|
||||
|
||||
type OverpaintStructureRepresentation3DFromScript = typeof OverpaintStructureRepresentation3DFromScript
|
||||
const OverpaintStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn({
|
||||
name: 'overpaint-structure-representation-3d-from-script',
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StructureComponentManager } from '../../mol-plugin-state/manager/struct
|
||||
import { StructureComponentRef, StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
|
||||
import { StructureSelectionModifier } from '../../mol-plugin-state/manager/structure/selection';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { compileResidueListSelection } from '../../mol-script/util/residue-list';
|
||||
import { compileIdListSelection } from '../../mol-script/util/id-list';
|
||||
import { memoizeLatest } from '../../mol-util/memoize';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { capitalize, stripTags } from '../../mol-util/string';
|
||||
@@ -172,7 +172,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
|
||||
// the plan is to add support to input queries in different languages
|
||||
// after this has been implemented in mol-script
|
||||
const helpers = [
|
||||
{ kind: 'residue-list' as SelectionHelperType, category: 'Helpers', label: 'Residue List', description: 'Create a selection from a list of residue ranges.' }
|
||||
{ kind: 'residue-list' as SelectionHelperType, category: 'Helpers', label: 'Atom/Residue Identifier List', description: 'Create a selection from a list of atom/residue ranges.' }
|
||||
];
|
||||
this.helpersItems = ActionMenu.createItems(helpers, {
|
||||
label: q => q.label,
|
||||
@@ -252,7 +252,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
|
||||
} else if (ActionHeader.has(this.state.action as any) && this.state.helper === 'residue-list') {
|
||||
const close = () => this.setState({ action: void 0, helper: void 0 });
|
||||
children = <div className='msp-selection-viewport-controls-actions'>
|
||||
<ControlGroup header='Residue List' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={close} topRightIcon={CloseSvg}>
|
||||
<ControlGroup header='Atom/Residue Identifier List' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={close} topRightIcon={CloseSvg}>
|
||||
<ResidueListSelectionHelper modifier={this.state.action as any} plugin={this.plugin} close={close} />
|
||||
</ControlGroup>
|
||||
</div>;
|
||||
@@ -384,8 +384,8 @@ class ApplyThemeControls extends PurePluginUIComponent<ApplyThemeControlsProps,
|
||||
}
|
||||
|
||||
const ResidueListIdTypeParams = {
|
||||
idType: ParamDefinition.Select<'auth' | 'label'>('auth', ParamDefinition.arrayToOptions(['auth', 'label'])),
|
||||
residues: ParamDefinition.Text('', { description: 'A comma separated list of residue ranges in given chain, e.g. A 10-15, B 25, C 30:i' })
|
||||
idType: ParamDefinition.Select<'auth' | 'label' | 'atom-id'>('auth', ParamDefinition.arrayToOptions(['auth', 'label', 'atom-id'])),
|
||||
identifiers: ParamDefinition.Text('', { description: 'A comma separated list of atom identifiers (e.g. 10, 15-25) or residue ranges in given chain (e.g. A 10-15, B 25, C 30:i)' })
|
||||
};
|
||||
|
||||
const DefaultResidueListIdTypeParams = ParamDefinition.getDefaultValues(ResidueListIdTypeParams);
|
||||
@@ -394,11 +394,11 @@ function ResidueListSelectionHelper({ modifier, plugin, close }: { modifier: Str
|
||||
const [state, setState] = React.useState(DefaultResidueListIdTypeParams);
|
||||
|
||||
const apply = () => {
|
||||
if (state.residues.length === 0) return;
|
||||
if (state.identifiers.trim().length === 0) return;
|
||||
|
||||
try {
|
||||
close();
|
||||
const query = compileResidueListSelection(state.residues, state.idType);
|
||||
const query = compileIdListSelection(state.identifiers, state.idType);
|
||||
plugin.managers.structure.selection.fromCompiledQuery(modifier, query, false);
|
||||
} catch (e) {
|
||||
plugin.log.error(`Failed to create selection: ${e}`);
|
||||
@@ -407,7 +407,7 @@ function ResidueListSelectionHelper({ modifier, plugin, close }: { modifier: Str
|
||||
|
||||
return <>
|
||||
<ParameterControls params={ResidueListIdTypeParams} values={state} onChangeValues={setState} onEnter={apply} />
|
||||
<Button className='msp-btn-commit msp-btn-commit-on' disabled={state.residues.length === 0} onClick={apply} style={{ marginTop: '1px' }}>
|
||||
<Button className='msp-btn-commit msp-btn-commit-on' disabled={state.identifiers.trim().length === 0} onClick={apply} style={{ marginTop: '1px' }}>
|
||||
{capitalize(modifier)} Selection
|
||||
</Button>
|
||||
</>;
|
||||
|
||||
@@ -20,6 +20,9 @@ import { ParameterControls } from '../controls/parameters';
|
||||
import { stripTags } from '../../mol-util/string';
|
||||
import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
|
||||
import { ToggleSelectionModeButton } from './selection';
|
||||
import { alignAndSuperposeWithBestDatabaseMapping } from '../../mol-model/structure/structure/util/superposition-db-mapping';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { BestDatabaseSequenceMapping } from '../../mol-model-props/sequence/best-database-mapping';
|
||||
|
||||
export class StructureSuperpositionControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
@@ -55,6 +58,7 @@ const SuperpositionTag = 'SuperpositionTransform';
|
||||
type SuperpositionControlsState = {
|
||||
isBusy: boolean,
|
||||
action?: 'byChains' | 'byAtoms' | 'options',
|
||||
canUseDb?: boolean,
|
||||
options: StructureSuperpositionOptions
|
||||
}
|
||||
|
||||
@@ -68,9 +72,10 @@ interface AtomsLociEntry extends LociEntry {
|
||||
atoms: StructureSelectionHistoryEntry[]
|
||||
};
|
||||
|
||||
export class SuperpositionControls extends PurePluginUIComponent<{}, SuperpositionControlsState> {
|
||||
export class SuperpositionControls extends PurePluginUIComponent<{ }, SuperpositionControlsState> {
|
||||
state: SuperpositionControlsState = {
|
||||
isBusy: false,
|
||||
canUseDb: false,
|
||||
action: undefined,
|
||||
options: DefaultStructureSuperpositionOptions
|
||||
}
|
||||
@@ -87,6 +92,10 @@ export class SuperpositionControls extends PurePluginUIComponent<{}, Superpositi
|
||||
this.subscribe(this.plugin.behaviors.state.isBusy, v => {
|
||||
this.setState({ isBusy: v });
|
||||
});
|
||||
|
||||
this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => {
|
||||
this.setState({ canUseDb: sel.structures.every(s => !!s.cell.obj?.data && s.cell.obj.data.models.some(m => BestDatabaseSequenceMapping.Provider.isApplicable(m)) ) });
|
||||
});
|
||||
}
|
||||
|
||||
get selection() {
|
||||
@@ -164,6 +173,25 @@ export class SuperpositionControls extends PurePluginUIComponent<{}, Superpositi
|
||||
}
|
||||
}
|
||||
|
||||
superposeDb = async () => {
|
||||
const input = this.plugin.managers.structure.hierarchy.behaviors.selection.value.structures;
|
||||
|
||||
const transforms = alignAndSuperposeWithBestDatabaseMapping(input.map(s => s.cell.obj?.data!));
|
||||
|
||||
let rmsd = 0;
|
||||
|
||||
for (const xform of transforms) {
|
||||
await this.transform(input[xform.other].cell, xform.transform.bTransform);
|
||||
rmsd += xform.transform.rmsd;
|
||||
}
|
||||
|
||||
rmsd /= transforms.length - 1;
|
||||
|
||||
this.plugin.log.info(`Superposed ${input.length} structures with avg. RMSD ${rmsd.toFixed(2)}.`);
|
||||
await new Promise(res => requestAnimationFrame(res));
|
||||
PluginCommands.Camera.Reset(this.plugin);
|
||||
};
|
||||
|
||||
toggleByChains = () => this.setState({ action: this.state.action === 'byChains' ? void 0 : 'byChains' });
|
||||
toggleByAtoms = () => this.setState({ action: this.state.action === 'byAtoms' ? void 0 : 'byAtoms' });
|
||||
toggleOptions = () => this.setState({ action: this.state.action === 'options' ? void 0 : 'options' });
|
||||
@@ -293,6 +321,14 @@ export class SuperpositionControls extends PurePluginUIComponent<{}, Superpositi
|
||||
</>;
|
||||
}
|
||||
|
||||
superposeByDbMapping() {
|
||||
return <>
|
||||
<Button icon={SuperposeChainsSvg} title='Superpose structures using database mapping.' className='msp-btn msp-btn-block' onClick={this.superposeDb} style={{ marginTop: '1px' }} disabled={this.state.isBusy}>
|
||||
DB
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
private setOptions = (values: StructureSuperpositionOptions) => {
|
||||
this.setState({ options: values });
|
||||
}
|
||||
@@ -300,8 +336,9 @@ export class SuperpositionControls extends PurePluginUIComponent<{}, Superpositi
|
||||
render() {
|
||||
return <>
|
||||
<div className='msp-flex-row'>
|
||||
<ToggleButton icon={SuperposeChainsSvg} label='By Chains' toggle={this.toggleByChains} isSelected={this.state.action === 'byChains'} disabled={this.state.isBusy} />
|
||||
<ToggleButton icon={SuperposeAtomsSvg} label='By Atoms' toggle={this.toggleByAtoms} isSelected={this.state.action === 'byAtoms'} disabled={this.state.isBusy} />
|
||||
<ToggleButton icon={SuperposeChainsSvg} label='Chains' toggle={this.toggleByChains} isSelected={this.state.action === 'byChains'} disabled={this.state.isBusy} />
|
||||
<ToggleButton icon={SuperposeAtomsSvg} label='Atoms' toggle={this.toggleByAtoms} isSelected={this.state.action === 'byAtoms'} disabled={this.state.isBusy} />
|
||||
{this.state.canUseDb && this.superposeByDbMapping()}
|
||||
<ToggleButton icon={TuneSvg} label='' title='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isBusy} style={{ flex: '0 0 40px', padding: 0 }} />
|
||||
</div>
|
||||
{this.state.action === 'byChains' && this.addByChains()}
|
||||
|
||||
@@ -205,19 +205,21 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
|
||||
try {
|
||||
const plugin = this.plugin;
|
||||
await 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 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();
|
||||
|
||||
@@ -11,5 +11,6 @@ export { AccessibleSurfaceArea } from './custom-props/computed/accessible-surfac
|
||||
export { Interactions } from './custom-props/computed/interactions';
|
||||
export { SecondaryStructure } from './custom-props/computed/secondary-structure';
|
||||
export { ValenceModel } from './custom-props/computed/valence-model';
|
||||
export { BestDatabaseSequenceMapping } from './custom-props/sequence/best-database-mapping';
|
||||
|
||||
export { CrossLinkRestraint } from './custom-props/integrative/cross-link-restraint';
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { OrderedSet } from '../../../../../mol-data/int';
|
||||
import { BestDatabaseSequenceMapping as BestDatabaseSequenceMappingProp } from '../../../../../mol-model-props/sequence/best-database-mapping';
|
||||
import { BestDatabaseSequenceMappingColorThemeProvider } from '../../../../../mol-model-props/sequence/themes/best-database-mapping';
|
||||
import { Loci } from '../../../../../mol-model/loci';
|
||||
import { StructureElement } from '../../../../../mol-model/structure';
|
||||
import { ParamDefinition as PD } from '../../../../../mol-util/param-definition';
|
||||
import { PluginBehavior } from '../../../behavior';
|
||||
|
||||
export const BestDatabaseSequenceMapping = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
|
||||
name: 'best-sequence-database-mapping-prop',
|
||||
category: 'custom-props',
|
||||
display: { name: 'Best Database Sequence Mapping' },
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
|
||||
private provider = BestDatabaseSequenceMappingProp.Provider
|
||||
|
||||
private labelProvider = {
|
||||
label: (loci: Loci): string | undefined => {
|
||||
if (!this.params.showTooltip) return;
|
||||
return bestDatabaseSequenceMappingLabel(loci);
|
||||
}
|
||||
}
|
||||
|
||||
update(p: { autoAttach: boolean, showTooltip: boolean }) {
|
||||
let updated = (
|
||||
this.params.autoAttach !== p.autoAttach ||
|
||||
this.params.showTooltip !== p.showTooltip
|
||||
);
|
||||
this.params.autoAttach = p.autoAttach;
|
||||
this.params.showTooltip = p.showTooltip;
|
||||
this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
|
||||
return updated;
|
||||
}
|
||||
|
||||
register(): void {
|
||||
this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.add(BestDatabaseSequenceMappingColorThemeProvider);
|
||||
this.ctx.managers.lociLabels.addProvider(this.labelProvider);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customModelProperties.unregister(this.provider.descriptor.name);
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.remove(BestDatabaseSequenceMappingColorThemeProvider);
|
||||
this.ctx.managers.lociLabels.removeProvider(this.labelProvider);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(true),
|
||||
showTooltip: PD.Boolean(true)
|
||||
})
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
function bestDatabaseSequenceMappingLabel(loci: Loci): string | undefined {
|
||||
if(loci.kind === 'element-loci') {
|
||||
if (loci.elements.length === 0) return;
|
||||
|
||||
const e = loci.elements[0];
|
||||
const u = e.unit;
|
||||
const se = StructureElement.Location.create(loci.structure, u, u.elements[OrderedSet.getAt(e.indices, 0)]);
|
||||
return BestDatabaseSequenceMappingProp.getLabel(se);
|
||||
}
|
||||
}
|
||||
@@ -276,10 +276,12 @@ const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
|
||||
// TODO: is this correct behavior?
|
||||
if (!channel) return StateTransformer.UpdateResult.Unchanged;
|
||||
|
||||
const visible = b.data.repr.state.visible;
|
||||
const params = createVolumeProps(a.data, newParams.channel);
|
||||
const props = { ...b.data.repr.props, ...params.type.params };
|
||||
b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: channel.data }, params));
|
||||
await b.data.repr.createOrUpdate(props, channel.data).runInContext(ctx);
|
||||
b.data.repr.setState({ visible });
|
||||
b.data.sourceData = channel.data;
|
||||
|
||||
// TODO: set the transform here as well in case the structure moves?
|
||||
|
||||
@@ -22,6 +22,7 @@ import { AssignColorVolume } from '../mol-plugin-state/actions/volume';
|
||||
import { StateTransforms } from '../mol-plugin-state/transforms';
|
||||
import { BoxifyVolumeStreaming, CreateVolumeStreamingBehavior, InitVolumeStreaming } from '../mol-plugin/behavior/dynamic/volume-streaming/transformers';
|
||||
import { AnimateStateInterpolation } from '../mol-plugin-state/animation/built-in/state-interpolation';
|
||||
import { AnimateStructureSpin } from '../mol-plugin-state/animation/built-in/spin-structure';
|
||||
|
||||
export { PluginSpec };
|
||||
|
||||
@@ -96,6 +97,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
|
||||
PluginSpec.Action(StateTransforms.Representation.ModelUnitcell3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.StructureBoundingBox3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.SpinStructureRepresentation3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),
|
||||
PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3DFromScript),
|
||||
PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3DFromScript),
|
||||
@@ -118,6 +120,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
|
||||
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.AccessibleSurfaceArea),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.BestDatabaseSequenceMapping),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.ValenceModel),
|
||||
@@ -128,6 +131,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
|
||||
AnimateCameraSpin,
|
||||
AnimateStateSnapshots,
|
||||
AnimateAssemblyUnwind,
|
||||
AnimateStructureSpin,
|
||||
AnimateStateInterpolation
|
||||
]
|
||||
});
|
||||
@@ -108,6 +108,7 @@ class ViewportScreenshotHelper extends PluginComponent {
|
||||
private createPass(mutlisample: boolean) {
|
||||
const c = this.plugin.canvas3d!;
|
||||
const { colorBufferFloat, textureFloat } = c.webgl.extensions;
|
||||
const aoProps = this.plugin.canvas3d!.props.postprocessing.occlusion;
|
||||
return this.plugin.canvas3d!.getImagePass({
|
||||
transparentBackground: this.values.transparent,
|
||||
cameraHelper: { axes: this.values.axes },
|
||||
@@ -117,7 +118,9 @@ class ViewportScreenshotHelper extends PluginComponent {
|
||||
},
|
||||
postprocessing: {
|
||||
...c.props.postprocessing,
|
||||
antialiasing: { name: 'off', params: {} }
|
||||
occlusion: aoProps.name === 'on'
|
||||
? { name: 'on', params: { ...aoProps.params, samples: 128 } }
|
||||
: aoProps
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -130,13 +133,16 @@ class ViewportScreenshotHelper extends PluginComponent {
|
||||
private _imagePass: ImagePass;
|
||||
get imagePass() {
|
||||
if (this._imagePass) {
|
||||
const aoProps = this.plugin.canvas3d!.props.postprocessing.occlusion;
|
||||
this._imagePass.setProps({
|
||||
cameraHelper: { axes: this.values.axes },
|
||||
transparentBackground: this.values.transparent,
|
||||
// TODO: optimize because this creates a copy of a large object!
|
||||
postprocessing: {
|
||||
...this.plugin.canvas3d!.props.postprocessing,
|
||||
antialiasing: { name: 'off', params: {} }
|
||||
occlusion: aoProps.name === 'on'
|
||||
? { name: 'on', params: { ...aoProps.params, samples: 128 } }
|
||||
: aoProps
|
||||
}
|
||||
});
|
||||
return this._imagePass;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user