mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a3eba66d | ||
|
|
41b8584fb7 | ||
|
|
523b17dfde | ||
|
|
c47b4d6078 | ||
|
|
b94073b96f | ||
|
|
905eb3ec2f | ||
|
|
3ae72e5c60 | ||
|
|
055dfd4946 | ||
|
|
2601d2ba63 | ||
|
|
340806d774 | ||
|
|
18ad848de2 | ||
|
|
9de8334af5 | ||
|
|
57580a5e6b | ||
|
|
7da4a85459 | ||
|
|
b7c380fd90 | ||
|
|
bcd304d058 | ||
|
|
fd50a8f8e0 | ||
|
|
27f251e8e4 | ||
|
|
8d2a44983e | ||
|
|
f806ac1444 | ||
|
|
63a585d88a | ||
|
|
a4b5a16fcd | ||
|
|
86bf859a63 | ||
|
|
1b8117d3f1 | ||
|
|
400e2bbc45 | ||
|
|
e2e26c7e9c | ||
|
|
5ca9020cbf | ||
|
|
ea4c411d5c | ||
|
|
ba7e3fe827 | ||
|
|
8f20571a17 | ||
|
|
c25a4247e6 | ||
|
|
1071d3d8ba | ||
|
|
e8dc046570 | ||
|
|
27f9c2aa67 | ||
|
|
a4962231c8 | ||
|
|
8833f29ce5 | ||
|
|
40b6038380 | ||
|
|
59e16e0187 | ||
|
|
ca5a50bd53 | ||
|
|
bccf54fabe | ||
|
|
57a790544c | ||
|
|
df0669598c | ||
|
|
fb912036af | ||
|
|
9efb5cd126 | ||
|
|
08a56ad6ab | ||
|
|
2c2bd6adda | ||
|
|
b010298acb | ||
|
|
7033a1e0b2 | ||
|
|
8ad617acdf | ||
|
|
31ab6aa93e | ||
|
|
0a2dbe14d7 | ||
|
|
89d305aaa1 | ||
|
|
dbb6b90fbc | ||
|
|
c57150f09f | ||
|
|
0b30c7344b | ||
|
|
d7ad5a6e9f | ||
|
|
86a74d1cc2 | ||
|
|
3f0f24cb99 | ||
|
|
b8ddc142ea | ||
|
|
cccaa48589 | ||
|
|
3ad355ad40 | ||
|
|
918186eb24 | ||
|
|
db4742cebf | ||
|
|
19fec3bbc1 | ||
|
|
7d6c77b3bd | ||
|
|
dfcc4e400d | ||
|
|
c9734d83a2 | ||
|
|
93943cc27b | ||
|
|
25836b2de0 | ||
|
|
c6874c922d | ||
|
|
0937c84f47 | ||
|
|
6a7f892d60 | ||
|
|
b4cd2d0a11 | ||
|
|
2067f02830 | ||
|
|
6d86ada6b4 | ||
|
|
f656cf09b7 | ||
|
|
a891b4c551 | ||
|
|
ded844c936 | ||
|
|
44b36637fd | ||
|
|
f590bd0f0a | ||
|
|
9474c80673 | ||
|
|
7b48d691c8 | ||
|
|
b03146852f | ||
|
|
9345f3584a | ||
|
|
4d058aa1a8 | ||
|
|
e7da6092aa | ||
|
|
94f6b864b0 | ||
|
|
6e90447511 | ||
|
|
b91030c4bd | ||
|
|
31819dbf16 | ||
|
|
1665dd7d00 | ||
|
|
9716fecdb9 | ||
|
|
684fd2d237 | ||
|
|
9432b9a7a7 | ||
|
|
3a37c95c17 | ||
|
|
6040b99c19 | ||
|
|
83bef0f0e7 | ||
|
|
95bb3a1f81 | ||
|
|
be677f47cb | ||
|
|
43bf69d09c | ||
|
|
b6cc626431 | ||
|
|
931fdfca9b | ||
|
|
1c10db5656 | ||
|
|
c4ccd8758f | ||
|
|
6c99c575bc | ||
|
|
ae2493b6e3 | ||
|
|
bcd50c294f | ||
|
|
9c0024dbab | ||
|
|
c15b3603c0 | ||
|
|
70647ba972 | ||
|
|
8d19357845 | ||
|
|
8e9817c4d1 | ||
|
|
b16147b88c | ||
|
|
9840d8f816 | ||
|
|
d892ccab4c | ||
|
|
65f88b3293 | ||
|
|
9e6e5eb795 | ||
|
|
2f755efeec | ||
|
|
012e616ec4 | ||
|
|
007d0e7608 | ||
|
|
bf313073b9 | ||
|
|
293928f3de | ||
|
|
2404f398b6 | ||
|
|
43ff6e24c8 | ||
|
|
9e62112366 | ||
|
|
026d6fc618 | ||
|
|
95fcd942dc | ||
|
|
805481db14 | ||
|
|
39175df025 | ||
|
|
cd0f451f6b | ||
|
|
fe1aa1a9bf | ||
|
|
fcfb6e6d5a | ||
|
|
c548c94575 | ||
|
|
2d45f4a77c | ||
|
|
a5ae887842 | ||
|
|
e4b53cdc6a | ||
|
|
c53940e67e | ||
|
|
6d61745f0f | ||
|
|
46d86d93b0 | ||
|
|
11772b64fb | ||
|
|
dbc8ab00c6 | ||
|
|
015fad4371 | ||
|
|
71a484586f | ||
|
|
f0b06ee746 | ||
|
|
b0694b886b | ||
|
|
eaf47b3169 | ||
|
|
ad9046fcf2 | ||
|
|
eabe4d46bc | ||
|
|
003c5f8fb7 | ||
|
|
68748a4a94 | ||
|
|
9bd6b8195d | ||
|
|
05848b651c | ||
|
|
0a8f87dd9f | ||
|
|
925aaa701d | ||
|
|
5be599bad4 | ||
|
|
e22ce53e65 | ||
|
|
4c49431027 | ||
|
|
4192d82ef3 | ||
|
|
ce220737f2 | ||
|
|
eeb7cd2c52 | ||
|
|
748111beb2 | ||
|
|
1f7d41c653 | ||
|
|
b9430ff387 | ||
|
|
6591bab035 | ||
|
|
4da446aec2 | ||
|
|
25c170e36d | ||
|
|
eba18d1dce | ||
|
|
2c87d01a5e | ||
|
|
e41a2baa32 | ||
|
|
c297017749 | ||
|
|
9a0fc1faa6 | ||
|
|
424513f23c | ||
|
|
895d672589 | ||
|
|
0c6253ed16 | ||
|
|
da97cd20aa | ||
|
|
ca6d73e048 | ||
|
|
88b79deefa | ||
|
|
d756e2e195 | ||
|
|
2ce126a8f5 | ||
|
|
01e95dada0 | ||
|
|
1c024f0943 | ||
|
|
5901e3d6a1 | ||
|
|
0cfe1cec66 | ||
|
|
c1930e4142 | ||
|
|
71375d908f | ||
|
|
728b87d4e4 | ||
|
|
9c17698a8a | ||
|
|
625381c446 | ||
|
|
da949a245e | ||
|
|
7000bdd15d | ||
|
|
adcf6a6fa8 | ||
|
|
b70af9f178 | ||
|
|
e5bdcfd781 | ||
|
|
6049705224 | ||
|
|
273d50d403 | ||
|
|
333ea724d6 | ||
|
|
e96dca91ef | ||
|
|
41a0048f64 | ||
|
|
5e97b05bd2 | ||
|
|
ebc6b2acce | ||
|
|
8372408d9c | ||
|
|
2c6822f5ab | ||
|
|
7efbf46e7a | ||
|
|
b6d6a518d3 | ||
|
|
2d690268f9 | ||
|
|
e0c794b557 | ||
|
|
f91f445631 | ||
|
|
1cc367c8d8 | ||
|
|
8c6969206d | ||
|
|
c0479e3d46 | ||
|
|
22e92b38c6 | ||
|
|
5741709023 | ||
|
|
2265fc02cc | ||
|
|
64180bef36 | ||
|
|
be3caef6e9 | ||
|
|
71a2f71866 | ||
|
|
3c6152054e | ||
|
|
080d649bf9 | ||
|
|
2852b09c77 | ||
|
|
5e53467541 | ||
|
|
42dc579ddb | ||
|
|
890c758585 | ||
|
|
e6c77069df | ||
|
|
e7ecf98f13 | ||
|
|
70ad32f62d | ||
|
|
69fe452055 | ||
|
|
9edeb84f4e | ||
|
|
e1db3114c8 | ||
|
|
8724badcb6 | ||
|
|
d413f74526 | ||
|
|
6752108c5f | ||
|
|
9302fdadb9 | ||
|
|
f7048c7535 | ||
|
|
3252a3f0f3 | ||
|
|
6805194d48 | ||
|
|
acf0dceb47 | ||
|
|
c53f500da6 | ||
|
|
defc04278e | ||
|
|
aa4d5e78a7 | ||
|
|
df3a432afd | ||
|
|
1b339d18cc | ||
|
|
c4650c91a8 | ||
|
|
e3c4f19563 | ||
|
|
85780a5d6a | ||
|
|
aab70e2ff0 | ||
|
|
3d95ed729c |
14
.git-blame-ignore-revs
Normal file
14
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,14 @@
|
||||
# added semicolons to linting rules
|
||||
fb0634a0f4aab3764b7e6368e38d8dea7615e591
|
||||
|
||||
# new linting rules (no default exports, no named tuples)
|
||||
6c5224f33e9de20fe9967a82536c269bacf29738
|
||||
|
||||
# lint: add space-in-parens rule
|
||||
1d21787e7ea1971817813c008351541e4640c261
|
||||
|
||||
# lint: add object-curly-spacing rule
|
||||
b31302ba3ad4ab7f98aedd500b762be642374ff0
|
||||
|
||||
# fix eslint warnings
|
||||
3b1513adc0048dc4879f1d70874b3e56aaffd10e
|
||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -4,6 +4,107 @@ 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]
|
||||
- Fix exported image artifacts on transparent background with emissive, bloom, or antialiasing
|
||||
- Fix cel-shaded ambient color being stripped to luminance (now uses full RGB, matching the classic lighting path)
|
||||
- Fix empty transforms default in `ShapeFromPly`
|
||||
- Use morton order for spheres in dot visual with lod-levels
|
||||
- Add `Camera.changed` event and rotation/translation setter/getter
|
||||
- Add `instanceGranularity: 'auto'` as a memory guard
|
||||
- Honor `instanceGranularity` in `Visual.getLoci`
|
||||
- Add mesoscale representation preset
|
||||
- Add presets option to `ObjectList` param definition
|
||||
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
|
||||
- Fix bugs in ModelServer surroundingLigands endpoint, resulting in omitWater not honored
|
||||
- Fix `Volume` and `Isosurface` getBoundingSphere ignoring instances
|
||||
- Fix aromatic ring detection not accounting for hybridization
|
||||
- Add axis param to camera spin/rock animation
|
||||
- Fix SSAO half/quarter resolution textures for multi-scale
|
||||
- Non-covalent interactions: water bridge support
|
||||
|
||||
## [v5.9.0] - 2026-05-03
|
||||
- Fix edge case when `PluginSpec.animations` is empty
|
||||
- Add 8K UHD option to `ViewportScreenshotHelper`
|
||||
- Handle MRC files with empty length header fields
|
||||
- Handle CCD bonds with Deuterium atoms
|
||||
- [Breaking] ComponentBond.Entry.map now returns ComponentBond.Pairs
|
||||
- Fix volume slice marking performance regression
|
||||
- Add GPU procedural animation (wiggle & tumble)
|
||||
- Per-vertex wiggle via fbm noise (position & group mode)
|
||||
- Per-instance tumble via fbm noise (rotation + translation)
|
||||
- `Wiggle` theme layer for data-driven per-group wiggle
|
||||
- `enableAnimation` Canvas3D param for global toggle
|
||||
- Add `AnimateTime` built-in for, e.g., exporting procedural animation
|
||||
- Add Procedural Animation panels
|
||||
- Viewer: structure dynamics & uncertainty
|
||||
- Mesoscale Explorer: entity dynamics
|
||||
- Fix `GraphQLClient` missing required headers
|
||||
- [Breaking] Use Record instead of Array for headers (assets & data-source utils)
|
||||
|
||||
## [v5.8.0] - 2026-04-03
|
||||
- Dependencies: remove `utils.promisify`, `node-fetch` (#1797)
|
||||
- Fix circular dependency which causes crash in bundlers (#1791)
|
||||
- Add `putty` as a mol-view-spec representation.
|
||||
- Fix detecting sidechain-only structures as coarse-grained (#1420)
|
||||
- Fix clip-object transform due to missing axis normalization
|
||||
- Sequence alignment: Fix return type & improve scoring for unknown residues
|
||||
- Use PDB SEQRES block to show unresolved residues in Sequence toolbar
|
||||
- Canvas3D debug-helpers
|
||||
- [Breaking] Move helpers to an extension as a PluginBehavior (params are no longer part of Canvas3D)
|
||||
- Add helpers for clip-object, direct-volume, image, mesh
|
||||
- Fix StructureComponent node update throwing error when substructure empty
|
||||
- CSS: Avoid tooltip box flickering when hovering something under it
|
||||
- Volume slice visual
|
||||
- Fix support for volume instances
|
||||
- Fix plane mode: ensure normalized & correctly oriented
|
||||
- MolViewSpec
|
||||
- Add `VolumeStreamingExtension` (`molstar_volume_streaming` custom property)
|
||||
- Fix focusing empty selections
|
||||
- Avoid re-calculating static model properties for trajectories
|
||||
|
||||
## [v5.7.0] - 2026-02-28
|
||||
- Text label improvements
|
||||
- Improve label background vertical centering
|
||||
- Handle label depth variant for correct transparent background
|
||||
- Draw border under text using fragment depth to prevent overlap on adjacent characters
|
||||
- Clamp border width to avoid exceeding SDF range
|
||||
- Increase font atlas quality (2x font size multiplier)
|
||||
- TM-align performance improvements (#1745)
|
||||
- Disable transparent outline close to opaque elements
|
||||
- Add axis param to trackball spin & rock animation
|
||||
- Color smoothing fixes (#1747)
|
||||
- Use correct instance for non instance-type
|
||||
- Never transform for non instance-type
|
||||
- Add extra radius to gaussian surface boundingsphere
|
||||
- MolViewSpec
|
||||
- Add `MVSData.toMVSX` function and `mvs-mvsj-to-mvsx.js` CLI utility
|
||||
- [Breaking] Add PQR file format support (#157)
|
||||
- Replace `isPdbqt` with `variant` param in `TrajectoryFromPDB`
|
||||
- Add `CustomVolumeProperty` (like for models and structures)
|
||||
- Geometry export
|
||||
- Fix missing `usePalette` support
|
||||
- Fix vertex-based coloring for non-mesh geometries
|
||||
- Support line-strips
|
||||
- Support vertex-based sizing
|
||||
- Support memory efficient line-strips in Lines geometry,
|
||||
- Add `StripLinesBuilder`
|
||||
- Add `computeFrenetFrames` helper
|
||||
- Streamlines support
|
||||
- Add basic calculation method
|
||||
- Add custom-volume-property
|
||||
- Add representation with lines and tube-mesh visuals
|
||||
- Fix `TextCtrl` always moving cursor to end position
|
||||
- Add `vertex` and `vertexInstance` granularity support for size themes
|
||||
- Add `transform` and `domain` parameters to volume-value size theme
|
||||
- Fix parsing of single charge type_symbols (e.g., N+) in cif-core
|
||||
- Detect metal-coordination when parsing pdb
|
||||
- Handle additional elements in `guessElementSymbol*` (As, Li, Ga)
|
||||
- Add more element-pair thresholds for bonding (Ag-S, CoSb, Ga-F)
|
||||
- Add `metalCoordination` style param (dashed, solid) for bonds
|
||||
- Fix `unitSymmetryGroups` for representations with `includeParent` enabled
|
||||
- Add `convexHull` helper
|
||||
- Add `Structure.coordination` sites
|
||||
- Add `Polyhedron` representation showing coordination sites
|
||||
- Guard against `xr-spatial-tracking` blocked in `Permissions-Policy`
|
||||
|
||||
## [v5.6.1] - 2026-01-23
|
||||
- Disable occlusion culling in `ImagePass` (#1758)
|
||||
|
||||
@@ -126,16 +126,16 @@ and navigate to `build/viewer`
|
||||
|
||||
**Ion names**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
|
||||
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
|
||||
|
||||
**Saccharide names**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
|
||||
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
|
||||
|
||||
### Other scripts
|
||||
**Create chem comp bond table**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
|
||||
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
|
||||
|
||||
**Test model server**
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ chemical.melting_point
|
||||
|
||||
chemical_formula.moiety
|
||||
chemical_formula.sum
|
||||
chemical_formula.iupac
|
||||
chemical_formula.weight
|
||||
|
||||
atom_type.symbol
|
||||
@@ -25,6 +26,8 @@ atom_type_scat.source
|
||||
|
||||
space_group.crystal_system
|
||||
space_group.name_h-m_full
|
||||
space_group.name_h-m_alt
|
||||
space_group.name_hall
|
||||
space_group.it_number
|
||||
space_group_symop.operation_xyz
|
||||
|
||||
|
||||
|
@@ -48,4 +48,7 @@
|
||||
* CLR (e.g. 3GKI) - four fused rings
|
||||
* Assembly symmetries
|
||||
* 5M30 (Assembly 1, C3 local and pseudo)
|
||||
* 1RB8 (Assembly 1, I global)
|
||||
* 1RB8 (Assembly 1, I global)
|
||||
* Deuterium atoms
|
||||
* 3CWH (XUL with D and DOD)
|
||||
* 8TT8 (HOH and other with D)
|
||||
23308
package-lock.json
generated
23308
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.6.1",
|
||||
"version": "5.9.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"url": "https://github.com/molstar/molstar/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
@@ -74,7 +74,7 @@
|
||||
"js"
|
||||
],
|
||||
"transform": {
|
||||
"\\.ts$": "esbuild-jest-transform"
|
||||
"\\.ts$": ["esbuild-jest-transform", { "tsconfigRaw": "{\"compilerOptions\":{\"useDefineForClassFields\":false}}" }]
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
@@ -124,7 +124,8 @@
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>",
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>",
|
||||
"Diego del Alamo <diego.delalamo@gmail.com>"
|
||||
"Diego del Alamo <diego.delalamo@gmail.com>",
|
||||
"Tianzhen Lin (Tangent) <tangent@usa.net>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -132,53 +133,51 @@
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.27",
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/webxr": "^0.5.24",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||
"@typescript-eslint/parser": "^8.53.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"cpx2": "^8.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"esbuild": "^0.27.2",
|
||||
"cpx2": "^8.0.2",
|
||||
"css-loader": "^7.1.4",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild-jest-transform": "^2.0.1",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"esbuild-sass-plugin": "^3.7.0",
|
||||
"eslint": "^10.3.0",
|
||||
"fs-extra": "^11.3.4",
|
||||
"globals": "^17.6.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest": "^30.3.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.97.2",
|
||||
"simple-git": "^3.30.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3"
|
||||
"sass": "^1.99.0",
|
||||
"simple-git": "^3.36.0",
|
||||
"tsc-alias": "^1.8.17",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^20.19.30",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/node": "^22.19.17",
|
||||
"@types/swagger-ui-dist": "3.30.6",
|
||||
"argparse": "^2.0.1",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immutable": "^5.1.4",
|
||||
"immutable": "^5.1.5",
|
||||
"io-ts": "^2.2.22",
|
||||
"mutative": "^1.3.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.31.0",
|
||||
"tslib": "^2.8.1",
|
||||
"util.promisify": "^1.1.3"
|
||||
"swagger-ui-dist": "^5.32.5",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
@@ -55,7 +55,7 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
|
||||
sizeTheme: {
|
||||
name: 'physical',
|
||||
params: {
|
||||
value: 1,
|
||||
scale: 1,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
|
||||
import { Model } from '../../../../mol-model/structure/model/model';
|
||||
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
|
||||
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
@@ -59,7 +60,7 @@ function getSpacefillParams(color: Color, scaleFactor: number, graphics: Graphic
|
||||
sizeTheme: {
|
||||
name: 'physical',
|
||||
params: {
|
||||
value: 1,
|
||||
scale: scaleFactor,
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -102,6 +103,8 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
});
|
||||
}
|
||||
|
||||
const coarseGrained = Model.isCoarseGrained(model.data!);
|
||||
|
||||
const entGroups = new Map<string, StateObjectSelector>();
|
||||
const entIds = new Map<string, { idx: number, members: Map<number, number> }>();
|
||||
const entColors = new Map<string, Color[]>();
|
||||
@@ -170,7 +173,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
for (let i = 0; i < entities._rowCount; i++) {
|
||||
const t = getEntityType(i);
|
||||
const color = entColors.get(t)![entIds.get(t)!.members.get(i)!];
|
||||
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || 1;
|
||||
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || (coarseGrained ? 2 : 1);
|
||||
|
||||
build = build
|
||||
.toRoot()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -10,6 +10,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
|
||||
import { getAnimationParam } from '../../../mol-geo/geometry/animation';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
@@ -21,7 +22,6 @@ import { Hcl } from '../../../mol-util/color/spaces/hcl';
|
||||
import { StateObjectCell, StateObjectRef, StateSelection } from '../../../mol-state';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
|
||||
import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/representation/spacefill';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { saturate } from '../../../mol-math/interpolate';
|
||||
import { Material } from '../../../mol-util/material';
|
||||
@@ -174,6 +174,8 @@ export const LodParams = {
|
||||
approximate: Spheres.Params.approximate,
|
||||
};
|
||||
|
||||
export const AnimationParams = getAnimationParam().params;
|
||||
|
||||
export const SimpleClipParams = {
|
||||
type: PD.Select('none', PD.objectToOptions(Clip.Type, t => stringToWords(t))),
|
||||
invert: PD.Boolean(false),
|
||||
@@ -281,6 +283,7 @@ export const MesoscaleGroupParams = {
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
lod: PD.Group(LodParams),
|
||||
clip: PD.Group(SimpleClipParams),
|
||||
animation: PD.Group(AnimationParams),
|
||||
};
|
||||
export type MesoscaleGroupProps = PD.Values<typeof MesoscaleGroupParams>;
|
||||
|
||||
@@ -318,38 +321,7 @@ export function getMesoscaleGroupParams(graphicsMode: GraphicsMode): MesoscaleGr
|
||||
export type LodLevels = typeof SpacefillRepresentationProvider.defaultValues['lodLevels']
|
||||
|
||||
export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): LodLevels {
|
||||
switch (graphicsMode) {
|
||||
case 'performance':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
|
||||
];
|
||||
case 'balanced':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
|
||||
];
|
||||
case 'quality':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
|
||||
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
|
||||
];
|
||||
case 'ultra':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
];
|
||||
default:
|
||||
assertUnreachable(graphicsMode);
|
||||
}
|
||||
return Spheres.LodLevelsPresets[graphicsMode];
|
||||
}
|
||||
|
||||
export type GraphicsMode = 'ultra' | 'quality' | 'balanced' | 'performance' | 'custom';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,7 +18,7 @@ import { CombinedColorControl } from '../../../mol-plugin-ui/controls/color';
|
||||
import { MarkerAction } from '../../../mol-util/marker-action';
|
||||
import { EveryLoci, Loci } from '../../../mol-model/loci';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, AnimationParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
|
||||
import React, { useState } from 'react';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { StructureElement } from '../../../mol-model/structure/structure/element';
|
||||
@@ -828,6 +828,26 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
update.commit();
|
||||
};
|
||||
|
||||
updateAnimation = (values: PD.Values) => {
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
for (const r of this.allFilteredEntities) {
|
||||
update.to(r).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = values;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const g of this.allGroups) {
|
||||
update.to(g).update(old => {
|
||||
old.animation = values;
|
||||
});
|
||||
}
|
||||
|
||||
update.commit();
|
||||
};
|
||||
|
||||
update = (props: MesoscaleGroupProps) => {
|
||||
this.plugin.state.data.build().to(this.ref).update(props);
|
||||
};
|
||||
@@ -865,6 +885,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
const rootValue = this.cell.params?.values.color;
|
||||
const clipValue = this.cell.params?.values.clip;
|
||||
const lodValue = this.cell.params?.values.lod;
|
||||
const animationValue = this.cell.params?.values.animation;
|
||||
const isRoot = this.cell.params?.values.root;
|
||||
|
||||
const groups = this.groups;
|
||||
@@ -904,6 +925,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
|
||||
<ParameterControls params={SimpleClipParams} values={clipValue} onChangeValues={this.updateClip} />
|
||||
<ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />
|
||||
<ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />
|
||||
</ControlGroup>
|
||||
</div>}
|
||||
{this.state.action === 'root' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
|
||||
@@ -1080,6 +1102,19 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
};
|
||||
}
|
||||
|
||||
get animationValue(): PD.Values<typeof AnimationParams> | undefined {
|
||||
const p = this.cell.transform.params?.type?.params?.animation;
|
||||
if (!p) return;
|
||||
return {
|
||||
wiggleMode: p.wiggleMode,
|
||||
wiggleSpeed: p.wiggleSpeed,
|
||||
wiggleAmplitude: p.wiggleAmplitude,
|
||||
wiggleFrequency: p.wiggleFrequency,
|
||||
tumbleSpeed: p.tumbleSpeed,
|
||||
tumbleAmplitude: p.tumbleAmplitude,
|
||||
};
|
||||
}
|
||||
|
||||
get patternValue(): { amplitude: number, frequency: number } | undefined {
|
||||
const p = this.cell.transform.params;
|
||||
if (p.type) return;
|
||||
@@ -1194,6 +1229,15 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
}
|
||||
};
|
||||
|
||||
updateAnimation = (values: PD.Values) => {
|
||||
const params = this.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
|
||||
if (!params.type) return;
|
||||
|
||||
this.plugin.build().to(this.ref).update(old => {
|
||||
old.type.params.animation = values;
|
||||
}).commit();
|
||||
};
|
||||
|
||||
updatePattern = (values: PD.Values) => {
|
||||
return this.plugin.build().to(this.ref).update(old => {
|
||||
if (!old.type) {
|
||||
@@ -1213,6 +1257,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
const opacityValue = this.opacityValue;
|
||||
const emissiveValue = this.emissiveValue;
|
||||
const lodValue = this.lodValue;
|
||||
const animationValue = this.animationValue;
|
||||
const patternValue = this.patternValue;
|
||||
|
||||
const l = getEntityLabel(this.plugin, this.cell);
|
||||
@@ -1251,6 +1296,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
|
||||
<ParameterMappingControl mapping={this.clipMapping} />
|
||||
{lodValue && <ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />}
|
||||
{animationValue && <ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />}
|
||||
</ControlGroup>
|
||||
</div>}
|
||||
</>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -13,7 +13,7 @@ import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { MesoscaleState } from '../data/state';
|
||||
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, MesoProceduralAnimationControls, ExplorerInfo } from './states';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { RendererParams } from '../../../mol-gl/renderer';
|
||||
@@ -145,6 +145,7 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
<StructureMeasurementsControls initiallyCollapsed={true}/>
|
||||
</>
|
||||
<MesoQuickStylesControls />
|
||||
<MesoProceduralAnimationControls />
|
||||
<Spacer />
|
||||
<SectionHeader title='Entities' />
|
||||
<EntityControls />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -8,7 +8,7 @@ import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { AnimationSvg, GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { CollapsableControls, PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
|
||||
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
|
||||
@@ -24,7 +24,7 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
|
||||
import { createGenericHierarchy } from '../data/generic/preset';
|
||||
import { createMmcifHierarchy } from '../data/mmcif/preset';
|
||||
import { createPetworldHierarchy } from '../data/petworld/preset';
|
||||
import { getAllEntities, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { getAllEntities, getAllGroups, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { now } from '../../../mol-util/now';
|
||||
import { readFromFile } from '../../../mol-util/data-source';
|
||||
@@ -779,3 +779,110 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export class MesoProceduralAnimationControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
header: 'Procedural Animation',
|
||||
brand: { accent: 'gray' as const, svg: AnimationSvg }
|
||||
};
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
return <>
|
||||
<MesoProceduralAnimation />
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
class MesoProceduralAnimation extends PluginUIComponent {
|
||||
private isMembrane(cell: { transform: { tags?: string[] } }) {
|
||||
return cell.transform.tags?.some(t => t.includes('mem')) ?? false;
|
||||
}
|
||||
|
||||
async dynamics() {
|
||||
const update = this.plugin.state.data.build();
|
||||
const entities = getAllEntities(this.plugin);
|
||||
const groups = getAllGroups(this.plugin);
|
||||
|
||||
for (const entity of entities) {
|
||||
const membrane = this.isMembrane(entity);
|
||||
update.to(entity).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = {
|
||||
...old.type.params.animation,
|
||||
wiggleMode: 'position',
|
||||
wiggleSpeed: 7,
|
||||
wiggleAmplitude: 1,
|
||||
wiggleFrequency: 0.2,
|
||||
tumbleSpeed: 1,
|
||||
tumbleAmplitude: membrane ? 0 : 4,
|
||||
tumbleFrequency: 0.2,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
const membrane = this.isMembrane(group);
|
||||
update.to(group).update(old => {
|
||||
old.animation = {
|
||||
...old.animation,
|
||||
wiggleMode: 'position',
|
||||
wiggleSpeed: 7,
|
||||
wiggleAmplitude: 1,
|
||||
wiggleFrequency: 0.2,
|
||||
tumbleSpeed: 1,
|
||||
tumbleAmplitude: membrane ? 0 : 4,
|
||||
tumbleFrequency: 0.2,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const update = this.plugin.state.data.build();
|
||||
const entities = getAllEntities(this.plugin);
|
||||
const groups = getAllGroups(this.plugin);
|
||||
|
||||
for (const entity of entities) {
|
||||
update.to(entity).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = {
|
||||
...old.type.params.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
update.to(group).update(old => {
|
||||
old.animation = {
|
||||
...old.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<div className='msp-flex-row'>
|
||||
<Button noOverflow title='Enable wiggle for all entities and tumble for non-membrane entities' onClick={() => this.dynamics()} style={{ width: 'auto' }}>
|
||||
Dynamics
|
||||
</Button>
|
||||
<Button noOverflow title='Set wiggle and tumble amplitude to zero for all entities' onClick={() => this.clear()} style={{ width: 'auto' }}>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 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>
|
||||
@@ -9,6 +9,7 @@
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { AssemblySymmetry } from '../../extensions/assembly-symmetry';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { DebugHelpers } from '../../extensions/debug-helpers';
|
||||
import { DnatcoNtCs } from '../../extensions/dnatco';
|
||||
import { G3DFormat } from '../../extensions/g3d/format';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
@@ -32,6 +33,7 @@ export const ExtensionMap = {
|
||||
// Mol* built-in extensions
|
||||
'mvs': PluginSpec.Behavior(MolViewSpec),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'debug-helpers': PluginSpec.Behavior(DebugHelpers),
|
||||
'model-export': PluginSpec.Behavior(ModelExport),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export),
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Josh McMenemy <josh.mcmenemy@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as path from 'path';
|
||||
import util from 'util';
|
||||
import fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -32,7 +31,7 @@ function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
|
||||
function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated ion names params file. Names extracted from CCD components.
|
||||
*
|
||||
@@ -41,7 +40,7 @@ function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
|
||||
export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
writeFileAsync(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as path from 'path';
|
||||
import util from 'util';
|
||||
import fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -44,7 +43,7 @@ function writeSaccharideNamesFile(filePath: string, ionNames: string[]) {
|
||||
|
||||
export const SaccharideNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
writeFileAsync(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { Database, Table, DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -250,14 +249,14 @@ async function run(out: string, binary = false, options = DefaultDataOptions, cc
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeFile(out, ccbCif);
|
||||
writeFileAsync(out, ccbCif);
|
||||
|
||||
if (!!ccaOut) {
|
||||
const ccaCif = getEncodedCif(CCA_TABLE_NAME, atoms, binary);
|
||||
if (!fs.existsSync(path.dirname(ccaOut))) {
|
||||
fs.mkdirSync(path.dirname(ccaOut));
|
||||
}
|
||||
writeFile(ccaOut, ccaCif);
|
||||
writeFileAsync(ccaOut, ccaCif);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { Progress } from '../../mol-task';
|
||||
import { Database } from '../../mol-data/db';
|
||||
@@ -27,9 +25,9 @@ export async function ensureAvailable(path: string, url: string, forceDownload =
|
||||
fs.mkdirSync(DATA_DIR);
|
||||
}
|
||||
if (url.endsWith('.gz')) {
|
||||
await writeFile(path, zlib.gunzipSync(await data.buffer()));
|
||||
await writeFileAsync(path, zlib.gunzipSync(await data.arrayBuffer()));
|
||||
} else {
|
||||
await writeFile(path, await data.text());
|
||||
await writeFileAsync(path, await data.text());
|
||||
}
|
||||
console.log(`done downloading ${url}`);
|
||||
}
|
||||
@@ -41,7 +39,7 @@ export async function ensureDataAvailable(options: DataOptions) {
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
const parsed = await parseCif(await readFile(path, 'utf8'));
|
||||
const parsed = await parseCif(await readFileAsync(path, 'utf8'));
|
||||
return CIF.toDatabaseCollection(schema, parsed.result);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import { CIF, CifCategory, getCifFieldType, CifField, CifFile } from '../../mol-io/reader/cif';
|
||||
@@ -22,7 +23,7 @@ function showProgress(p: Progress) {
|
||||
process.stdout.write(`\r${Progress.format(p)}`);
|
||||
}
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
const unzipAsync = util.promisify<zlib.InputType, Buffer>(zlib.unzip);
|
||||
|
||||
async function readFile(ctx: RuntimeContext, filename: string): Promise<ReaderResult<CifFile>> {
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import { convert } from './converter';
|
||||
|
||||
require('util.promisify').shim();
|
||||
|
||||
async function process(srcPath: string, outPath: string, configPath?: string, filterPath?: string) {
|
||||
const config = configPath ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : void 0;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { parseCsv } from '../../mol-io/reader/csv/parser';
|
||||
import { CifFrame, CifBlock } from '../../mol-io/reader/cif';
|
||||
@@ -166,9 +166,9 @@ const MA_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist
|
||||
const CIF_CORE_DIC_PATH = `${DIC_DIR}/cif_core.dic`;
|
||||
const CIF_CORE_DIC_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/cif_core.dic';
|
||||
const CIF_CORE_ENUM_PATH = `${DIC_DIR}/templ_enum.cif`;
|
||||
const CIF_CORE_ENUM_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_enum.cif';
|
||||
const CIF_CORE_ENUM_URL = 'https://raw.githubusercontent.com/COMCIFS/Enumeration_Templates/refs/heads/main/templ_enum.cif';
|
||||
const CIF_CORE_ATTR_PATH = `${DIC_DIR}/templ_attr.cif`;
|
||||
const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_attr.cif';
|
||||
const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/Attribute_Templates/refs/heads/main/templ_attr.cif';
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
|
||||
@@ -93,6 +93,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'Implied':
|
||||
case 'Word':
|
||||
case 'Uri':
|
||||
case 'Iri':
|
||||
return wrapContainer('str', ',', description, container);
|
||||
case 'Real':
|
||||
return wrapContainer('float', ',', description, container);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -65,7 +65,9 @@ function getTypeDef(c: Column): string {
|
||||
case 'float': return 'float';
|
||||
case 'coord': return 'coord';
|
||||
case 'enum':
|
||||
return `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
|
||||
return c.subType === 'int'
|
||||
? `Aliased<${c.values.join(' | ')}>(${c.subType})`
|
||||
: `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
|
||||
case 'matrix':
|
||||
return `Matrix(${c.rows}, ${c.columns})`;
|
||||
case 'vector':
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { UniqueArray } from '../../mol-data/generic';
|
||||
|
||||
const LIPIDS_DIR = path.resolve(__dirname, '../../../../build/lipids/');
|
||||
|
||||
60
src/cli/mvs/mvs-mvsj-to-mvsx.ts
Normal file
60
src/cli/mvs/mvs-mvsj-to-mvsx.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*
|
||||
* Command-line application for converting MolViewSpec MVSJ into MSVX files
|
||||
* Build: npm run build
|
||||
* Run: node lib/commonjs/cli/mvs/mvs-mvsj-to-mvsx -i examples/mvs/1cbs.mvsj -o tmp/1cbs.mvsx
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import fs from 'fs';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { setFSModule } from '../../mol-util/data-source';
|
||||
|
||||
|
||||
setFSModule(fs);
|
||||
|
||||
/** Command line argument values for `main` */
|
||||
interface Args {
|
||||
input: string[],
|
||||
output: string[] | undefined,
|
||||
base_uri: string | undefined,
|
||||
skip_external: boolean,
|
||||
}
|
||||
|
||||
/** Return parsed command line arguments for `main` */
|
||||
function parseArguments(): Args {
|
||||
const parser = new ArgumentParser({ description: 'Command-line application for converting MolViewSpec MVSJ into MSVX files' });
|
||||
parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj format.' });
|
||||
parser.add_argument('-o', '--output', { required: false, nargs: '+', help: 'File path(s) for output files in .mvsx format (one output path for each input file). If ommitted, filenames will be created automatically by replacing file extension.' });
|
||||
parser.add_argument('--base-uri', { help: 'Base URI/path used to resolve relative URIs in the input file (default: path of the input file itself). Use `--base-uri .` for using the current working directory as base URI.' });
|
||||
parser.add_argument('--skip-external', { action: 'store_true', help: 'Do not include external resources (i.e. absolute URIs) in the MVSX.' });
|
||||
const args: Args = parser.parse_args();
|
||||
if (args.output && args.output.length !== args.input.length) {
|
||||
parser.error(`argument: --output: must specify the same number of input and output file paths (specified ${args.input.length} input path${args.input.length !== 1 ? 's' : ''} but ${args.output.length} output path${args.output.length !== 1 ? 's' : ''})`);
|
||||
}
|
||||
return { ...args };
|
||||
}
|
||||
|
||||
/** Main workflow for converting MVSJ to MVSX files. */
|
||||
async function main(args: Args): Promise<void> {
|
||||
const cache = {};
|
||||
for (let i = 0; i < args.input.length; i++) {
|
||||
const input = args.input[i];
|
||||
const output = args.output?.[i] ?? input.replace(/(\.mvsj)?$/i, '.mvsx');
|
||||
console.log(`Processing ${input} -> ${output}`);
|
||||
const mvsj = fs.readFileSync(input, { encoding: 'utf8' });
|
||||
const mvsData = MVSData.fromMVSJ(mvsj);
|
||||
const mvsx = await MVSData.toMVSX(mvsData, {
|
||||
baseUri: args.base_uri ?? input,
|
||||
skipExternal: args.skip_external,
|
||||
cache,
|
||||
});
|
||||
fs.writeFileSync(output, mvsx);
|
||||
}
|
||||
}
|
||||
|
||||
main(parseArguments());
|
||||
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
import { Progress } from '../../mol-task';
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
|
||||
async function readFile(path: string) {
|
||||
if (path.match(/\.bcif$/)) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CifFrame } from '../../mol-io/reader/cif';
|
||||
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing, Trajectory } from '../../mol-model/structure';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { downloadCif } from './helpers';
|
||||
@@ -20,8 +20,7 @@ import { createVolumeIsosurfaceMesh } from '../../mol-repr/volume/isosurface';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
import { volumeFromDensityServerData, DscifFormat } from '../../mol-model-formats/volume/density-server';
|
||||
|
||||
require('util.promisify').shim();
|
||||
const writeFileAsync = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
async function getVolume(url: string): Promise<Volume> {
|
||||
const cif = await downloadCif(url, true);
|
||||
|
||||
@@ -24,6 +24,7 @@ import './index.html';
|
||||
import './tm-align.html';
|
||||
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData, tmAlignStructures, loadStructuresNoAlignment, sequenceAlignStructures } from './superposition';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
|
||||
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
|
||||
|
||||
@@ -95,7 +96,7 @@ class BasicWrapper {
|
||||
...trackball,
|
||||
animate: trackball.animate.name === 'spin'
|
||||
? { name: 'off', params: {} }
|
||||
: { name: 'spin', params: { speed: 1 } }
|
||||
: { name: 'spin', params: { speed: 0.1, axis: Vec3.create(0, -1, 0) } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
async function getMappings(id: string) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
(async function () {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { createProteopediaCustomTheme } from './coloring';
|
||||
import { LoadParams, ModelInfo, RepresentationStyle, StateElements, SupportedFormats } from './helpers';
|
||||
import './index.html';
|
||||
import { volumeStreamingControls } from './ui/controls';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
require('../../mol-plugin-ui/skin/light.scss');
|
||||
|
||||
class MolStarProteopediaWrapper {
|
||||
@@ -267,7 +268,7 @@ class MolStarProteopediaWrapper {
|
||||
...trackball,
|
||||
animate: trackball.animate.name === 'spin'
|
||||
? { name: 'off', params: {} }
|
||||
: { name: 'spin', params: { speed: 1 } }
|
||||
: { name: 'spin', params: { speed: 0.1, axis: Vec3.create(0, -1, 0) } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -118,6 +118,7 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null)
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
@@ -151,6 +152,7 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null)
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,32 +18,33 @@ import { TransformData } from '../../mol-geo/geometry/transform-data';
|
||||
import { sphereVertexCount } from '../../mol-geo/primitive/sphere';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Geometry } from '../../mol-geo/geometry/geometry';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const DebugHelperParams = {
|
||||
export const BoundingSphereHelperParams = {
|
||||
sceneBoundingSpheres: PD.Boolean(false, { description: 'Show full scene bounding spheres.' }),
|
||||
visibleSceneBoundingSpheres: PD.Boolean(false, { description: 'Show visible scene bounding spheres.' }),
|
||||
objectBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible render objects.' }),
|
||||
instanceBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible instances.' }),
|
||||
};
|
||||
export type DebugHelperParams = typeof DebugHelperParams
|
||||
export type DebugHelperProps = PD.Values<DebugHelperParams>
|
||||
export type BoundingSphereHelperParams = typeof BoundingSphereHelperParams;
|
||||
export type BoundingSphereHelperProps = PD.Values<BoundingSphereHelperParams>;
|
||||
|
||||
type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: GraphicsRenderObject, mesh: Mesh }
|
||||
|
||||
export class BoundingSphereHelper {
|
||||
export class BoundingSphereHelper implements DebugHelper<BoundingSphereHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: DebugHelperProps;
|
||||
private _props: BoundingSphereHelperProps;
|
||||
private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>();
|
||||
private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>();
|
||||
private sceneData: BoundingSphereData | undefined;
|
||||
private visibleSceneData: BoundingSphereData | undefined;
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) {
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<BoundingSphereHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(DebugHelperParams), ...props };
|
||||
this._props = { ...PD.getDefaultValues(BoundingSphereHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -120,9 +121,9 @@ export class BoundingSphereHelper {
|
||||
this._props.objectBoundingSpheres || this._props.instanceBoundingSpheres
|
||||
);
|
||||
}
|
||||
get props() { return this._props as Readonly<DebugHelperProps>; }
|
||||
get props() { return this._props as Readonly<BoundingSphereHelperProps>; }
|
||||
|
||||
setProps(props: Partial<DebugHelperProps>) {
|
||||
setProps(props: Partial<BoundingSphereHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
@@ -162,4 +163,4 @@ const instanceMaterialId = getNextMaterialId();
|
||||
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1, transform);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
}
|
||||
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal file
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
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 { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Box } from '../../mol-geo/primitive/box';
|
||||
import { Plane } from '../../mol-geo/primitive/plane';
|
||||
import { Cylinder } from '../../mol-geo/primitive/cylinder';
|
||||
import { Sphere } from '../../mol-geo/primitive/sphere';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const ClipObjectHelperParams = {
|
||||
clipObjects: PD.Boolean(false, { description: 'Show clip-objects of visible render objects.' }),
|
||||
};
|
||||
export type ClipObjectHelperParams = typeof ClipObjectHelperParams;
|
||||
export type ClipObjectHelperProps = PD.Values<ClipObjectHelperParams>;
|
||||
|
||||
//
|
||||
|
||||
/** Serializes clip object params to a string key for deduplication */
|
||||
function clipObjectKey(type: number, invert: boolean, position: ArrayLike<number>, posOffset: number, rotation: ArrayLike<number>, rotOffset: number, scale: ArrayLike<number>, scaleOffset: number, transform: ArrayLike<number>, transformOffset: number): string {
|
||||
// Round floats to 5 decimal places to avoid floating point noise
|
||||
const r = (v: number) => Math.round(v * 100000) / 100000;
|
||||
const parts = [
|
||||
type, invert ? 1 : 0,
|
||||
r(position[posOffset]), r(position[posOffset + 1]), r(position[posOffset + 2]),
|
||||
r(rotation[rotOffset]), r(rotation[rotOffset + 1]), r(rotation[rotOffset + 2]), r(rotation[rotOffset + 3]),
|
||||
r(scale[scaleOffset]), r(scale[scaleOffset + 1]), r(scale[scaleOffset + 2]),
|
||||
];
|
||||
for (let j = 0; j < 16; ++j) {
|
||||
parts.push(r(transform[transformOffset + j]));
|
||||
}
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
type ClipObjectData = {
|
||||
key: string,
|
||||
renderObject: GraphicsRenderObject,
|
||||
indicatorRenderObject: GraphicsRenderObject,
|
||||
mesh: Mesh,
|
||||
}
|
||||
|
||||
const clipObjectColors: Record<number, Color> = {
|
||||
[Clip.Type.plane]: ColorNames.orange,
|
||||
[Clip.Type.sphere]: ColorNames.green,
|
||||
[Clip.Type.cube]: ColorNames.dodgerblue,
|
||||
[Clip.Type.cylinder]: ColorNames.gold,
|
||||
[Clip.Type.infiniteCone]: ColorNames.crimson,
|
||||
};
|
||||
|
||||
const clipMaterialId = getNextMaterialId();
|
||||
const indicatorMaterialId = getNextMaterialId();
|
||||
|
||||
// Pre-rotation matrices for aligning primitives to GLSL SDF local frames
|
||||
// Plane: Rx(-90°) maps primitive Z-normal to GLSL Y-normal
|
||||
const preRotPlaneQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), -Math.PI / 2);
|
||||
const preRotPlaneMat = Mat4.fromQuat(Mat4(), preRotPlaneQuat);
|
||||
// Cone: Rx(+90°) maps primitive Y-axis to GLSL Z-axis
|
||||
const preRotConeQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), Math.PI / 2);
|
||||
const preRotConeMat = Mat4.fromQuat(Mat4(), preRotConeQuat);
|
||||
|
||||
// Temp variables for constructing transforms
|
||||
const _position = Vec3();
|
||||
const _rotation = Quat();
|
||||
const _scale = Vec3();
|
||||
const _clipTransform = Mat4();
|
||||
const _invClipTransform = Mat4();
|
||||
const _rotMat = Mat4();
|
||||
const _translateMat = Mat4();
|
||||
const _baseMat = Mat4();
|
||||
const _tmpMat = Mat4();
|
||||
const _axisEnd = Vec3();
|
||||
const _yAxis = Vec3.create(0, 1, 0);
|
||||
const _zAxis = Vec3.create(0, 0, 1);
|
||||
const _indicatorPos = Vec3();
|
||||
|
||||
export class ClipObjectHelper implements DebugHelper<ClipObjectHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: ClipObjectHelperProps;
|
||||
private objectsData = new Map<string, ClipObjectData>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ClipObjectHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(ClipObjectHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const currentKeys = new Set<string>();
|
||||
const sceneRadius = this.parent.boundingSphereVisible.radius || 50;
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
|
||||
const count = ro.values.dClipObjectCount.ref.value;
|
||||
if (count === 0) return;
|
||||
|
||||
const types = ro.values.uClipObjectType.ref.value;
|
||||
const inverts = ro.values.uClipObjectInvert.ref.value;
|
||||
const positions = ro.values.uClipObjectPosition.ref.value;
|
||||
const rotations = ro.values.uClipObjectRotation.ref.value;
|
||||
const scales = ro.values.uClipObjectScale.ref.value;
|
||||
const transforms = ro.values.uClipObjectTransform.ref.value;
|
||||
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const type = types[i];
|
||||
if (type === Clip.Type.none) continue;
|
||||
|
||||
const key = clipObjectKey(
|
||||
type, inverts[i],
|
||||
positions, i * 3,
|
||||
rotations, i * 4,
|
||||
scales, i * 3,
|
||||
transforms, i * 16
|
||||
);
|
||||
|
||||
currentKeys.add(key);
|
||||
|
||||
if (this.objectsData.has(key)) continue;
|
||||
|
||||
// Extract per-object params
|
||||
Vec3.fromArray(_position, positions, i * 3);
|
||||
Quat.fromArray(_rotation, rotations, i * 4);
|
||||
Quat.normalize(_rotation, _rotation); // ensure unit quaternion for proper rotation
|
||||
Vec3.fromArray(_scale, scales, i * 3);
|
||||
Mat4.fromArray(_clipTransform, transforms, i * 16);
|
||||
|
||||
// Build base transform (translate * rotate) without scale,
|
||||
// so each shape can insert pre-rotations before scale.
|
||||
Mat4.fromQuat(_rotMat, _rotation);
|
||||
Mat4.fromTranslation(_translateMat, _position);
|
||||
Mat4.mul(_baseMat, _translateMat, _rotMat);
|
||||
|
||||
// apply inverse of clip transform
|
||||
if (!Mat4.isIdentity(_clipTransform)) {
|
||||
Mat4.invert(_invClipTransform, _clipTransform);
|
||||
Mat4.mul(_baseMat, _invClipTransform, _baseMat);
|
||||
}
|
||||
|
||||
const mesh = createClipObjectMesh(type, _baseMat, _scale, sceneRadius);
|
||||
const color = clipObjectColors[type] || ColorNames.white;
|
||||
const renderObject = createClipObjectRenderObject(mesh, color, clipMaterialId, type);
|
||||
|
||||
// Create position/rotation indicator mesh
|
||||
const invert = inverts[i];
|
||||
const indicatorMesh = createIndicatorMesh(_position, _rotation, _clipTransform, _scale, type, invert);
|
||||
const indicatorRenderObject = createIndicatorRenderObject(indicatorMesh, indicatorMaterialId);
|
||||
|
||||
this.scene.add(renderObject);
|
||||
this.scene.add(indicatorRenderObject);
|
||||
this.objectsData.set(key, { key, renderObject, indicatorRenderObject, mesh });
|
||||
}
|
||||
});
|
||||
|
||||
// Remove clip objects no longer present
|
||||
this.objectsData.forEach((data, key) => {
|
||||
if (!currentKeys.has(key)) {
|
||||
this.scene.remove(data.renderObject);
|
||||
this.scene.remove(data.indicatorRenderObject);
|
||||
this.objectsData.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.clipObjects;
|
||||
this.objectsData.forEach(data => {
|
||||
data.renderObject.state.visible = visible;
|
||||
data.indicatorRenderObject.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.objectsData.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.clipObjects;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<ClipObjectHelperProps>; }
|
||||
|
||||
setProps(props: Partial<ClipObjectHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function createClipObjectMesh(type: number, baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
|
||||
switch (type) {
|
||||
case Clip.Type.plane: return createPlaneMesh(baseMat, sceneRadius);
|
||||
case Clip.Type.sphere: return createSphereMesh(baseMat, scale);
|
||||
case Clip.Type.cube: return createCubeMesh(baseMat, scale);
|
||||
case Clip.Type.cylinder: return createCylinderMesh(baseMat, scale);
|
||||
case Clip.Type.infiniteCone: return createConeMesh(baseMat, scale, sceneRadius);
|
||||
default: return createSphereMesh(baseMat, scale); // fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plane: GLSL normal is quaternionTransform(rotation, vec3(0,1,0)) — Y-up default.
|
||||
* Plane() primitive lies in XY with normal (0,0,1) along Z.
|
||||
* Pre-rotate Rx(-90°) to align primitive Z-normal to GLSL Y-normal.
|
||||
* Sized to cover the scene bounding sphere. Clip scale is ignored (plane is infinite in GLSL).
|
||||
*/
|
||||
function createPlaneMesh(baseMat: Mat4, sceneRadius: number): Mesh {
|
||||
const size = Math.max(sceneRadius * 2, 10);
|
||||
// baseMat * preRotPlane * uniformScale(size)
|
||||
Mat4.mul(_tmpMat, baseMat, preRotPlaneMat);
|
||||
Mat4.scale(_tmpMat, _tmpMat, Vec3.create(size, size, 1));
|
||||
|
||||
const plane = Plane();
|
||||
const builderState = MeshBuilder.createState(256, 128);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, plane);
|
||||
// Add flipped backface for double-sided visibility
|
||||
MeshBuilder.addPrimitiveFlipped(builderState, _tmpMat, plane);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sphere: SDF uses scale * 0.5 as the radii (ellipsoid).
|
||||
* Sphere primitive has radius 1.
|
||||
* Transform: baseMat * scale * 0.5
|
||||
*/
|
||||
function createSphereMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
const detail = 2;
|
||||
const sphere = getSphereForHelper(detail);
|
||||
// baseMat * scale(scale * 0.5)
|
||||
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1] * 0.5, scale[2] * 0.5));
|
||||
|
||||
const vertexCount = 10 * Math.pow(2, 2 * detail) + 2;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, sphere);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
let _helperSphere: ReturnType<typeof Sphere> | undefined;
|
||||
function getSphereForHelper(detail: number) {
|
||||
if (!_helperSphere) _helperSphere = Sphere(detail);
|
||||
return _helperSphere;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cube: SDF uses scale * 0.5 as half-extents.
|
||||
* Box() primitive is ±0.5 (unit cube), so scaling by `scale` gives half-extents of scale*0.5.
|
||||
*/
|
||||
function createCubeMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
// baseMat * scale(scale)
|
||||
Mat4.scale(_tmpMat, baseMat, scale);
|
||||
|
||||
const box = Box();
|
||||
const builderState = MeshBuilder.createState(256, 128);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, box);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cylinder: SDF axis along Y, radius = scale.x * 0.5, half-height = scale.y * 0.5.
|
||||
* Cylinder primitive: axis along Y, radius=1 in XZ, half-height=0.5 in Y.
|
||||
* Need: X/Z *= scale.x * 0.5 (radius 1 → scale.x*0.5), Y *= scale.y (half-height 0.5 → scale.y*0.5).
|
||||
*/
|
||||
function createCylinderMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
const cyl = Cylinder({ radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: true, bottomCap: true });
|
||||
// baseMat * scale(scale.x * 0.5, scale.y, scale.x * 0.5) — use scale.x for both radial axes
|
||||
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1], scale[0] * 0.5));
|
||||
|
||||
const vertexCount = cyl.vertices.length / 3;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, cyl);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* InfiniteCone: GLSL SDF axis along Z, radial in XY.
|
||||
* surface: size.x * length(t.xy) + size.y * t.z = 0 (size = scale * 0.5)
|
||||
* half-angle: tan(θ) = scale.y / scale.x
|
||||
* apex at clip position (origin), opens in -Z direction.
|
||||
*
|
||||
* Cylinder primitive (radiusTop=0, radiusBottom=1, height=1):
|
||||
* axis along Y, tip at Y=+0.5, base at Y=-0.5, base radius=1.
|
||||
*
|
||||
* Transform chain (right-to-left):
|
||||
* 1. Scale(baseRadius, coneLength, baseRadius): stretch primitive to correct proportions
|
||||
* 2. Translate(0, -0.5*coneLength, 0): move tip from Y=+0.5·cL to Y=0 (apex at origin)
|
||||
* (after scale, tip is at Y=+0.5·cL; shifting by -0.5·cL puts it at Y=0)
|
||||
* 3. preRotCone Rx(+90°): map prim-Y→Z, so cone axis becomes Z, opening in -Z
|
||||
* 4. baseMat: position + rotation of clip object
|
||||
*/
|
||||
function createConeMesh(baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
|
||||
const cone = Cylinder({ radiusTop: 0, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: false, bottomCap: true });
|
||||
|
||||
// Visible length of the (infinite) cone, and base radius matching the GLSL half-angle
|
||||
const coneLength = Math.max(sceneRadius * 2, 10);
|
||||
const tanHalfAngle = (scale[1] || 1) / (scale[0] || 1); // tan(θ) = scaleY / scaleX
|
||||
const baseRadius = coneLength * tanHalfAngle;
|
||||
|
||||
// baseMat * preRotCone * Translate(0, -coneLength/2, 0) * Scale(baseRadius, coneLength, baseRadius)
|
||||
const scaleMat = Mat4.fromScaling(Mat4(), Vec3.create(baseRadius, coneLength, baseRadius));
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), Vec3.create(0, -coneLength * 0.5, 0));
|
||||
Mat4.mul(_tmpMat, translateMat, scaleMat);
|
||||
Mat4.mul(_tmpMat, preRotConeMat, _tmpMat);
|
||||
Mat4.mul(_tmpMat, baseMat, _tmpMat);
|
||||
|
||||
const vertexCount = cone.vertices.length / 3;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, cone);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
function createClipObjectRenderObject(mesh: Mesh, color: Color, materialId: number, type: number) {
|
||||
const alpha = type === Clip.Type.plane ? 0.25 : 0.15;
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mesh with a sphere at the clip object position and a cylinder
|
||||
* along the characteristic axis to indicate orientation.
|
||||
*
|
||||
* - Plane/sphere/cube/cylinder: axis = rotated Y (matches GLSL normal/axis direction)
|
||||
* - InfiniteCone: axis = rotated Z (cone axis is Z in local frame)
|
||||
* - Plane with invert: direction is flipped
|
||||
*/
|
||||
function createIndicatorMesh(position: Vec3, rotation: Quat, clipTransform: Mat4, scale: Vec3, type: number, invert: boolean): Mesh {
|
||||
const objectSize = Math.max(scale[0], scale[1], scale[2]);
|
||||
const sphereRadius = Math.max(objectSize * 0.004, 0.01);
|
||||
const cylinderRadius = sphereRadius * 0.4;
|
||||
const axisLength = Math.max(objectSize * 0.1, 2);
|
||||
|
||||
// Transform position by inverse of clipTransform if non-identity
|
||||
Vec3.copy(_indicatorPos, position);
|
||||
if (!Mat4.isIdentity(clipTransform)) {
|
||||
Mat4.invert(_invClipTransform, clipTransform);
|
||||
Vec3.transformMat4(_indicatorPos, _indicatorPos, _invClipTransform);
|
||||
}
|
||||
|
||||
// Choose the local-frame axis based on clip type
|
||||
const localAxis = type === Clip.Type.infiniteCone ? _zAxis : _yAxis;
|
||||
Vec3.transformQuat(_axisEnd, localAxis, rotation);
|
||||
|
||||
// Cone opens in -Z locally, so negate to point along the cone opening
|
||||
if (type === Clip.Type.infiniteCone) {
|
||||
Vec3.negate(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// For planes, the normal points toward the clipped (removed) side.
|
||||
// Flip so the indicator points toward the non-clipped (kept) geometry.
|
||||
// When inverted, the kept side is the normal side, so don't flip.
|
||||
if (type === Clip.Type.plane && !invert) {
|
||||
Vec3.negate(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// If clipTransform is non-identity, also transform the axis direction
|
||||
if (!Mat4.isIdentity(clipTransform)) {
|
||||
// Transform direction (not position) by inverse clipTransform
|
||||
const endWorld = Vec3();
|
||||
Vec3.add(endWorld, position, Vec3.scale(Vec3(), _axisEnd, axisLength));
|
||||
Vec3.transformMat4(endWorld, endWorld, _invClipTransform);
|
||||
Vec3.sub(_axisEnd, endWorld, _indicatorPos);
|
||||
Vec3.normalize(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// Axis cylinder endpoint
|
||||
const axisEndPoint = Vec3();
|
||||
Vec3.scaleAndAdd(axisEndPoint, _indicatorPos, _axisEnd, axisLength);
|
||||
|
||||
const builderState = MeshBuilder.createState(512, 256);
|
||||
// Position sphere
|
||||
addSphere(builderState, _indicatorPos, sphereRadius, 1);
|
||||
// Rotation axis cylinder
|
||||
addCylinder(builderState, _indicatorPos, axisEndPoint, 1, { radiusTop: cylinderRadius, radiusBottom: cylinderRadius, radialSegments: 8 });
|
||||
// Small sphere at tip of axis
|
||||
addSphere(builderState, axisEndPoint, cylinderRadius * 1.5, 1);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
function createIndicatorRenderObject(mesh: Mesh, materialId: number) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.7, doubleSided: false, cellSize: 0, batchSize: 0 }, ColorNames.white, 1);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal file
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { DirectVolumeValues } from '../../mol-gl/renderable/direct-volume';
|
||||
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const DirectVolumeHelperParams = {
|
||||
directVolumeEdges: PD.Boolean(false, { description: 'Show edges of visible direct-volume render objects.' }),
|
||||
};
|
||||
export type DirectVolumeHelperParams = typeof DirectVolumeHelperParams;
|
||||
export type DirectVolumeHelperProps = PD.Values<DirectVolumeHelperParams>;
|
||||
|
||||
const directVolumeMaterialId = getNextMaterialId();
|
||||
|
||||
type TrackedEntry = { ro: GraphicsRenderObject, version: number };
|
||||
|
||||
export class DirectVolumeHelper implements DebugHelper<DirectVolumeHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: DirectVolumeHelperProps;
|
||||
private renderObjects = new Map<number, TrackedEntry>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DirectVolumeHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(DirectVolumeHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'direct-volume') return;
|
||||
|
||||
const values = ro.values as DirectVolumeValues;
|
||||
const version = values.uUnitToCartn.ref.version + values.uGridDim.ref.version + values.aTransform.ref.version;
|
||||
|
||||
const existing = this.renderObjects.get(ro.id);
|
||||
if (existing && existing.version === version) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old entry if version changed
|
||||
if (existing) {
|
||||
this.scene.remove(existing.ro);
|
||||
this.renderObjects.delete(ro.id);
|
||||
}
|
||||
|
||||
const lines = createVolumeEdgeLines(values);
|
||||
if (!lines) return;
|
||||
|
||||
const linesRO = createLinesRenderObject(lines, directVolumeMaterialId);
|
||||
this.scene.add(linesRO);
|
||||
this.renderObjects.set(ro.id, { ro: linesRO, version });
|
||||
previousIds.delete(ro.id);
|
||||
});
|
||||
|
||||
for (const id of previousIds) {
|
||||
const entry = this.renderObjects.get(id);
|
||||
if (entry) {
|
||||
this.scene.remove(entry.ro);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.directVolumeEdges;
|
||||
this.renderObjects.forEach(entry => {
|
||||
entry.ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.directVolumeEdges;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<DirectVolumeHelperProps>; }
|
||||
|
||||
setProps(props: Partial<DirectVolumeHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* The volume proxy box in the shader uses aPosition in [-0.5, 0.5]^3,
|
||||
* shifted to [0,1]^3 (unitCoord = aPosition + 0.5), then transformed by:
|
||||
* uUnitToCartn → Cartesian space
|
||||
* aTransform → instance space
|
||||
*
|
||||
* We replicate this pipeline to get the correct world-space edges.
|
||||
* Grid ticks are placed at 1/gridDim intervals along each edge.
|
||||
*/
|
||||
function createVolumeEdgeLines(values: DirectVolumeValues): Lines | undefined {
|
||||
const unitToCartn = values.uUnitToCartn.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
const bs = values.boundingSphere.ref.value;
|
||||
if (bs.radius < 1e-6) return undefined;
|
||||
|
||||
const builder = LinesBuilder.create(128 * instanceCount);
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
// Combined transform: aTransform * uUnitToCartn
|
||||
const combined = Mat4.mul(Mat4(), instTransform, unitToCartn);
|
||||
addBox(builder, combined, 0);
|
||||
}
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.8 };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.orange, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
266
src/extensions/debug-helpers/image-helper.ts
Normal file
266
src/extensions/debug-helpers/image-helper.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
|
||||
import { ImageValues } from '../../mol-gl/renderable/image';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { addSphere as addLinesSphere } from '../../mol-geo/geometry/lines/builder/sphere';
|
||||
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
|
||||
import { addPlane } from '../../mol-geo/geometry/lines/builder/plane';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const ImageHelperParams = {
|
||||
imageEdges: PD.Boolean(false, { description: 'Show edges of visible image render objects.' }),
|
||||
};
|
||||
export type ImageHelperParams = typeof ImageHelperParams;
|
||||
export type ImageHelperProps = PD.Values<ImageHelperParams>;
|
||||
|
||||
const imageEdgeMaterialId = getNextMaterialId();
|
||||
const imageTrimMaterialId = getNextMaterialId();
|
||||
|
||||
// Temp vectors
|
||||
const _trimPos = Vec3();
|
||||
const _trimScale = Vec3();
|
||||
const _trimRot = Quat();
|
||||
const _trimTransform = Mat4();
|
||||
const _tmpMat = Mat4();
|
||||
|
||||
export class ImageHelper implements DebugHelper<ImageHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: ImageHelperProps;
|
||||
private renderObjects = new Map<number, { roList: GraphicsRenderObject[], version: number }>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ImageHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(ImageHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'image') return;
|
||||
|
||||
const values = ro.values as ImageValues;
|
||||
const version = values.aPosition.ref.version
|
||||
+ values.uTrimType.ref.version + values.uTrimCenter.ref.version
|
||||
+ values.uTrimRotation.ref.version + values.uTrimScale.ref.version
|
||||
+ values.uTrimTransform.ref.version + values.aTransform.ref.version;
|
||||
|
||||
const existing = this.renderObjects.get(ro.id);
|
||||
if (existing && existing.version === version) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old entries if version changed
|
||||
if (existing) {
|
||||
for (const oldRO of existing.roList) this.scene.remove(oldRO);
|
||||
this.renderObjects.delete(ro.id);
|
||||
}
|
||||
|
||||
const roList: GraphicsRenderObject[] = [];
|
||||
|
||||
const edgeLines = createImageEdgeLines(values);
|
||||
if (edgeLines) {
|
||||
const edgeRO = createLinesRenderObject(edgeLines, imageEdgeMaterialId, ColorNames.cyan, 0.8);
|
||||
this.scene.add(edgeRO);
|
||||
roList.push(edgeRO);
|
||||
}
|
||||
|
||||
const trimLines = createTrimEdgeLines(values);
|
||||
if (trimLines) {
|
||||
const trimRO = createLinesRenderObject(trimLines, imageTrimMaterialId, ColorNames.yellow, 0.7);
|
||||
this.scene.add(trimRO);
|
||||
roList.push(trimRO);
|
||||
}
|
||||
|
||||
if (roList.length > 0) {
|
||||
this.renderObjects.set(ro.id, { roList, version });
|
||||
}
|
||||
previousIds.delete(ro.id);
|
||||
});
|
||||
|
||||
for (const id of previousIds) {
|
||||
const entry = this.renderObjects.get(id);
|
||||
if (entry) {
|
||||
for (const ro of entry.roList) this.scene.remove(ro);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.imageEdges;
|
||||
this.renderObjects.forEach(entry => {
|
||||
for (const ro of entry.roList) ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.imageEdges;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<ImageHelperProps>; }
|
||||
|
||||
setProps(props: Partial<ImageHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* Image quad vertex layout (from image.ts):
|
||||
* Vertex 0: UV (0,1) — top-left
|
||||
* Vertex 1: UV (0,0) — bottom-left
|
||||
* Vertex 2: UV (1,1) — top-right
|
||||
* Vertex 3: UV (1,0) — bottom-right
|
||||
*
|
||||
* addPlane expects corners in winding order (0→1→2→3→0),
|
||||
* so we reorder to: top-left, bottom-left, bottom-right, top-right.
|
||||
*/
|
||||
const _planeCorners = new Float32Array(12);
|
||||
|
||||
function createImageEdgeLines(values: ImageValues): Lines | undefined {
|
||||
const positions = values.aPosition.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
if (positions.length < 12) return undefined; // need 4 vertices × 3 components
|
||||
|
||||
// Reorder from [TL, BL, TR, BR] to winding order [TL, BL, BR, TR]
|
||||
// V0 (TL) → slot 0
|
||||
_planeCorners[0] = positions[0]; _planeCorners[1] = positions[1]; _planeCorners[2] = positions[2];
|
||||
// V1 (BL) → slot 1
|
||||
_planeCorners[3] = positions[3]; _planeCorners[4] = positions[4]; _planeCorners[5] = positions[5];
|
||||
// V3 (BR) → slot 2
|
||||
_planeCorners[6] = positions[9]; _planeCorners[7] = positions[10]; _planeCorners[8] = positions[11];
|
||||
// V2 (TR) → slot 3
|
||||
_planeCorners[9] = positions[6]; _planeCorners[10] = positions[7]; _planeCorners[11] = positions[8];
|
||||
|
||||
const builder = LinesBuilder.create(4 * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const transform = Mat4();
|
||||
Mat4.fromArray(transform, transforms, inst * 16);
|
||||
addPlane(builder, _planeCorners, transform, 0);
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createTrimEdgeLines(values: ImageValues): Lines | undefined {
|
||||
const trimType = values.uTrimType.ref.value as number;
|
||||
if (trimType === 0) return undefined; // no trim
|
||||
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
Vec3.copy(_trimPos, values.uTrimCenter.ref.value);
|
||||
Quat.copy(_trimRot, values.uTrimRotation.ref.value);
|
||||
Vec3.copy(_trimScale, values.uTrimScale.ref.value);
|
||||
Mat4.copy(_trimTransform, values.uTrimTransform.ref.value);
|
||||
|
||||
if (trimType === Clip.Type.cube) {
|
||||
return createCubeTrimLines(transforms, instanceCount);
|
||||
} else if (trimType === Clip.Type.sphere) {
|
||||
return createSphereTrimLines(transforms, instanceCount);
|
||||
}
|
||||
|
||||
// For other trim types (plane, cylinder, cone), draw a cube outline as a fallback
|
||||
// using the trim center/scale/rotation
|
||||
return createCubeTrimLines(transforms, instanceCount);
|
||||
}
|
||||
|
||||
function createCubeTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
|
||||
// Build cube transform: translate * rotate * scale
|
||||
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
|
||||
const scaleMat = Mat4.fromScaling(Mat4(), _trimScale);
|
||||
Mat4.mul(_tmpMat, translateMat, rotMat);
|
||||
Mat4.mul(_tmpMat, _tmpMat, scaleMat);
|
||||
|
||||
// Apply inverse of trim transform
|
||||
if (!Mat4.isIdentity(_trimTransform)) {
|
||||
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
|
||||
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
|
||||
}
|
||||
|
||||
// addBox uses [0,1]^3, trim cube uses [-0.5,0.5]^3 — prepend offset
|
||||
const offset = Mat4.fromTranslation(Mat4(), Vec3.create(-0.5, -0.5, -0.5));
|
||||
Mat4.mul(_tmpMat, _tmpMat, offset);
|
||||
|
||||
const builder = LinesBuilder.create(12 * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
|
||||
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
|
||||
addBox(builder, combined, 0);
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createSphereTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
|
||||
const radius = Math.max(_trimScale[0] * 0.5, _trimScale[1] * 0.5, _trimScale[2] * 0.5);
|
||||
|
||||
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
|
||||
Mat4.mul(_tmpMat, translateMat, rotMat);
|
||||
|
||||
if (!Mat4.isIdentity(_trimTransform)) {
|
||||
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
|
||||
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
|
||||
}
|
||||
|
||||
const segments = 32;
|
||||
const circlesPerDimension = 3;
|
||||
const builder = LinesBuilder.create(segments * 3 * circlesPerDimension * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
|
||||
|
||||
addLinesSphere(builder, radius, combined, 0, { segments, circlesPerDimension });
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createLinesRenderObject(lines: Lines, materialId: number, color: Color, alpha: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, color, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
71
src/extensions/debug-helpers/index.ts
Normal file
71
src/extensions/debug-helpers/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { BoundingSphereHelper, BoundingSphereHelperParams } from './bounding-sphere-helper';
|
||||
import { ClipObjectHelper, ClipObjectHelperParams } from './clip-object-helper';
|
||||
import { DirectVolumeHelper, DirectVolumeHelperParams } from './direct-volume-helper';
|
||||
import { ImageHelper, ImageHelperParams } from './image-helper';
|
||||
import { MeshHelper, MeshHelperParams } from './mesh-helper';
|
||||
|
||||
const DebugHelpersParams = {
|
||||
...BoundingSphereHelperParams,
|
||||
...ClipObjectHelperParams,
|
||||
...MeshHelperParams,
|
||||
...ImageHelperParams,
|
||||
...DirectVolumeHelperParams,
|
||||
};
|
||||
type DebugHelpersParams = typeof DebugHelpersParams;
|
||||
type DebugHelpersProps = PD.Values<DebugHelpersParams>;
|
||||
|
||||
export const DebugHelpers = PluginBehavior.create<DebugHelpersProps>({
|
||||
name: 'extension-debug-helpers',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'Debug Helpers'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<DebugHelpersProps> {
|
||||
async register(): Promise<void> {
|
||||
await this.ctx.canvas3dInitialized;
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const dr = canvas3d.debugRegistry;
|
||||
const { ctx, parent } = dr;
|
||||
|
||||
dr.register('bounding-sphere', new BoundingSphereHelper(ctx, parent, this.params));
|
||||
dr.register('clip-object', new ClipObjectHelper(ctx, parent, this.params));
|
||||
dr.register('mesh', new MeshHelper(ctx, parent, this.params));
|
||||
dr.register('image', new ImageHelper(ctx, parent, this.params));
|
||||
dr.register('direct-volume', new DirectVolumeHelper(ctx, parent, this.params));
|
||||
}
|
||||
|
||||
update(params: DebugHelpersProps) {
|
||||
const changed = super.update(params);
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (changed && canvas3d) {
|
||||
canvas3d.debugRegistry.setProps(params);
|
||||
canvas3d.requestDraw();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const dr = canvas3d.debugRegistry;
|
||||
dr.unregister('bounding-sphere');
|
||||
dr.unregister('clip-object');
|
||||
dr.unregister('mesh');
|
||||
dr.unregister('image');
|
||||
dr.unregister('direct-volume');
|
||||
}
|
||||
},
|
||||
params: () => DebugHelpersParams,
|
||||
canAutoUpdate: () => true,
|
||||
});
|
||||
164
src/extensions/debug-helpers/mesh-helper.ts
Normal file
164
src/extensions/debug-helpers/mesh-helper.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { MeshValues } from '../../mol-gl/renderable/mesh';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const MeshHelperParams = {
|
||||
meshNormals: PD.Boolean(false, { description: 'Show normals of visible mesh render objects.' }),
|
||||
};
|
||||
export type MeshHelperParams = typeof MeshHelperParams;
|
||||
export type MeshHelperProps = PD.Values<MeshHelperParams>;
|
||||
|
||||
const meshHelperMaterialId = getNextMaterialId();
|
||||
|
||||
const _v = Vec3();
|
||||
const _n = Vec3();
|
||||
const _start = Vec3();
|
||||
const _end = Vec3();
|
||||
|
||||
export class MeshHelper implements DebugHelper<MeshHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: MeshHelperProps;
|
||||
private renderObjects = new Map<number, GraphicsRenderObject>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<MeshHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(MeshHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
const currentIds = new Set<number>();
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'mesh') return;
|
||||
|
||||
currentIds.add(ro.id);
|
||||
|
||||
// Skip if we already have normals for this render object
|
||||
if (this.renderObjects.has(ro.id)) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = ro.values as MeshValues;
|
||||
const lines = createNormalLines(values);
|
||||
if (!lines) return;
|
||||
|
||||
const linesRO = createNormalLinesRenderObject(lines, meshHelperMaterialId);
|
||||
this.scene.add(linesRO);
|
||||
this.renderObjects.set(ro.id, linesRO);
|
||||
});
|
||||
|
||||
// Remove normals for render objects no longer present
|
||||
for (const id of previousIds) {
|
||||
const linesRO = this.renderObjects.get(id);
|
||||
if (linesRO) {
|
||||
this.scene.remove(linesRO);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.meshNormals;
|
||||
this.renderObjects.forEach(ro => {
|
||||
ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.meshNormals;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<MeshHelperProps>; }
|
||||
|
||||
setProps(props: Partial<MeshHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function createNormalLines(values: MeshValues): Lines | undefined {
|
||||
const positions = values.aPosition.ref.value;
|
||||
const normals = values.aNormal.ref.value;
|
||||
const indices = values.elements.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
const vertexCount = positions.length / 3;
|
||||
if (vertexCount === 0) return undefined;
|
||||
|
||||
// Determine normal line length: proportional to bounding sphere radius
|
||||
const bs = values.boundingSphere.ref.value;
|
||||
const normalLength = Math.max(bs.radius * 0.01, 0.1);
|
||||
|
||||
// Count unique vertices referenced by indices
|
||||
const indexCount = values.drawCount.ref.value;
|
||||
|
||||
const builder = LinesBuilder.create(indexCount * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const tOffset = inst * 16;
|
||||
const transform = Mat4();
|
||||
Mat4.fromArray(transform, transforms, tOffset);
|
||||
|
||||
// Use a set to avoid drawing duplicate normals for shared vertices
|
||||
const visited = new Set<number>();
|
||||
|
||||
for (let i = 0; i < indexCount; ++i) {
|
||||
const vi = indices[i];
|
||||
if (visited.has(vi)) continue;
|
||||
visited.add(vi);
|
||||
|
||||
const vo = vi * 3;
|
||||
Vec3.set(_v, positions[vo], positions[vo + 1], positions[vo + 2]);
|
||||
Vec3.set(_n, normals[vo], normals[vo + 1], normals[vo + 2]);
|
||||
|
||||
// Transform vertex position and normal direction by instance transform
|
||||
Vec3.transformMat4(_start, _v, transform);
|
||||
Vec3.transformDirection(_end, _n, transform);
|
||||
Vec3.normalize(_end, _end);
|
||||
Vec3.scaleAndAdd(_end, _start, _end, normalLength);
|
||||
|
||||
builder.addVec(_start, _end, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createNormalLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.7 };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.magenta, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export async function getG3dDataBlock(ctx: PluginContext, header: G3dHeader, url
|
||||
|
||||
async function getRawData(ctx: PluginContext, urlOrData: string | Uint8Array, range: { offset: number, size: number }) {
|
||||
if (typeof urlOrData === 'string') {
|
||||
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: [['Range', `bytes=${range.offset}-${range.offset + range.size - 1}`]], type: 'binary' }));
|
||||
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: { 'Range': `bytes=${range.offset}-${range.offset + range.size - 1}` }, type: 'binary' }));
|
||||
} else {
|
||||
return urlOrData.slice(range.offset, range.offset + range.size);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -241,7 +241,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
|
||||
// create a glTF mesh if needed
|
||||
if (instanceIndex === 0 || !sameGeometryBuffers || !sameColorBuffer) {
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = GlbExporter.getInstance(input, instanceIndex);
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = GlbExporter.getInstance(input, instanceIndex);
|
||||
|
||||
// create geometry buffers if needed
|
||||
if (instanceIndex === 0 || !sameGeometryBuffers) {
|
||||
@@ -253,7 +253,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
|
||||
// create a color buffer if needed
|
||||
if (instanceIndex === 0 || !sameColorBuffer) {
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
}
|
||||
|
||||
// glTF mesh
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -30,6 +30,10 @@ import { RenderObjectExporter, RenderObjectExportData } from './render-object-ex
|
||||
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
|
||||
import { assertUnreachable } from '../../mol-util/type-helpers';
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { computeFrenetFrames } from '../../mol-math/linear-algebra/3d/frenet-frames';
|
||||
import { addTube } from '../../mol-geo/geometry/mesh/builder/tube';
|
||||
import { arrayCopyOffset } from '../../mol-util/array';
|
||||
|
||||
const GeoExportName = 'geo-export';
|
||||
|
||||
@@ -49,22 +53,32 @@ export interface AddMeshInput {
|
||||
groups: Float32Array | Uint8Array
|
||||
vertexCount: number
|
||||
drawCount: number
|
||||
vertexMapping?: number[]
|
||||
} | undefined
|
||||
meshes: Mesh[] | undefined
|
||||
values: BaseValues & { readonly uDoubleSided?: ValueCell<any> }
|
||||
values: BaseValues & {
|
||||
readonly uDoubleSided?: ValueCell<boolean>
|
||||
readonly aGroup?: ValueCell<Float32Array>
|
||||
readonly tPositionGroup?: ValueCell<TextureImage<Float32Array>>
|
||||
}
|
||||
isGeoTexture: boolean
|
||||
mode: MeshMode
|
||||
webgl: WebGLContext | undefined
|
||||
ctx: RuntimeContext
|
||||
vertexMapping?: number[]
|
||||
}
|
||||
|
||||
export type MeshGeoData = {
|
||||
values: BaseValues,
|
||||
groups: Float32Array | Uint8Array,
|
||||
vertexCount: number,
|
||||
instanceIndex: number,
|
||||
isGeoTexture: boolean,
|
||||
values: BaseValues & {
|
||||
readonly aGroup?: ValueCell<Float32Array>
|
||||
readonly tPositionGroup?: ValueCell<TextureImage<Float32Array>>
|
||||
}
|
||||
groups?: Float32Array | Uint8Array,
|
||||
vertexCount: number
|
||||
instanceIndex: number
|
||||
isGeoTexture: boolean
|
||||
mode: MeshMode
|
||||
vertexMapping?: number[]
|
||||
}
|
||||
|
||||
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
|
||||
@@ -77,7 +91,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
return unpackRGBToInt(r, g, b) / sizeDataFactor;
|
||||
}
|
||||
|
||||
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
|
||||
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number, vertexIndex: number): number {
|
||||
const tSize = values.tSize.ref.value;
|
||||
let size = 0;
|
||||
switch (values.dSizeType.ref.value) {
|
||||
@@ -94,6 +108,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex * groupCount + group);
|
||||
break;
|
||||
case 'vertex':
|
||||
size = MeshExporter.getSizeFromTexture(tSize, vertexIndex);
|
||||
break;
|
||||
case 'vertexInstance':
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex * vertexCount + vertexIndex);
|
||||
break;
|
||||
}
|
||||
return size * values.uSizeFactor.ref.value;
|
||||
}
|
||||
@@ -225,13 +246,14 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
indices: mesh.indexBuffer.ref.value,
|
||||
groups: mesh.groupBuffer.ref.value,
|
||||
vertexCount: mesh.vertexCount,
|
||||
drawCount: mesh.triangleCount * 3
|
||||
drawCount: mesh.triangleCount * 3,
|
||||
vertexMapping: input.vertexMapping,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
|
||||
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
|
||||
const { values, groups, instanceIndex, isGeoTexture, mode } = geoData;
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const uColor = values.uColor.ref.value;
|
||||
@@ -239,13 +261,23 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const overpaintType = values.dOverpaintType.ref.value;
|
||||
const dOverpaint = values.dOverpaint.ref.value;
|
||||
const tOverpaint = values.tOverpaint.ref.value.array;
|
||||
const usePalette = values.dUsePalette.ref.value;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
if (geoData.vertexMapping) {
|
||||
vertexIndex = geoData.vertexMapping[vertexIndex];
|
||||
vertexCount = values.uVertexCount.ref.value;
|
||||
} else if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
const group = isGeoTexture
|
||||
? MeshExporter.getGroup(groups!, vertexIndex)
|
||||
: values.dGeometryType.ref.value === 'spheres'
|
||||
? values.tPositionGroup!.ref.value.array[vertexIndex * 4 + 3]
|
||||
: values.aGroup!.ref.value[vertexIndex];
|
||||
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
@@ -255,12 +287,10 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
color = Color.fromArray(tColor, instanceIndex * 3);
|
||||
break;
|
||||
case 'group': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
color = Color.fromArray(tColor, group * 3);
|
||||
break;
|
||||
}
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
|
||||
break;
|
||||
}
|
||||
@@ -279,12 +309,31 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
default: throw new Error('Unsupported color type.');
|
||||
}
|
||||
|
||||
if (usePalette) {
|
||||
const palette = values.tPalette.ref.value;
|
||||
const paletteArray = palette.array;
|
||||
const paletteLength = paletteArray.length / 3;
|
||||
const [r, g, b] = Color.toRgb(color);
|
||||
const paletteValue = ((r * 256 * 256 + g * 256 + b) - 1) / ColorTheme.PaletteScale;
|
||||
const fIndex = paletteValue * (paletteLength - 1);
|
||||
if (palette.filter === 'nearest') {
|
||||
const index = Math.round(fIndex);
|
||||
color = Color.fromArray(paletteArray, index * 3);
|
||||
} else { // linear
|
||||
const index0 = Math.floor(fIndex);
|
||||
const index1 = index0 + 1;
|
||||
const t = fIndex - index0;
|
||||
const color0 = Color.fromArray(paletteArray, index0 * 3);
|
||||
const color1 = Color.fromArray(paletteArray, index1 * 3);
|
||||
color = Color.interpolate(color0, color1, t);
|
||||
}
|
||||
}
|
||||
|
||||
if (dOverpaint) {
|
||||
let overpaintColor: Color;
|
||||
let overpaintAlpha: number;
|
||||
switch (overpaintType) {
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
const idx = (instanceIndex * groupCount + group) * 4;
|
||||
overpaintColor = Color.fromArray(tOverpaint, idx);
|
||||
overpaintAlpha = tOverpaint[idx + 3] / 255;
|
||||
@@ -320,16 +369,24 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const transparencyType = values.dTransparencyType.ref.value;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
if (geoData.vertexMapping) {
|
||||
vertexIndex = geoData.vertexMapping[vertexIndex];
|
||||
vertexCount = values.uVertexCount.ref.value;
|
||||
} else if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
const group = isGeoTexture
|
||||
? MeshExporter.getGroup(groups!, vertexIndex)
|
||||
: values.dGeometryType.ref.value === 'spheres'
|
||||
? values.tPositionGroup!.ref.value.array[vertexIndex * 4 + 3]
|
||||
: values.aGroup!.ref.value[vertexIndex];
|
||||
|
||||
let transparency: number = 0;
|
||||
if (dTransparency) {
|
||||
switch (transparencyType) {
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
const idx = (instanceIndex * groupCount + group);
|
||||
transparency = tTransparency[idx] / 255;
|
||||
break;
|
||||
@@ -373,10 +430,133 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
private async addLineStrips(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const stripCount = values.stripCount.ref.value;
|
||||
|
||||
const stripOffsets = values.stripOffsets.ref.value;
|
||||
const aMapping = values.aMapping.ref.value;
|
||||
|
||||
if (this.options.linesAsTriangles) {
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
const radialSegments = 6;
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let s = 0; s < stripCount; ++s) {
|
||||
const stripStart = stripOffsets[s];
|
||||
const stripEnd = stripOffsets[s + 1];
|
||||
|
||||
// Collect segments for this strip (only end-side vertices)
|
||||
const segmentIndices: number[] = [];
|
||||
for (let v = stripStart; v < stripEnd; v += 2) {
|
||||
const mappingY = aMapping[v * 2 + 1];
|
||||
if (mappingY < 0) continue;
|
||||
segmentIndices.push(v);
|
||||
}
|
||||
if (segmentIndices.length === 0) continue;
|
||||
|
||||
const nPoints = segmentIndices.length + 1;
|
||||
const linearSegments = nPoints - 1;
|
||||
const curvePoints = new Float32Array(nPoints * 3);
|
||||
const curveOrigIndices: number[] = [];
|
||||
const widthValues = new Float32Array(nPoints);
|
||||
const heightValues = new Float32Array(nPoints);
|
||||
|
||||
// First point: start of first segment
|
||||
const v0 = segmentIndices[0];
|
||||
arrayCopyOffset(curvePoints, aStart, 0, v0 * 3, 3);
|
||||
curveOrigIndices.push(v0);
|
||||
const radius0 = MeshExporter.getSize(values, instanceIndex, aGroup[v0], v0) * 0.03;
|
||||
widthValues[0] = radius0;
|
||||
heightValues[0] = radius0;
|
||||
|
||||
// Subsequent points: end of each segment
|
||||
for (let j = 0; j < segmentIndices.length; ++j) {
|
||||
const v = segmentIndices[j];
|
||||
arrayCopyOffset(curvePoints, aEnd, (j + 1) * 3, v * 3, 3);
|
||||
curveOrigIndices.push(v);
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, aGroup[v], v) * 0.03;
|
||||
widthValues[j + 1] = radius;
|
||||
heightValues[j + 1] = radius;
|
||||
}
|
||||
|
||||
const normalVectors = new Float32Array(nPoints * 3);
|
||||
const binormalVectors = new Float32Array(nPoints * 3);
|
||||
computeFrenetFrames(curvePoints, normalVectors, binormalVectors, nPoints);
|
||||
|
||||
addTube(state, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, true, true, 'elliptical');
|
||||
|
||||
// Build vertex mapping
|
||||
if (instanceIndex === 0) {
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
for (let j = 0; j < radialSegments; ++j) {
|
||||
vertexMapping.push(curveOrigIndices[i]);
|
||||
}
|
||||
}
|
||||
for (let j = 0; j <= radialSegments; ++j) {
|
||||
vertexMapping.push(curveOrigIndices[0]);
|
||||
}
|
||||
for (let j = 0; j <= radialSegments; ++j) {
|
||||
vertexMapping.push(curveOrigIndices[linearSegments]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
} else {
|
||||
// Decompose strips into individual line segments
|
||||
let nLineSegments = 0;
|
||||
for (let s = 0; s < stripCount; ++s) {
|
||||
const stripStart = stripOffsets[s];
|
||||
const stripEnd = stripOffsets[s + 1];
|
||||
for (let v = stripStart; v < stripEnd; v += 2) {
|
||||
const mappingY = aMapping[v * 2 + 1];
|
||||
if (mappingY < 0) continue;
|
||||
nLineSegments++;
|
||||
}
|
||||
}
|
||||
|
||||
const vertexCount = nLineSegments * 2;
|
||||
const drawCount = nLineSegments;
|
||||
const vertices = new Float32Array(vertexCount * 3);
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
let vertexIndex = 0;
|
||||
for (let s = 0; s < stripCount; ++s) {
|
||||
const stripStart = stripOffsets[s];
|
||||
const stripEnd = stripOffsets[s + 1];
|
||||
for (let v = stripStart; v < stripEnd; v += 2) {
|
||||
const mappingY = aMapping[v * 2 + 1];
|
||||
if (mappingY < 0) continue;
|
||||
|
||||
arrayCopyOffset(vertices, aStart, vertexIndex * 3, v * 3, 3);
|
||||
vertexMapping[vertexIndex] = v;
|
||||
vertexIndex++;
|
||||
|
||||
arrayCopyOffset(vertices, aEnd, vertexIndex * 3, v * 3, 3);
|
||||
vertexMapping[vertexIndex] = v;
|
||||
vertexIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount, vertexMapping }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addLineSegments(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
|
||||
const vertexCount = (values.uVertexCount.ref.value / 4) * 2;
|
||||
const drawCount = values.drawCount.ref.value / (2 * 3);
|
||||
|
||||
@@ -391,6 +571,8 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const topCap = true;
|
||||
const bottomCap = true;
|
||||
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
@@ -398,35 +580,44 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
const group = aGroup[i / 4];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i / 4) * 0.03;
|
||||
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
} else {
|
||||
const n = vertexCount / 2;
|
||||
const vertices = new Float32Array(n * 2 * 3);
|
||||
for (let i = 0; i < n; ++i) {
|
||||
vertices[i * 6] = aStart[i * 4 * 3];
|
||||
vertices[i * 6 + 1] = aStart[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 2] = aStart[i * 4 * 3 + 2];
|
||||
|
||||
vertices[i * 6 + 3] = aEnd[i * 4 * 3];
|
||||
vertices[i * 6 + 4] = aEnd[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 5] = aEnd[i * 4 * 3 + 2];
|
||||
arrayCopyOffset(vertices, aStart, i * 6, i * 4 * 3, 3);
|
||||
arrayCopyOffset(vertices, aEnd, i * 6 + 3, i * 4 * 3, 3);
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
if (values.stripCount.ref.value !== 0) {
|
||||
await this.addLineStrips(values, webgl, ctx);
|
||||
} else {
|
||||
await this.addLineSegments(values, webgl, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
@@ -440,6 +631,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const detail = 0;
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
@@ -448,15 +640,21 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
state.currentGroup = group;
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i) * 0.03;
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addSphere(state, center, radius, detail);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
} else {
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'points', webgl, ctx });
|
||||
}
|
||||
@@ -471,7 +669,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const sphereCount = vertexCount / 6 * instanceCount;
|
||||
const sphereCount = (vertexCount / 6) * instanceCount;
|
||||
let detail: number;
|
||||
switch (this.options.primitivesQuality) {
|
||||
case 'auto':
|
||||
@@ -492,6 +690,8 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
@@ -499,15 +699,21 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group);
|
||||
state.currentGroup = group;
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i);
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addSphere(state, center, radius, detail);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
}
|
||||
|
||||
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
@@ -545,6 +751,8 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
@@ -554,7 +762,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3sub(dir, end, start);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i) * aScale[i];
|
||||
const cap = aCap[i];
|
||||
let topCap = cap === 1 || cap === 3;
|
||||
let bottomCap = cap >= 2;
|
||||
@@ -562,14 +770,20 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
[bottomCap, topCap] = [topCap, bottomCap];
|
||||
}
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
}
|
||||
|
||||
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
|
||||
@@ -107,7 +107,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
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);
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = ObjExporter.getInstance(input, instanceIndex);
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
@@ -137,7 +137,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping };
|
||||
|
||||
// color
|
||||
const quantizedColors = new Uint8Array(drawCount * 3);
|
||||
|
||||
@@ -100,7 +100,7 @@ def Material "material${materialKey}"
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
|
||||
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = UsdzExporter.getInstance(input, instanceIndex);
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = UsdzExporter.getInstance(input, instanceIndex);
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
@@ -134,7 +134,7 @@ def Material "material${materialKey}"
|
||||
StringBuilder.writeSafe(normalBuilder, ')');
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping };
|
||||
|
||||
// face
|
||||
for (let i = 0; i < drawCount; ++i) {
|
||||
|
||||
@@ -25,6 +25,7 @@ export type InteractionElementSchema =
|
||||
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
|
||||
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
|
||||
| { kind: 'water-bridge' } & InteractionElementSchemaBase
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
|
||||
|
||||
export type InteractionKind = InteractionElementSchema['kind']
|
||||
@@ -39,6 +40,7 @@ export const InteractionKinds: InteractionKind[] = [
|
||||
'weak-hydrogen-bond',
|
||||
'hydrophobic',
|
||||
'metal-coordination',
|
||||
'water-bridge',
|
||||
'covalent',
|
||||
];
|
||||
|
||||
@@ -52,6 +54,7 @@ export type InteractionInfo =
|
||||
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'hydrophobic' }
|
||||
| { kind: 'metal-coordination' }
|
||||
| { kind: 'water-bridge' }
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
|
||||
|
||||
export interface StructureInteractionElement {
|
||||
@@ -80,4 +83,5 @@ export const InteractionTypeToKind = {
|
||||
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
|
||||
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
|
||||
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
|
||||
[InteractionType.WaterBridge]: 'water-bridge' as InteractionKind,
|
||||
};
|
||||
@@ -47,6 +47,7 @@ export const InteractionVisualParams = {
|
||||
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
|
||||
'hydrophobic': visualParams({ color: Color(0x555555) }),
|
||||
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
|
||||
'water-bridge': visualParams({ color: Color(0x00CCEE), style: 'dashed' }),
|
||||
'covalent': PD.Group({
|
||||
color: PD.Color(Color(0x999999)),
|
||||
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),
|
||||
|
||||
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal file
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { VolumeStreaming } from '../../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
|
||||
import { CreateVolumeStreamingBehavior, CreateVolumeStreamingInfo, VolumeStreamingVisual } from '../../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
|
||||
import { mapObjectMap } from '../../../mol-util/object';
|
||||
import { decodeColor } from '../helpers/utils';
|
||||
import { MolstarLoadingExtension } from '../load';
|
||||
import { UpdateTarget } from '../load-generic';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
|
||||
|
||||
/** Type of `molstar_volume_streaming` custom property, used by `VolumeStreamingExtension` MVS loading extension. */
|
||||
export type MolstarVolumeStreamingCustomProp = {
|
||||
/** URL of the volume streaming server, e.g. 'https://www.ebi.ac.uk/pdbe/densities'. */
|
||||
server_url?: string,
|
||||
/** Volume streaming view type ('off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'). Default value depends on structure type (X-ray/EM). */
|
||||
view?: VolumeStreaming.ViewTypes,
|
||||
/** Customization of channel parameters. */
|
||||
channel_params?: { [name in VolumeStreaming.ChannelType]?: Partial<ChannelParams_> },
|
||||
/** List of volume streaming entries (if not specified, will be retrieved automatically based on PDB ID) */
|
||||
entries?: ReturnType<typeof CreateVolumeStreamingInfo['createDefaultParams']>['entries'],
|
||||
} | boolean | undefined;
|
||||
|
||||
|
||||
/** This MVS loading extension allows turning on volume streaming for a structure by providing custom property `molstar_volume_streaming`.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1cbs_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: true,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
*
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1tqn_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: {
|
||||
* channel_params: {
|
||||
* '2fo-fc': { color: 'skyblue', opacity: 0.3 },
|
||||
* 'fo-fc(+ve)': { color: 'greenyellow', wireframe: true, isoValue: { kind: 'relative', relativeValue: +2.5 } },
|
||||
* 'fo-fc(-ve)': { color: 'orange', wireframe: true, isoValue: { kind: 'relative', relativeValue: -2.5 } },
|
||||
* },
|
||||
* } satisfies MolstarVolumeStreamingCustomProp,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
*
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/8hra_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: {
|
||||
* server_url: 'https://www.ebi.ac.uk/pdbe/densities', // = default
|
||||
* entries: [{ dataId: 'EMD-34965', source: { name: 'em', params: { isoValue: { kind: 'absolute', absoluteValue: 0.015 } } } }],
|
||||
* view: 'auto', // default is 'auto' for EM, 'selection-box' for X-ray structures
|
||||
* channel_params: {
|
||||
* em: { color: '#ff0000', opacity: 0.4, isoValue: { kind: 'absolute', absoluteValue: 0.025 } },
|
||||
* },
|
||||
* } satisfies MolstarVolumeStreamingCustomProp,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
* ```
|
||||
*/
|
||||
export const VolumeStreamingExtension: MolstarLoadingExtension<{}> = {
|
||||
id: 'wwpdb/volume-streaming',
|
||||
description: 'Allow turning on volume streaming for a structure',
|
||||
createExtensionContext: () => ({}),
|
||||
action: (updateTarget, node, context, extContext) => {
|
||||
if (node.kind !== 'structure') return;
|
||||
let params: MolstarVolumeStreamingCustomProp = node.custom?.molstar_volume_streaming;
|
||||
if (!params) return;
|
||||
if (params === true) params = {};
|
||||
|
||||
const streamingInfo = UpdateTarget.apply(updateTarget, CreateVolumeStreamingInfo, {
|
||||
serverUrl: params.server_url,
|
||||
autoEntries: !params.entries,
|
||||
entries: params.entries,
|
||||
defaultView: params.view,
|
||||
defaultChannelParams: params.channel_params && mapObjectMap(params.channel_params, normalizeChannelParams),
|
||||
}, { state: { isCollapsed: true } });
|
||||
|
||||
const streamingBehavior = UpdateTarget.apply(streamingInfo, CreateVolumeStreamingBehavior);
|
||||
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true }, tags: '2fo-fc' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true }, tags: 'fo-fc(+ve)' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true }, tags: 'fo-fc(-ve)' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true }, tags: 'em' });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
interface ChannelParams_ extends Omit<VolumeStreaming.ChannelParams, 'color'> {
|
||||
color: ColorT | number,
|
||||
}
|
||||
|
||||
function normalizeChannelParams(p: Partial<ChannelParams_> | undefined): Partial<VolumeStreaming.ChannelParams> | undefined {
|
||||
if (!p) return undefined;
|
||||
return {
|
||||
...p,
|
||||
color: decodeColor(p.color),
|
||||
};
|
||||
}
|
||||
@@ -405,6 +405,15 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
}
|
||||
case 'putty': {
|
||||
const sizeTheme = params.size_theme ?? 'uniform';
|
||||
return {
|
||||
type: { name: 'putty', params: { alpha, sizeFactor: params.size_factor } },
|
||||
sizeTheme: sizeTheme === 'uncertainty'
|
||||
? { name: 'uncertainty', params: {} }
|
||||
: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -31,6 +31,7 @@ import { MVSTrajectoryWithCoordinates } from './components/trajectory';
|
||||
import { generateStateTransition } from './helpers/animation';
|
||||
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
|
||||
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
|
||||
import { VolumeStreamingExtension } from './load-extensions/volume-streaming';
|
||||
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, clippingForNode, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
|
||||
@@ -194,6 +195,7 @@ function molstarTreeToEntry(
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs });
|
||||
}
|
||||
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
|
||||
snapshot.structureFocus = {}; // avoid structure focus persisting through states (causes weird behaviors, e.g. when turning on Volume Streaming)
|
||||
|
||||
if (tree.custom?.molstar_on_load_markdown_commands) {
|
||||
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
|
||||
@@ -305,7 +307,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { isPdbqt: format === 'pdbqt' });
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { variant: format });
|
||||
case 'gro':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromGRO);
|
||||
case 'xyz':
|
||||
@@ -502,4 +504,5 @@ export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<Molsta
|
||||
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
|
||||
NonCovalentInteractionsExtension,
|
||||
IsHiddenCustomStateExtension,
|
||||
VolumeStreamingExtension,
|
||||
];
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { treeValidationIssues } from './tree/generic/tree-validation';
|
||||
import { treeToString } from './tree/generic/tree-utils';
|
||||
import { ajaxGet } from '../../mol-util/data-source';
|
||||
import { deepClone } from '../../mol-util/object';
|
||||
import { createMVSX } from './export';
|
||||
import { MVSAnimationSchema, MVSAnimationTree } from './tree/animation/animation-tree';
|
||||
import { findUris, replaceUris, resolveUri, treeToString, windowUrl } from './tree/generic/tree-utils';
|
||||
import { treeValidationIssues } from './tree/generic/tree-validation';
|
||||
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
|
||||
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
@@ -102,6 +105,55 @@ export const MVSData = {
|
||||
return JSON.stringify(mvsData, undefined, space);
|
||||
},
|
||||
|
||||
/** Encode `MVSData` to MVSX (MolViewSpec JSON zipped together with referenced assets). Automatically fetches all referenced assets unless specified otherwise in `options`. */
|
||||
async toMVSX(mvsData: MVSData, options: {
|
||||
/** Explicitely define assets to be included in the MVSX (binary data or string with asset content).
|
||||
* If not specified, assets will be fetched automatically. */
|
||||
assets?: { [uri: string]: Uint8Array<ArrayBuffer> | string },
|
||||
/** Base URI for resolving relative URIs (only applies if `assets` not specified). */
|
||||
baseUri?: string,
|
||||
/** Do not include external resources (i.e. absolute URIs) in the MVSX (default is to include both relative and absolute URIs) (only applies if `assets` not specified). */
|
||||
skipExternal?: boolean,
|
||||
/** Optional cache for sharing fetched assets across multiple `toMVSX` calls (only applies if `assets` not specified). */
|
||||
cache?: { [absoluteUri: string]: Uint8Array<ArrayBuffer> | string },
|
||||
} = {}): Promise<Uint8Array<ArrayBuffer>> {
|
||||
let { assets, baseUri, skipExternal, cache } = options;
|
||||
mvsData = deepClone(mvsData);
|
||||
const uriParamNames = ['uri', 'url'];
|
||||
const trees = mvsData.kind === 'multiple' ? mvsData.snapshots.map(s => s.root) : [mvsData.root];
|
||||
// Fetch assets:
|
||||
if (!assets) {
|
||||
assets = {};
|
||||
cache ??= {};
|
||||
const theWindowUrl = windowUrl();
|
||||
const uris = new Set<string>();
|
||||
for (const tree of trees) {
|
||||
findUris(tree, uriParamNames, uris);
|
||||
}
|
||||
for (const uri of uris) {
|
||||
if (skipExternal && isAbsoluteUri(uri)) continue;
|
||||
const resolvedUri = resolveUri(uri, baseUri, theWindowUrl)!;
|
||||
const content = cache[resolvedUri] ??= await ajaxGet({ url: resolvedUri, type: 'binary' }).run();
|
||||
assets[uri] = content;
|
||||
}
|
||||
}
|
||||
// Replace URIs by asset names:
|
||||
const uriMapping: Record<string, string> = {};
|
||||
const namedAssets: { name: string, content: string | Uint8Array<ArrayBuffer> }[] = [];
|
||||
let counter = 0;
|
||||
for (const uri in assets) {
|
||||
const nameHint = uri.split('/').pop()!.replace(/[^\w\.+-]/g, '_').slice(0, 64);
|
||||
const assetName = `./assets/${counter++}-${nameHint}`;
|
||||
uriMapping[uri] = assetName;
|
||||
namedAssets.push({ name: assetName, content: assets[uri] });
|
||||
}
|
||||
for (const tree of trees) {
|
||||
replaceUris(tree, uriMapping, uriParamNames);
|
||||
}
|
||||
// Zip:
|
||||
return await createMVSX(mvsData, namedAssets);
|
||||
},
|
||||
|
||||
/** Validate `MVSData`. Return `true` if OK; `false` if not OK.
|
||||
* If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */
|
||||
isValid(mvsData: MVSData, options: { noExtra?: boolean } = {}): boolean {
|
||||
@@ -207,3 +259,12 @@ function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: {
|
||||
function utcNowISO(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function isAbsoluteUri(uri: string): boolean {
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
return !!url.protocol;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -186,7 +186,7 @@ export function resolveUris<T extends Tree>(tree: T, baseUri: string, uriParamNa
|
||||
* (i.e. the last one is the base URI). Skip any `undefined`.
|
||||
* E.g. `resolveUri('./unexpected.png', '/spanish/inquisition/expectations.html', 'https://example.org/spam/spam/spam')`
|
||||
* returns `'https://example.org/spanish/inquisition/unexpected.png'`. */
|
||||
function resolveUri(...refs: (string | undefined)[]): string | undefined {
|
||||
export function resolveUri(...refs: (string | undefined)[]): string | undefined {
|
||||
let result: string | undefined = undefined;
|
||||
for (const ref of refs.reverse()) {
|
||||
if (ref !== undefined) {
|
||||
@@ -197,7 +197,43 @@ function resolveUri(...refs: (string | undefined)[]): string | undefined {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return URL of the current page when running in a browser; `undefined` when running in Node. */
|
||||
function windowUrl(): string | undefined {
|
||||
return (typeof window !== 'undefined') ? window.location.href : undefined;
|
||||
/** Gather any URI params in a tree. URI params are those listed in `uriParamNames`. */
|
||||
export function findUris<T extends Tree>(tree: T, uriParamNames: string[], out = new Set<string>()): Set<string> {
|
||||
dfs(tree, node => {
|
||||
const params = node.params as Record<string, any> | undefined;
|
||||
if (!params) return;
|
||||
for (const name of uriParamNames) {
|
||||
const uri = params[name];
|
||||
if (typeof uri === 'string') {
|
||||
out.add(uri);
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Replace any URI params in a tree using the given `uriMapping`, in place. URI params are those listed in `uriParamNames`. */
|
||||
export function replaceUris<T extends Tree>(tree: T, uriMapping: { [oldUri: string]: string }, uriParamNames: string[]): void {
|
||||
dfs(tree, node => {
|
||||
const params = node.params as Record<string, any> | undefined;
|
||||
if (!params) return;
|
||||
for (const name of uriParamNames) {
|
||||
const oldUri = params[name];
|
||||
if (typeof oldUri === 'string' && typeof uriMapping[oldUri] === 'string') {
|
||||
params[name] = uriMapping[oldUri];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Return URL of the current page when running in a browser; or file:// URL of the current working directory when running in Node. */
|
||||
export function windowUrl(): string | undefined {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.location.href;
|
||||
}
|
||||
if (typeof process !== 'undefined') {
|
||||
const cwd = process.cwd().replace(/\/?$/, '/');
|
||||
return `file://${cwd}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Zachary Charlop-Powers <zach.charlop.powers@gmail.com>
|
||||
*/
|
||||
|
||||
import { MVSData } from '../../../mvs-data';
|
||||
@@ -17,6 +18,42 @@ describe('mvs-builder', () => {
|
||||
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('putty representation works', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure()
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'putty' });
|
||||
const state = builder.getState();
|
||||
expect(MVSData.validationIssues(state)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('putty representation works with uniform size_theme', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure()
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'putty', size_theme: 'uniform', size_factor: 0.5 });
|
||||
const state = builder.getState();
|
||||
expect(MVSData.validationIssues(state)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('putty representation works with uncertainty size_theme', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure()
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'putty', size_theme: 'uncertainty' });
|
||||
const state = builder.getState();
|
||||
expect(MVSData.validationIssues(state)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('volume builder works', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
|
||||
@@ -46,6 +46,13 @@ const Carbohydrate = {
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
};
|
||||
|
||||
const Putty = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls how the tube radius is determined. */
|
||||
size_theme: OptionalField(literal('uniform', 'uncertainty'), 'uniform', "Controls how the tube radius is determined. 'uniform' uses a constant radius scaled by size_factor. 'uncertainty' drives the radius from per-residue B-factor/RMSF values."),
|
||||
};
|
||||
|
||||
const Surface = {
|
||||
/** Type of surface representation. (Default is 'molecular') */
|
||||
surface_type: OptionalField(literal('molecular', 'gaussian'), 'molecular', `Type of surface representation. (Default is 'molecular')`),
|
||||
@@ -66,6 +73,7 @@ export const MVSRepresentationParams = UnionParamsSchema(
|
||||
spacefill: SimpleParamsSchema(Spacefill),
|
||||
carbohydrate: SimpleParamsSchema(Carbohydrate),
|
||||
surface: SimpleParamsSchema(Surface),
|
||||
putty: SimpleParamsSchema(Putty),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Viewport, cameraProject, cameraUnproject } from './camera/util';
|
||||
import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { Scene } from '../mol-gl/scene';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
|
||||
@@ -15,6 +15,7 @@ import { Mat4 } from '../mol-math/linear-algebra/3d/mat4';
|
||||
import { Vec4 } from '../mol-math/linear-algebra/3d/vec4';
|
||||
import { Vec3 } from '../mol-math/linear-algebra/3d/vec3';
|
||||
import { EPSILON } from '../mol-math/linear-algebra/3d/common';
|
||||
import { Euler } from '../mol-math/linear-algebra/3d/euler';
|
||||
|
||||
export type { ICamera };
|
||||
|
||||
@@ -42,6 +43,12 @@ interface ICamera {
|
||||
}
|
||||
|
||||
const tmpClip = Vec4();
|
||||
const tmpForward = Vec3();
|
||||
const tmpRight = Vec3();
|
||||
const tmpUp = Vec3();
|
||||
const tmpBack = Vec3();
|
||||
const tmpDelta = Vec3();
|
||||
const tmpRotMat = Mat4.identity();
|
||||
|
||||
export class Camera implements ICamera {
|
||||
readonly view: Mat4 = Mat4.identity();
|
||||
@@ -70,6 +77,8 @@ export class Camera implements ICamera {
|
||||
|
||||
readonly transition: CameraTransitionManager = new CameraTransitionManager(this);
|
||||
readonly stateChanged = new BehaviorSubject<Partial<Camera.Snapshot>>(this.state);
|
||||
/** Fires whenever update() produces a changed view/projection (covers all mutations, including direct ones from controls). */
|
||||
readonly changed = new Subject<void>();
|
||||
|
||||
get position() { return this.state.position; }
|
||||
set position(v: Vec3) { Vec3.copy(this.state.position, v); }
|
||||
@@ -123,6 +132,7 @@ export class Camera implements ICamera {
|
||||
|
||||
Mat4.copy(this.prevView, this.view);
|
||||
Mat4.copy(this.prevProjection, this.projection);
|
||||
this.changed.next();
|
||||
}
|
||||
|
||||
return changed;
|
||||
@@ -237,6 +247,57 @@ export class Camera implements ICamera {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** How much the camera is rotated around its target. Uses 'ZYX' order. */
|
||||
getRotation(out: Euler) {
|
||||
const { position, target, up } = this.state;
|
||||
Vec3.normalize(tmpForward, Vec3.sub(tmpForward, target, position));
|
||||
Vec3.normalize(tmpRight, Vec3.cross(tmpRight, tmpForward, up));
|
||||
Vec3.cross(tmpUp, tmpRight, tmpForward);
|
||||
|
||||
Mat4.setIdentity(tmpRotMat);
|
||||
tmpRotMat[0] = tmpRight[0]; tmpRotMat[1] = tmpRight[1]; tmpRotMat[2] = tmpRight[2];
|
||||
tmpRotMat[4] = tmpUp[0]; tmpRotMat[5] = tmpUp[1]; tmpRotMat[6] = tmpUp[2];
|
||||
tmpRotMat[8] = -tmpForward[0]; tmpRotMat[9] = -tmpForward[1]; tmpRotMat[10] = -tmpForward[2];
|
||||
|
||||
return Euler.fromMat4(out, tmpRotMat, 'ZYX');
|
||||
}
|
||||
|
||||
/** Set the camera rotation around its target. Expects 'ZYX' order. */
|
||||
setRotation(rotation: Euler, durationMs?: number) {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
const distance = Vec3.distance(snapshot.position, snapshot.target);
|
||||
|
||||
Mat4.fromEuler(tmpRotMat, rotation, 'ZYX');
|
||||
|
||||
// back = R * (0,0,1) → column 2 of R
|
||||
Vec3.set(tmpBack, tmpRotMat[8], tmpRotMat[9], tmpRotMat[10]);
|
||||
// up = R * (0,1,0) → column 1 of R
|
||||
Vec3.set(tmpUp, tmpRotMat[4], tmpRotMat[5], tmpRotMat[6]);
|
||||
|
||||
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
|
||||
Vec3.scaleAndAdd(state.position, snapshot.target, tmpBack, distance);
|
||||
Vec3.copy(state.up, tmpUp);
|
||||
|
||||
this.setState(state, durationMs);
|
||||
}
|
||||
|
||||
/** Translation of the camera target relative to world origin (0, 0, 0) */
|
||||
getTranslation(out: Vec3) {
|
||||
return Vec3.copy(out, this.state.target);
|
||||
}
|
||||
|
||||
/** Set the camera target to the given translation, moving position by the same delta so orientation/distance are preserved */
|
||||
setTranslation(translation: Vec3, durationMs?: number) {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
Vec3.sub(tmpDelta, translation, snapshot.target);
|
||||
|
||||
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
|
||||
Vec3.add(state.position, snapshot.position, tmpDelta);
|
||||
Vec3.copy(state.target, translation);
|
||||
|
||||
this.setState(state, durationMs);
|
||||
}
|
||||
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {
|
||||
this.viewport = viewport;
|
||||
Camera.copySnapshot(this.state, state);
|
||||
|
||||
@@ -23,7 +23,7 @@ import { MarkerAction } from '../mol-util/marker-action';
|
||||
import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
|
||||
import { Camera } from './camera';
|
||||
import { ParamDefinition as PD } from '../mol-util/param-definition';
|
||||
import { DebugHelperParams } from './helper/bounding-sphere-helper';
|
||||
import { DebugRegistry } from './helper/debug-registry';
|
||||
import { SetUtils } from '../mol-util/set';
|
||||
import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
|
||||
import { PostprocessingParams } from './passes/postprocessing';
|
||||
@@ -109,7 +109,6 @@ export const Canvas3DParams = {
|
||||
renderer: PD.Group(RendererParams),
|
||||
trackball: PD.Group(TrackballControlsParams),
|
||||
interaction: PD.Group(Canvas3dInteractionHelperParams),
|
||||
debug: PD.Group(DebugHelperParams),
|
||||
handle: PD.Group(HandleHelperParams),
|
||||
pointer: PD.Group(PointerHelperParams),
|
||||
xr: PD.Group(XRManagerParams, { label: 'XR' }),
|
||||
@@ -388,6 +387,8 @@ interface Canvas3D {
|
||||
readonly stats: RendererStats
|
||||
readonly interaction: Canvas3dInteractionHelper['events']
|
||||
|
||||
readonly debugRegistry: DebugRegistry
|
||||
|
||||
readonly xr: {
|
||||
request(): Promise<void>
|
||||
end(): Promise<void>
|
||||
@@ -674,7 +675,8 @@ namespace Canvas3D {
|
||||
const xrChanged = xrManager.update(xrFrame);
|
||||
if (!xrChanged && xrFrame) return false;
|
||||
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged;
|
||||
const activeAnimation = renderer.props.enableAnimation && scene.hasAnimation;
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged || activeAnimation;
|
||||
forceNextRender = false;
|
||||
|
||||
if (passes.illumination.supported && p.illumination.enabled && !xrFrame) {
|
||||
@@ -753,6 +755,7 @@ namespace Canvas3D {
|
||||
if (webgl.xr.session && !options?.xrFrame) return;
|
||||
|
||||
currentTime = t;
|
||||
renderer.setTime((currentTime - startTime) / 1000);
|
||||
commit(options?.isSynchronous);
|
||||
|
||||
// update the controler before the camera transition
|
||||
@@ -1078,7 +1081,6 @@ namespace Canvas3D {
|
||||
renderer: { ...renderer.props },
|
||||
trackball: { ...controls.props },
|
||||
interaction: { ...interactionHelper.props },
|
||||
debug: { ...helper.debug.props },
|
||||
handle: { ...helper.handle.props },
|
||||
pointer: { ...helper.pointer.props },
|
||||
xr: { ...xrManager.props },
|
||||
@@ -1348,7 +1350,6 @@ namespace Canvas3D {
|
||||
}
|
||||
if (props.trackball) controls.setProps(props.trackball);
|
||||
if (props.interaction) interactionHelper.setProps(props.interaction);
|
||||
if (props.debug) helper.debug.setProps(props.debug);
|
||||
if (props.handle) helper.handle.setProps(props.handle);
|
||||
if (props.pointer) helper.pointer.setProps(props.pointer);
|
||||
if (props.xr) xrManager.setProps(props.xr);
|
||||
@@ -1399,6 +1400,7 @@ namespace Canvas3D {
|
||||
get interaction() {
|
||||
return interactionHelper.events;
|
||||
},
|
||||
debugRegistry: helper.debug,
|
||||
xr,
|
||||
dispose: () => {
|
||||
contextLostSub?.unsubscribe();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 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>
|
||||
@@ -76,11 +76,13 @@ export const TrackballControlsParams = {
|
||||
off: PD.EmptyGroup(),
|
||||
spin: PD.Group({
|
||||
speed: PD.Numeric(0.1, { min: -2, max: 2, step: 0.01 }, { description: 'Number of rotations per second' }),
|
||||
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
|
||||
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
|
||||
}, { description: 'Spin the 3D scene around an axis in camera space' }),
|
||||
rock: PD.Group({
|
||||
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }, { description: 'Number of oscilations per second' }),
|
||||
angle: PD.Numeric(10, { min: 0, max: 90, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
|
||||
}, { description: 'Rock the 3D scene around the x-axis in view space' })
|
||||
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
|
||||
}, { description: 'Rock the 3D scene around an axis in camera space' })
|
||||
}),
|
||||
|
||||
staticMoving: PD.Boolean(true, { isHidden: true }),
|
||||
@@ -855,27 +857,67 @@ namespace TrackballControls {
|
||||
leaveSub.unsubscribe();
|
||||
}
|
||||
|
||||
const _spinSpeed = Vec2.create(0.005, 0);
|
||||
const _animateQuat = Quat();
|
||||
const _animateAxis = Vec3();
|
||||
const _animateUp = Vec3();
|
||||
const _animateSide = Vec3();
|
||||
const _animateDir = Vec3();
|
||||
function spin(deltaT: number) {
|
||||
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
|
||||
|
||||
const radPerMs = 2 * Math.PI * p.animate.params.speed / 1000;
|
||||
_spinSpeed[0] = deltaT * radPerMs / getRotateFactor();
|
||||
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
||||
const angle = deltaT * radPerMs;
|
||||
|
||||
// Transform axis from camera space to world space
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
Vec3.normalize(_animateDir, _eye); // Z-axis (view direction)
|
||||
Vec3.normalize(_animateUp, camera.up); // Y-axis (up)
|
||||
Vec3.cross(_animateSide, _animateUp, _animateDir); // X-axis (right)
|
||||
Vec3.normalize(_animateSide, _animateSide);
|
||||
|
||||
const axis = p.animate.params.axis;
|
||||
Vec3.set(_animateAxis,
|
||||
axis[0] * _animateSide[0] + axis[1] * _animateUp[0] + axis[2] * _animateDir[0],
|
||||
axis[0] * _animateSide[1] + axis[1] * _animateUp[1] + axis[2] * _animateDir[1],
|
||||
axis[0] * _animateSide[2] + axis[1] * _animateUp[2] + axis[2] * _animateDir[2]
|
||||
);
|
||||
Vec3.normalize(_animateAxis, _animateAxis);
|
||||
|
||||
Quat.setAxisAngle(_animateQuat, _animateAxis, angle);
|
||||
Vec3.transformQuat(_eye, _eye, _animateQuat);
|
||||
Vec3.transformQuat(camera.up, camera.up, _animateQuat);
|
||||
Vec3.add(camera.position, camera.target, _eye);
|
||||
}
|
||||
|
||||
let _rockPhase = 0;
|
||||
const _rockSpeed = Vec2.create(0.005, 0);
|
||||
function rock(deltaT: number) {
|
||||
if (p.animate.name !== 'rock' || p.animate.params.speed === 0 || _isInteracting) return;
|
||||
|
||||
const dt = deltaT / 1000 * p.animate.params.speed;
|
||||
const maxAngle = degToRad(p.animate.params.angle) / getRotateFactor();
|
||||
const maxAngle = degToRad(p.animate.params.angle);
|
||||
const angleA = Math.sin(_rockPhase * Math.PI * 2) * maxAngle;
|
||||
const angleB = Math.sin((_rockPhase + dt) * Math.PI * 2) * maxAngle;
|
||||
const angle = angleB - angleA;
|
||||
|
||||
_rockSpeed[0] = angleB - angleA;
|
||||
Vec2.add(_rotCurr, _rotPrev, _rockSpeed);
|
||||
// Transform axis from camera space to world space
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
Vec3.normalize(_animateDir, _eye); // Z-axis (view direction)
|
||||
Vec3.normalize(_animateUp, camera.up); // Y-axis (up)
|
||||
Vec3.cross(_animateSide, _animateUp, _animateDir); // X-axis (right)
|
||||
Vec3.normalize(_animateSide, _animateSide);
|
||||
|
||||
const axis = p.animate.params.axis;
|
||||
Vec3.set(_animateAxis,
|
||||
axis[0] * _animateSide[0] + axis[1] * _animateUp[0] + axis[2] * _animateDir[0],
|
||||
axis[0] * _animateSide[1] + axis[1] * _animateUp[1] + axis[2] * _animateDir[1],
|
||||
axis[0] * _animateSide[2] + axis[1] * _animateUp[2] + axis[2] * _animateDir[2]
|
||||
);
|
||||
Vec3.normalize(_animateAxis, _animateAxis);
|
||||
|
||||
Quat.setAxisAngle(_animateQuat, _animateAxis, angle);
|
||||
Vec3.transformQuat(_eye, _eye, _animateQuat);
|
||||
Vec3.transformQuat(camera.up, camera.up, _animateQuat);
|
||||
Vec3.add(camera.position, camera.target, _eye);
|
||||
|
||||
_rockPhase += dt;
|
||||
if (_rockPhase >= 1) {
|
||||
|
||||
85
src/mol-canvas3d/helper/debug-registry.ts
Normal file
85
src/mol-canvas3d/helper/debug-registry.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { isDebugMode } from '../../mol-util/debug';
|
||||
|
||||
export interface DebugHelper<T extends {} = {}> {
|
||||
readonly scene: Scene;
|
||||
update(): void;
|
||||
syncVisibility(): void;
|
||||
clear(): void;
|
||||
readonly isEnabled: boolean;
|
||||
readonly props: T;
|
||||
setProps(props: Partial<T>): void;
|
||||
}
|
||||
|
||||
export class DebugRegistry {
|
||||
readonly ctx: WebGLContext;
|
||||
readonly parent: Scene;
|
||||
|
||||
private readonly entries = new Map<string, DebugHelper>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene) {
|
||||
this.ctx = ctx;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
register<T extends {}>(name: string, entry: DebugHelper<T>) {
|
||||
if (this.entries.has(name)) {
|
||||
if (isDebugMode) {
|
||||
console.warn(`Debug helper with name '${name}' already exists, replacing.`);
|
||||
}
|
||||
this.entries.get(name)!.clear();
|
||||
}
|
||||
this.entries.set(name, entry);
|
||||
}
|
||||
|
||||
unregister(name: string) {
|
||||
const entry = this.entries.get(name);
|
||||
if (entry) {
|
||||
entry.clear();
|
||||
this.entries.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
get scenes(): Scene[] {
|
||||
return Array.from(this.entries.values()).map(e => e.scene);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.entries.forEach(entry => {
|
||||
if (entry.isEnabled) entry.update();
|
||||
});
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
this.entries.forEach(entry => {
|
||||
entry.syncVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.entries.forEach(entry => {
|
||||
entry.clear();
|
||||
});
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
let enabled = false;
|
||||
this.entries.forEach(entry => {
|
||||
if (entry.isEnabled) enabled = true;
|
||||
});
|
||||
return enabled;
|
||||
}
|
||||
|
||||
setProps<T extends {}>(props: Partial<T>) {
|
||||
this.entries.forEach(entry => {
|
||||
entry.setProps(props);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,13 +7,12 @@
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { BoundingSphereHelper, DebugHelperParams } from './bounding-sphere-helper';
|
||||
import { DebugRegistry } from './debug-registry';
|
||||
import { CameraHelper, CameraHelperParams } from './camera-helper';
|
||||
import { HandleHelper, HandleHelperParams } from './handle-helper';
|
||||
import { PointerHelper, PointerHelperParams } from './pointer-helper';
|
||||
|
||||
export const HelperParams = {
|
||||
debug: PD.Group(DebugHelperParams),
|
||||
camera: PD.Group({
|
||||
helper: PD.Group(CameraHelperParams)
|
||||
}),
|
||||
@@ -25,7 +24,7 @@ export type HelperProps = PD.Values<typeof HelperParams>
|
||||
|
||||
|
||||
export class Helper {
|
||||
readonly debug: BoundingSphereHelper;
|
||||
readonly debug: DebugRegistry;
|
||||
readonly camera: CameraHelper;
|
||||
readonly handle: HandleHelper;
|
||||
readonly pointer: PointerHelper;
|
||||
@@ -33,7 +32,7 @@ export class Helper {
|
||||
constructor(webgl: WebGLContext, scene: Scene, props: Partial<HelperProps> = {}) {
|
||||
const p = { ...DefaultHelperProps, ...props };
|
||||
|
||||
this.debug = new BoundingSphereHelper(webgl, scene, p.debug);
|
||||
this.debug = new DebugRegistry(webgl, scene);
|
||||
this.camera = new CameraHelper(webgl, p.camera.helper);
|
||||
this.handle = new HandleHelper(webgl, p.handle);
|
||||
this.pointer = new PointerHelper(webgl, p.pointer);
|
||||
|
||||
@@ -121,7 +121,7 @@ export class PointerHelper {
|
||||
|
||||
this.camera = new Camera();
|
||||
|
||||
this.shape = getPointerMeshShape(this.getData(), this.props, this.shape);
|
||||
this.shape = getPointerMeshShape(this.getData(), this.props);
|
||||
this.renderObject = createMeshRenderObject(this.shape, this.props);
|
||||
this.scene.add(this.renderObject);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -23,6 +23,7 @@ import { Canvas3dInteractionHelper } from './interaction-events';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { cameraProject } from '../camera/util';
|
||||
import { Binding } from '../../mol-util/binding';
|
||||
import { isDebugMode } from '../../mol-util/debug';
|
||||
|
||||
const B = ButtonsType;
|
||||
const Trigger = Binding.Trigger;
|
||||
@@ -281,15 +282,27 @@ export class XRManager {
|
||||
}
|
||||
|
||||
private checkSupported = async () => {
|
||||
if (!navigator.xr) return false;
|
||||
if (!navigator.xr) {
|
||||
this.isSupported.next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const [arSupported, vrSupported] = await Promise.all([
|
||||
navigator.xr.isSessionSupported('immersive-ar'),
|
||||
navigator.xr.isSessionSupported('immersive-vr'),
|
||||
]);
|
||||
this.isSupported.next(arSupported || vrSupported);
|
||||
try {
|
||||
const [arSupported, vrSupported] = await Promise.all([
|
||||
navigator.xr.isSessionSupported('immersive-ar'),
|
||||
navigator.xr.isSessionSupported('immersive-vr'),
|
||||
]);
|
||||
this.isSupported.next(arSupported || vrSupported);
|
||||
} catch (e) {
|
||||
if (isDebugMode) console.warn(e);
|
||||
this.isSupported.next(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This may fail due to permissions policy, device capabilities, etc.
|
||||
* Always wrap calls to it in a try/catch block to handle errors.
|
||||
*/
|
||||
async request() {
|
||||
if (!navigator.xr) return;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -446,18 +446,28 @@ export class DrawPass {
|
||||
target.bind();
|
||||
}
|
||||
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
renderer.renderBlended(helper.debug.scene, camera);
|
||||
if (helper.debug.isEnabled || helper.pointer.isEnabled) {
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.attachFramebuffer(target.framebuffer, 'depth');
|
||||
}
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
for (const scene of helper.debug.scenes) {
|
||||
renderer.renderBlended(scene, camera);
|
||||
}
|
||||
}
|
||||
if (helper.pointer.isEnabled) {
|
||||
helper.pointer.setCamera(camera);
|
||||
renderer.update(helper.pointer.camera, helper.pointer.scene);
|
||||
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
|
||||
}
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.detachFramebuffer(target.framebuffer, 'depth');
|
||||
}
|
||||
}
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera);
|
||||
}
|
||||
if (helper.pointer.isEnabled) {
|
||||
helper.pointer.setCamera(camera);
|
||||
renderer.update(helper.pointer.camera, helper.pointer.scene);
|
||||
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera, helper.camera.scene);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2024-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -329,6 +329,31 @@ export class IlluminationPass {
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera, scene);
|
||||
this.renderInput(renderer, camera, scene, props);
|
||||
|
||||
this.transparentTarget.bind();
|
||||
if (helper.debug.isEnabled || helper.pointer.isEnabled) {
|
||||
this.drawPass.depthTextureOpaque.attachFramebuffer(this.transparentTarget.framebuffer, 'depth');
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
for (const scene of helper.debug.scenes) {
|
||||
renderer.renderBlended(scene, camera);
|
||||
}
|
||||
}
|
||||
if (helper.pointer.isEnabled) {
|
||||
helper.pointer.setCamera(camera);
|
||||
renderer.update(helper.pointer.camera, helper.pointer.scene);
|
||||
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
|
||||
}
|
||||
this.drawPass.depthTextureOpaque.detachFramebuffer(this.transparentTarget.framebuffer, 'depth');
|
||||
}
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera, helper.camera.scene);
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
|
||||
}
|
||||
}
|
||||
|
||||
state.disable(gl.BLEND);
|
||||
@@ -433,19 +458,6 @@ export class IlluminationPass {
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera, scene);
|
||||
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
renderer.renderBlended(helper.debug.scene, camera);
|
||||
}
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera, helper.camera.scene);
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
let targetIsDrawingbuffer = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -484,27 +484,45 @@ export class SsaoPass {
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
|
||||
}
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
if (multiScale) {
|
||||
// half-resolution viewport (matches dimensions of depthHalfTarget*)
|
||||
const hsx = Math.floor(sx * 0.5);
|
||||
const hsy = Math.floor(sy * 0.5);
|
||||
const hsw = Math.ceil(sw * 0.5);
|
||||
const hsh = Math.ceil(sh * 0.5);
|
||||
state.viewport(hsx, hsy, hsw, hsh);
|
||||
state.scissor(hsx, hsy, hsw, hsh);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
this.depthHalfTargetOpaque.bind();
|
||||
this.depthHalfRenderableOpaque.render();
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthHalfTargetTransparent.bind();
|
||||
this.depthHalfRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
if (includeTransparent) {
|
||||
this.depthHalfTargetTransparent.bind();
|
||||
this.depthHalfRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
if (multiScale) {
|
||||
// quarter-resolution viewport (matches dimensions of depthQuarterTarget*)
|
||||
const qsx = Math.floor(sx * 0.25);
|
||||
const qsy = Math.floor(sy * 0.25);
|
||||
const qsw = Math.ceil(sw * 0.25);
|
||||
const qsh = Math.ceil(sh * 0.25);
|
||||
state.viewport(qsx, qsy, qsw, qsh);
|
||||
state.scissor(qsx, qsy, qsw, qsh);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
this.depthQuarterTargetOpaque.bind();
|
||||
this.depthQuarterRenderableOpaque.render();
|
||||
if (includeTransparent) {
|
||||
this.depthQuarterTargetTransparent.bind();
|
||||
this.depthQuarterRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
// restore full-scale viewport for SSAO + blur passes
|
||||
state.viewport(sx, sy, sw, sh);
|
||||
state.scissor(sx, sy, sw, sh);
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthQuarterTargetTransparent.bind();
|
||||
this.depthQuarterRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.opaque');
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
|
||||
|
||||
@@ -577,3 +577,25 @@ export class PCG {
|
||||
return this.int() / 0x100000000;
|
||||
}
|
||||
}
|
||||
|
||||
export function mortonOrder3d(x: number, y: number, z: number): number {
|
||||
let out = 0;
|
||||
for (let i = 0; i < 21; ++i) {
|
||||
out |= ((x >> i) & 1) << (3 * i + 2);
|
||||
out |= ((y >> i) & 1) << (3 * i + 1);
|
||||
out |= ((z >> i) & 1) << (3 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function invertMortonOrder3d(code: number): [number, number, number] {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let z = 0;
|
||||
for (let i = 0; i < 21; ++i) {
|
||||
x |= ((code >> (3 * i + 2)) & 1) << i;
|
||||
y |= ((code >> (3 * i + 1)) & 1) << i;
|
||||
z |= ((code >> (3 * i)) & 1) << i;
|
||||
}
|
||||
return [x, y, z];
|
||||
}
|
||||
|
||||
64
src/mol-geo/geometry/animation.ts
Normal file
64
src/mol-geo/geometry/animation.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
export type AnimationData = {
|
||||
uWiggleSpeed: ValueCell<number>,
|
||||
uWiggleAmplitude: ValueCell<number>,
|
||||
uWiggleFrequency: ValueCell<number>,
|
||||
uWiggleMode: ValueCell<number>,
|
||||
uTumbleSpeed: ValueCell<number>,
|
||||
uTumbleAmplitude: ValueCell<number>,
|
||||
uTumbleFrequency: ValueCell<number>,
|
||||
}
|
||||
|
||||
export function getAnimationParam() {
|
||||
return PD.Group({
|
||||
wiggleMode: PD.Select('position', [['position', 'Position'], ['group', 'Group']] as const, { description: 'Noise seeding mode. Position: spatially correlated (nearby atoms move together). Group: per-group independent noise.' }),
|
||||
wiggleSpeed: PD.Numeric(7, { min: 0, max: 10, step: 0.1 }, { description: 'Speed of vertex wiggle animation.' }),
|
||||
wiggleAmplitude: PD.Numeric(0, { min: 0, max: 5, step: 0.01 }, { description: 'Amplitude of vertex wiggle animation.' }),
|
||||
wiggleFrequency: PD.Numeric(0.2, { min: 0.01, max: 2, step: 0.01 }, { description: 'Spatial frequency of vertex wiggle noise (position mode). Lower values correlate nearby atoms more.' }),
|
||||
tumbleSpeed: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, { description: 'Speed of instance tumble animation.' }),
|
||||
tumbleAmplitude: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, { description: 'Amplitude of instance tumble animation. In Ångströms of implied surface displacement.' }),
|
||||
tumbleFrequency: PD.Numeric(0.2, { min: 0, max: 2, step: 0.01 }, { description: 'Spatial frequency multiplier for tumble noise.' }),
|
||||
});
|
||||
}
|
||||
export type AnimationParam = ReturnType<typeof getAnimationParam>
|
||||
export type AnimationProps = AnimationParam['defaultValue'];
|
||||
|
||||
export function areAnimationPropsEqual(a: AnimationProps, b: AnimationProps): boolean {
|
||||
return a.wiggleMode === b.wiggleMode
|
||||
&& a.wiggleSpeed === b.wiggleSpeed
|
||||
&& a.wiggleAmplitude === b.wiggleAmplitude
|
||||
&& a.wiggleFrequency === b.wiggleFrequency
|
||||
&& a.tumbleSpeed === b.tumbleSpeed
|
||||
&& a.tumbleAmplitude === b.tumbleAmplitude
|
||||
&& a.tumbleFrequency === b.tumbleFrequency;
|
||||
}
|
||||
|
||||
export function createAnimationValues(props: AnimationProps) {
|
||||
return {
|
||||
uWiggleSpeed: ValueCell.create(props.wiggleSpeed),
|
||||
uWiggleAmplitude: ValueCell.create(props.wiggleAmplitude),
|
||||
uWiggleFrequency: ValueCell.create(props.wiggleFrequency),
|
||||
uWiggleMode: ValueCell.create(props.wiggleMode === 'position' ? 0 : 1),
|
||||
uTumbleSpeed: ValueCell.create(props.tumbleSpeed),
|
||||
uTumbleAmplitude: ValueCell.create(props.tumbleAmplitude),
|
||||
uTumbleFrequency: ValueCell.create(props.tumbleFrequency),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAnimationValues(values: AnimationData, props: AnimationProps) {
|
||||
ValueCell.updateIfChanged(values.uWiggleSpeed, props.wiggleSpeed);
|
||||
ValueCell.updateIfChanged(values.uWiggleAmplitude, props.wiggleAmplitude);
|
||||
ValueCell.updateIfChanged(values.uWiggleFrequency, props.wiggleFrequency);
|
||||
ValueCell.updateIfChanged(values.uWiggleMode, props.wiggleMode === 'position' ? 0 : 1);
|
||||
ValueCell.updateIfChanged(values.uTumbleSpeed, props.tumbleSpeed);
|
||||
ValueCell.updateIfChanged(values.uTumbleAmplitude, props.tumbleAmplitude);
|
||||
ValueCell.updateIfChanged(values.uTumbleFrequency, props.tumbleFrequency);
|
||||
}
|
||||
@@ -72,6 +72,25 @@ export function getColorSmoothingProps(smoothColors: PD.Values<ColorSmoothingPar
|
||||
|
||||
//
|
||||
|
||||
export type InstanceGranularityValue = true | false | 'auto'
|
||||
export const InstanceGranularityOptions: [InstanceGranularityValue, string][] = [[true, 'On'], [false, 'Off'], ['auto', 'Auto']];
|
||||
|
||||
/**
|
||||
* Threshold (in `groupCount * instanceCount`, e.g. number of marker-texture
|
||||
* slots) above which `instanceGranularity: 'auto'` resolves to `true`.
|
||||
*/
|
||||
export const AutoInstanceGranularityThreshold = 50_000_000;
|
||||
|
||||
/**
|
||||
* Resolves the `instanceGranularity` param value to a boolean.
|
||||
*/
|
||||
export function resolveInstanceGranularity(value: InstanceGranularityValue, groupCount: number, instanceCount: number): boolean {
|
||||
if (value === 'auto') return groupCount * instanceCount > AutoInstanceGranularityThreshold;
|
||||
return value;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export namespace BaseGeometry {
|
||||
export const MaterialCategory: PD.Info = { category: 'Material' };
|
||||
export const ShadingCategory: PD.Info = { category: 'Shading' };
|
||||
@@ -88,7 +107,7 @@ export namespace BaseGeometry {
|
||||
clip: PD.Group(Clip.Params),
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
density: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }, { description: 'Density value to estimate object thickness.' }),
|
||||
instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
|
||||
instanceGranularity: PD.Select<InstanceGranularityValue>('auto', InstanceGranularityOptions, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory. When set to `auto`, granularity is enabled if `groupCount * instanceCount` exceeds `AutoInstanceGranularityThreshold`.' }),
|
||||
lod: PD.Vec3(Vec3(), undefined, { ...CullingLodCategory, description: 'Level of detail.', fieldLabels: { x: 'Min Distance', y: 'Max Distance', z: 'Overlap (Shader)' } }),
|
||||
cellSize: PD.Numeric(200, { min: 0, max: 5000, step: 100 }, { ...CullingLodCategory, description: 'Instance grid cell size.' }),
|
||||
batchSize: PD.Numeric(2000, { min: 0, max: 50000, step: 500 }, { ...CullingLodCategory, description: 'Instance grid batch size.' }),
|
||||
@@ -130,7 +149,7 @@ export namespace BaseGeometry {
|
||||
uClipObjectScale: ValueCell.create(clip.objects.scale),
|
||||
uClipObjectTransform: ValueCell.create(clip.objects.transform),
|
||||
|
||||
instanceGranularity: ValueCell.create(props.instanceGranularity),
|
||||
instanceGranularity: ValueCell.create(resolveInstanceGranularity(props.instanceGranularity, counts.groupCount, counts.instanceCount)),
|
||||
uLod: ValueCell.create(Vec4.create(props.lod[0], props.lod[1], props.lod[2], 0)),
|
||||
};
|
||||
}
|
||||
@@ -153,7 +172,7 @@ export namespace BaseGeometry {
|
||||
ValueCell.update(values.uClipObjectScale, clip.objects.scale);
|
||||
ValueCell.update(values.uClipObjectTransform, clip.objects.transform);
|
||||
|
||||
ValueCell.updateIfChanged(values.instanceGranularity, props.instanceGranularity);
|
||||
ValueCell.updateIfChanged(values.instanceGranularity, resolveInstanceGranularity(props.instanceGranularity, values.uGroupCount.ref.value, values.instanceCount.ref.value));
|
||||
ValueCell.update(values.uLod, Vec4.set(values.uLod.ref.value, props.lod[0], props.lod[1], props.lod[2], 0));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -28,7 +28,9 @@ import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { getInteriorParam, updateInteriorValues, createInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface Cylinders {
|
||||
readonly kind: 'cylinders',
|
||||
@@ -180,6 +182,7 @@ export namespace Cylinders {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
colorMode: PD.Select('default', PD.arrayToOptions(['default', 'interpolate'] as const), BaseGeometry.ShadingCategory)
|
||||
};
|
||||
export type Params = typeof Params
|
||||
@@ -221,8 +224,8 @@ export namespace Cylinders {
|
||||
const positionIt = createPositionIterator(cylinders, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -230,6 +233,7 @@ export namespace Cylinders {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
|
||||
|
||||
@@ -258,6 +262,7 @@ export namespace Cylinders {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
padding: ValueCell.create(padding),
|
||||
@@ -272,9 +277,10 @@ export namespace Cylinders {
|
||||
dSolidInterior: ValueCell.create(props.solidInterior),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
dDualColor: ValueCell.create(props.colorMode === 'interpolate'),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,9 +301,9 @@ export namespace Cylinders {
|
||||
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
ValueCell.updateIfChanged(values.dDualColor, props.colorMode === 'interpolate');
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: CylindersValues, cylinders: Cylinders) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Box } from '../../primitive/box';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createColors } from '../color-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createMarkers } from '../marker-data';
|
||||
@@ -29,6 +29,7 @@ import { createEmptyClipping } from '../clipping-data';
|
||||
import { Grid } from '../../../mol-model/volume';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
|
||||
const VolumeBox = Box();
|
||||
|
||||
@@ -227,7 +228,7 @@ export namespace DirectVolume {
|
||||
const positionIt = createPositionIterator(directVolume, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -235,6 +236,7 @@ export namespace DirectVolume {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const [x, y, z] = gridDimension.ref.value;
|
||||
const counts = { drawCount: VolumeBox.indices.length, vertexCount: x * y * z, groupCount, instanceCount };
|
||||
@@ -255,6 +257,7 @@ export namespace DirectVolume {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -85,7 +85,7 @@ export namespace Geometry {
|
||||
case 'spheres': return geometry.sphereCount * 6;
|
||||
case 'cylinders': return geometry.cylinderCount * 6;
|
||||
case 'text': return geometry.charCount * 4;
|
||||
case 'lines': return geometry.lineCount * 4;
|
||||
case 'lines': return geometry.vertexCount;
|
||||
case 'direct-volume':
|
||||
const [x, y, z] = geometry.gridDimension.ref.value;
|
||||
return x * y * z;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Theme } from '../../../mol-theme/theme';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createColors } from '../color-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createMarkers } from '../marker-data';
|
||||
@@ -28,6 +28,7 @@ import { NullLocation } from '../../../mol-model/location';
|
||||
import { QuadPositions } from '../../../mol-gl/compute/util';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
|
||||
const QuadIndices = new Uint32Array([
|
||||
0, 1, 2,
|
||||
@@ -74,6 +75,8 @@ interface Image {
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
|
||||
readonly meta: { [k: string]: unknown }
|
||||
}
|
||||
|
||||
namespace Image {
|
||||
@@ -136,7 +139,8 @@ namespace Image {
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(image);
|
||||
}
|
||||
},
|
||||
meta: {}
|
||||
};
|
||||
return image;
|
||||
}
|
||||
@@ -197,7 +201,7 @@ namespace Image {
|
||||
const positionIt = createPositionIterator(image, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -205,6 +209,7 @@ namespace Image {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
|
||||
|
||||
@@ -221,6 +226,7 @@ namespace Image {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -8,6 +8,12 @@ import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { Material } from '../../mol-util/material';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
|
||||
export type InteriorData = {
|
||||
uInteriorColor: ValueCell<Vec4>,
|
||||
uInteriorSubstance: ValueCell<Vec4>,
|
||||
}
|
||||
|
||||
export function getInteriorParam() {
|
||||
return PD.Group({
|
||||
@@ -17,23 +23,36 @@ export function getInteriorParam() {
|
||||
substanceStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
});
|
||||
}
|
||||
export type InteriorProp = ReturnType<typeof getInteriorParam>['defaultValue'];
|
||||
export type InteriorParam = ReturnType<typeof getInteriorParam>
|
||||
export type InteriorProps = InteriorParam['defaultValue'];
|
||||
|
||||
export function areInteriorPropsEquals(a: InteriorProp, b: InteriorProp): boolean {
|
||||
export function areInteriorPropsEquals(a: InteriorProps, b: InteriorProps): boolean {
|
||||
return a.color === b.color
|
||||
&& a.colorStrength === b.colorStrength
|
||||
&& Material.areEqual(a.substance, b.substance)
|
||||
&& a.substanceStrength === b.substanceStrength;
|
||||
}
|
||||
|
||||
export function getInteriorColor(props: InteriorProp, out: Vec4): Vec4 {
|
||||
export function getInteriorColor(props: InteriorProps, out: Vec4): Vec4 {
|
||||
Color.toArrayNormalized(props.color, out, 0);
|
||||
out[3] = props.colorStrength;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getInteriorSubstance(props: InteriorProp, out: Vec4): Vec4 {
|
||||
export function getInteriorSubstance(props: InteriorProps, out: Vec4): Vec4 {
|
||||
Material.toArrayNormalized(props.substance, out, 0);
|
||||
out[3] = props.substanceStrength;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function createInteriorValues(props: InteriorProps) {
|
||||
return {
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props, Vec4())),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateInteriorValues(values: InteriorData, props: InteriorProps) {
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props, values.uInteriorSubstance.ref.value));
|
||||
}
|
||||
|
||||
51
src/mol-geo/geometry/lines/builder/box.ts
Normal file
51
src/mol-geo/geometry/lines/builder/box.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { LinesBuilder } from '../lines-builder';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
const _start = Vec3();
|
||||
const _end = Vec3();
|
||||
|
||||
// 8 corners of the unit cube [0,1]^3: indexed by 3 bits (x=bit0, y=bit1, z=bit2)
|
||||
const _corners: Vec3[] = [];
|
||||
for (let i = 0; i < 8; ++i) _corners.push(Vec3());
|
||||
|
||||
// 12 box edges: [startCorner, endCorner, axis]
|
||||
// axis: 0=x, 1=y, 2=z — the axis along which the edge runs
|
||||
const BoxEdges: [number, number, number][] = [
|
||||
// X-axis edges (y,z vary)
|
||||
[0, 1, 0], [2, 3, 0], [4, 5, 0], [6, 7, 0],
|
||||
// Y-axis edges (x,z vary)
|
||||
[0, 2, 1], [1, 3, 1], [4, 6, 1], [5, 7, 1],
|
||||
// Z-axis edges (x,y vary)
|
||||
[0, 4, 2], [1, 5, 2], [2, 6, 2], [3, 7, 2],
|
||||
];
|
||||
|
||||
/**
|
||||
* Add a wireframe box to a LinesBuilder.
|
||||
*/
|
||||
export function addBox(builder: LinesBuilder, transform: Mat4, group: number) {
|
||||
// Compute 8 corners in world space from unit cube [0,1]^3
|
||||
// Corner index bits: bit0=x(0/1), bit1=y(0/1), bit2=z(0/1)
|
||||
for (let ci = 0; ci < 8; ++ci) {
|
||||
Vec3.set(_corners[ci],
|
||||
(ci & 1) ? 1 : 0,
|
||||
(ci & 2) ? 1 : 0,
|
||||
(ci & 4) ? 1 : 0,
|
||||
);
|
||||
Vec3.transformMat4(_corners[ci], _corners[ci], transform);
|
||||
}
|
||||
|
||||
for (const [si, ei] of BoxEdges) {
|
||||
Vec3.copy(_start, _corners[si]);
|
||||
Vec3.copy(_end, _corners[ei]);
|
||||
|
||||
// Draw edge line
|
||||
builder.addVec(_start, _end, group);
|
||||
}
|
||||
}
|
||||
34
src/mol-geo/geometry/lines/builder/plane.ts
Normal file
34
src/mol-geo/geometry/lines/builder/plane.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { LinesBuilder } from '../lines-builder';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
const _c0 = Vec3();
|
||||
const _c1 = Vec3();
|
||||
const _c2 = Vec3();
|
||||
const _c3 = Vec3();
|
||||
|
||||
/**
|
||||
* Add wireframe edges of a quad to a LinesBuilder.
|
||||
*/
|
||||
export function addPlane(builder: LinesBuilder, corners: ArrayLike<number>, transform: Mat4, group: number) {
|
||||
Vec3.fromArray(_c0, corners, 0);
|
||||
Vec3.fromArray(_c1, corners, 3);
|
||||
Vec3.fromArray(_c2, corners, 6);
|
||||
Vec3.fromArray(_c3, corners, 9);
|
||||
|
||||
Vec3.transformMat4(_c0, _c0, transform);
|
||||
Vec3.transformMat4(_c1, _c1, transform);
|
||||
Vec3.transformMat4(_c2, _c2, transform);
|
||||
Vec3.transformMat4(_c3, _c3, transform);
|
||||
|
||||
builder.addVec(_c0, _c1, group);
|
||||
builder.addVec(_c1, _c2, group);
|
||||
builder.addVec(_c2, _c3, group);
|
||||
builder.addVec(_c3, _c0, group);
|
||||
}
|
||||
82
src/mol-geo/geometry/lines/builder/sphere.ts
Normal file
82
src/mol-geo/geometry/lines/builder/sphere.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { LinesBuilder } from '../lines-builder';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
const _p0 = Vec3();
|
||||
const _p1 = Vec3();
|
||||
|
||||
export interface AddSphereOptions {
|
||||
/** Number of segments per circle (default: 32) */
|
||||
segments?: number,
|
||||
/** Number of circles per dimension, evenly spaced along each axis (default: 1) */
|
||||
circlesPerDimension?: number,
|
||||
}
|
||||
|
||||
const DefaultAddSphereOptions: Required<AddSphereOptions> = {
|
||||
segments: 32,
|
||||
circlesPerDimension: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a wireframe sphere to a LinesBuilder as orthogonal circles in XY, XZ, YZ planes.
|
||||
*/
|
||||
export function addSphere(builder: LinesBuilder, radius: number, transform: Mat4, group: number, options?: AddSphereOptions) {
|
||||
const segments = options?.segments ?? DefaultAddSphereOptions.segments;
|
||||
const circlesPerDim = options?.circlesPerDimension ?? DefaultAddSphereOptions.circlesPerDimension;
|
||||
|
||||
// For each dimension, draw `circlesPerDim` circles spaced evenly along the axis.
|
||||
// circlesPerDim=1 → one circle at the center (offset=0)
|
||||
// circlesPerDim=3 → circles at -r/2, 0, +r/2
|
||||
|
||||
for (let dim = 0; dim < 3; ++dim) {
|
||||
for (let ci = 0; ci < circlesPerDim; ++ci) {
|
||||
// offset along the perpendicular axis: evenly spaced in [-radius, radius]
|
||||
const offset = circlesPerDim === 1
|
||||
? 0
|
||||
: -radius + (2 * radius * (ci + 1)) / (circlesPerDim + 1);
|
||||
|
||||
// Choose a smaller radius for offset circles (cross-section of the sphere)
|
||||
const r = Math.sqrt(Math.max(0, radius * radius - offset * offset));
|
||||
if (r < 1e-8) continue;
|
||||
|
||||
addCircle(builder, r, offset, dim, transform, group, segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addCircle(builder: LinesBuilder, radius: number, offset: number, perpAxis: number, transform: Mat4, group: number, segments: number) {
|
||||
// perpAxis: 0=X (circle in YZ), 1=Y (circle in XZ), 2=Z (circle in XY)
|
||||
for (let i = 0; i < segments; ++i) {
|
||||
const a0 = (i / segments) * Math.PI * 2;
|
||||
const a1 = ((i + 1) / segments) * Math.PI * 2;
|
||||
|
||||
const cos0 = Math.cos(a0) * radius;
|
||||
const sin0 = Math.sin(a0) * radius;
|
||||
const cos1 = Math.cos(a1) * radius;
|
||||
const sin1 = Math.sin(a1) * radius;
|
||||
|
||||
if (perpAxis === 2) {
|
||||
// XY circle at z=offset
|
||||
Vec3.set(_p0, cos0, sin0, offset);
|
||||
Vec3.set(_p1, cos1, sin1, offset);
|
||||
} else if (perpAxis === 1) {
|
||||
// XZ circle at y=offset
|
||||
Vec3.set(_p0, cos0, offset, sin0);
|
||||
Vec3.set(_p1, cos1, offset, sin1);
|
||||
} else {
|
||||
// YZ circle at x=offset
|
||||
Vec3.set(_p0, offset, cos0, sin0);
|
||||
Vec3.set(_p1, offset, cos1, sin1);
|
||||
}
|
||||
|
||||
Vec3.transformMat4(_p0, _p0, transform);
|
||||
Vec3.transformMat4(_p1, _p1, transform);
|
||||
builder.addVec(_p0, _p1, group);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -10,6 +10,15 @@ import { Lines } from './lines';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Cage } from '../../primitive/cage';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const caAdd = ChunkedArray.add;
|
||||
const caAdd2 = ChunkedArray.add2;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
|
||||
const tmpVecA = Vec3();
|
||||
const tmpVecB = Vec3();
|
||||
const tmpDir = Vec3();
|
||||
|
||||
export interface LinesBuilder {
|
||||
add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number): void
|
||||
addVec(start: Vec3, end: Vec3, group: number): void
|
||||
@@ -19,14 +28,6 @@ export interface LinesBuilder {
|
||||
getLines(): Lines
|
||||
}
|
||||
|
||||
const tmpVecA = Vec3();
|
||||
const tmpVecB = Vec3();
|
||||
const tmpDir = Vec3();
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const caAdd = ChunkedArray.add;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
|
||||
export namespace LinesBuilder {
|
||||
export function create(initialCount = 2048, chunkSize = 1024, lines?: Lines): LinesBuilder {
|
||||
const groups = ChunkedArray.create(Float32Array, 1, chunkSize, lines ? lines.groupBuffer.ref.value : initialCount);
|
||||
@@ -89,13 +90,15 @@ export namespace LinesBuilder {
|
||||
},
|
||||
getLines: () => {
|
||||
const lineCount = groups.elementCount / 4;
|
||||
const vertexCount = groups.elementCount;
|
||||
const gb = ChunkedArray.compact(groups, true) as Float32Array;
|
||||
const sb = ChunkedArray.compact(starts, true) as Float32Array;
|
||||
const eb = ChunkedArray.compact(ends, true) as Float32Array;
|
||||
const mb = lines && lineCount <= lines.lineCount ? lines.mappingBuffer.ref.value : new Float32Array(lineCount * 8);
|
||||
const ib = lines && lineCount <= lines.lineCount ? lines.indexBuffer.ref.value : new Uint32Array(lineCount * 6);
|
||||
if (!lines || lineCount > lines.lineCount) fillMappingAndIndices(lineCount, mb, ib);
|
||||
return Lines.create(mb, ib, gb, sb, eb, lineCount, lines);
|
||||
const mb = lines && lineCount <= lines.lineCount && lines.stripCount.ref.value === 0 ? lines.mappingBuffer.ref.value : new Float32Array(lineCount * 8);
|
||||
const ib = lines && lineCount <= lines.lineCount && lines.stripCount.ref.value === 0 ? lines.indexBuffer.ref.value : new Uint32Array(lineCount * 6);
|
||||
const ob = lines ? lines.stripBuffer.ref.value : new Uint32Array(0);
|
||||
if (!lines || lineCount > lines.lineCount || lines.stripCount.ref.value > 0) fillMappingAndIndices(lineCount, mb, ib);
|
||||
return Lines.create(mb, ib, gb, sb, eb, ob, lineCount, vertexCount, 0, lines);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -117,3 +120,104 @@ function fillMappingAndIndices(n: number, mb: Float32Array, ib: Uint32Array) {
|
||||
ib[io + 3] = o + 1; ib[io + 4] = o + 3; ib[io + 5] = o + 2;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export interface StripLinesBuilder {
|
||||
start(group: number): void
|
||||
add(x: number, y: number, z: number): void
|
||||
addVec(v: Vec3): void
|
||||
end(): void
|
||||
getLines(): Lines
|
||||
}
|
||||
|
||||
export namespace StripLinesBuilder {
|
||||
export function create(initialCount = 2048, chunkSize = 1024, lines?: Lines): StripLinesBuilder {
|
||||
const groups = ChunkedArray.create(Float32Array, 1, chunkSize, lines ? lines.groupBuffer.ref.value : initialCount);
|
||||
const starts = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.startBuffer.ref.value : initialCount);
|
||||
const ends = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.endBuffer.ref.value : initialCount);
|
||||
const mapping = ChunkedArray.create(Float32Array, 2, chunkSize, lines ? lines.mappingBuffer.ref.value : initialCount);
|
||||
const indices = ChunkedArray.create(Uint32Array, 3, chunkSize, lines ? lines.indexBuffer.ref.value : initialCount);
|
||||
const strips = ChunkedArray.create(Uint32Array, 1, chunkSize, lines ? lines.stripBuffer.ref.value : initialCount);
|
||||
|
||||
let stripGroup = 0;
|
||||
let pointCount = 0;
|
||||
let firstVertexOffset = 0;
|
||||
let prevX = 0, prevY = 0, prevZ = 0;
|
||||
|
||||
const addPoint = (x: number, y: number, z: number) => {
|
||||
if (pointCount === 0) {
|
||||
firstVertexOffset = groups.elementCount;
|
||||
prevX = x; prevY = y; prevZ = z;
|
||||
pointCount = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexOffset = groups.elementCount;
|
||||
|
||||
if (pointCount === 1) {
|
||||
caAdd3(starts, prevX, prevY, prevZ);
|
||||
caAdd3(ends, x, y, z);
|
||||
caAdd(groups, stripGroup);
|
||||
caAdd2(mapping, -1, -1); // left, start
|
||||
|
||||
caAdd3(starts, prevX, prevY, prevZ);
|
||||
caAdd3(ends, x, y, z);
|
||||
caAdd(groups, stripGroup);
|
||||
caAdd2(mapping, 1, -1); // right, start
|
||||
}
|
||||
|
||||
caAdd3(starts, prevX, prevY, prevZ);
|
||||
caAdd3(ends, x, y, z);
|
||||
caAdd(groups, stripGroup);
|
||||
caAdd2(mapping, -1, 1); // left, end
|
||||
|
||||
caAdd3(starts, prevX, prevY, prevZ);
|
||||
caAdd3(ends, x, y, z);
|
||||
caAdd(groups, stripGroup);
|
||||
caAdd2(mapping, 1, 1); // right, end
|
||||
|
||||
const prevOffset = pointCount === 1 ? firstVertexOffset : vertexOffset - 2;
|
||||
const currOffset = pointCount === 1 ? vertexOffset + 2 : vertexOffset;
|
||||
// Triangle 1: prev-left, prev-right, curr-left
|
||||
caAdd3(indices, prevOffset, prevOffset + 1, currOffset);
|
||||
// Triangle 2: prev-right, curr-right, curr-left
|
||||
caAdd3(indices, prevOffset + 1, currOffset + 1, currOffset);
|
||||
|
||||
prevX = x; prevY = y; prevZ = z;
|
||||
pointCount++;
|
||||
};
|
||||
|
||||
return {
|
||||
start: (group: number) => {
|
||||
stripGroup = group;
|
||||
pointCount = 0;
|
||||
if (strips.elementCount === 0) {
|
||||
caAdd(strips, 0);
|
||||
}
|
||||
},
|
||||
add: (x: number, y: number, z: number) => {
|
||||
addPoint(x, y, z);
|
||||
},
|
||||
addVec: (v: Vec3) => {
|
||||
addPoint(v[0], v[1], v[2]);
|
||||
},
|
||||
end: () => {
|
||||
pointCount = 0;
|
||||
caAdd(strips, groups.elementCount);
|
||||
},
|
||||
getLines: () => {
|
||||
const lineCount = indices.elementCount / 2;
|
||||
const vertexCount = groups.elementCount;
|
||||
const stripCount = strips.elementCount - 1;
|
||||
const gb = ChunkedArray.compact(groups, true) as Float32Array;
|
||||
const sb = ChunkedArray.compact(starts, true) as Float32Array;
|
||||
const eb = ChunkedArray.compact(ends, true) as Float32Array;
|
||||
const mb = ChunkedArray.compact(mapping, true) as Float32Array;
|
||||
const ib = ChunkedArray.compact(indices, true) as Uint32Array;
|
||||
const ob = ChunkedArray.compact(strips, true) as Uint32Array;
|
||||
return Lines.create(mb, ib, gb, sb, eb, ob, lineCount, vertexCount, stripCount, lines);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,15 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
/** Wide line */
|
||||
export interface Lines {
|
||||
@@ -35,6 +37,8 @@ export interface Lines {
|
||||
|
||||
/** Number of lines */
|
||||
lineCount: number,
|
||||
/** Number of vertices */
|
||||
vertexCount: number,
|
||||
|
||||
/** Mapping buffer as array of xy values wrapped in a value cell */
|
||||
readonly mappingBuffer: ValueCell<Float32Array>,
|
||||
@@ -47,6 +51,11 @@ export interface Lines {
|
||||
/** Line end buffer as array of xyz values wrapped in a value cell */
|
||||
readonly endBuffer: ValueCell<Float32Array>,
|
||||
|
||||
/** Number of strips wrapped in a value cell */
|
||||
readonly stripCount: ValueCell<number>,
|
||||
/** Strip buffer as array of vertex offsets wrapped in a value cell */
|
||||
readonly stripBuffer: ValueCell<Uint32Array>,
|
||||
|
||||
/** Bounding sphere of the lines */
|
||||
readonly boundingSphere: Sphere3D
|
||||
/** Maps group ids to line indices */
|
||||
@@ -57,10 +66,10 @@ export interface Lines {
|
||||
}
|
||||
|
||||
export namespace Lines {
|
||||
export function create(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number, lines?: Lines): Lines {
|
||||
export function create(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, strips: Uint32Array, lineCount: number, vertexCount: number, stripCount: number, lines?: Lines): Lines {
|
||||
return lines ?
|
||||
update(mappings, indices, groups, starts, ends, lineCount, lines) :
|
||||
fromArrays(mappings, indices, groups, starts, ends, lineCount);
|
||||
update(mappings, indices, groups, starts, ends, strips, lineCount, vertexCount, stripCount, lines) :
|
||||
fromArrays(mappings, indices, groups, starts, ends, strips, lineCount, vertexCount, stripCount);
|
||||
}
|
||||
|
||||
export function createEmpty(lines?: Lines): Lines {
|
||||
@@ -69,7 +78,8 @@ export namespace Lines {
|
||||
const gb = lines ? lines.groupBuffer.ref.value : new Float32Array(0);
|
||||
const sb = lines ? lines.startBuffer.ref.value : new Float32Array(0);
|
||||
const eb = lines ? lines.endBuffer.ref.value : new Float32Array(0);
|
||||
return create(mb, ib, gb, sb, eb, 0, lines);
|
||||
const ob = lines ? lines.stripBuffer.ref.value : new Uint32Array(0);
|
||||
return create(mb, ib, gb, sb, eb, ob, 0, 0, 0, lines);
|
||||
}
|
||||
|
||||
export function fromMesh(mesh: Mesh, lines?: Lines) {
|
||||
@@ -95,12 +105,14 @@ export namespace Lines {
|
||||
|
||||
function hashCode(lines: Lines) {
|
||||
return hashFnv32a([
|
||||
lines.lineCount, lines.mappingBuffer.ref.version, lines.indexBuffer.ref.version,
|
||||
lines.groupBuffer.ref.version, lines.startBuffer.ref.version, lines.endBuffer.ref.version
|
||||
lines.lineCount, lines.vertexCount,
|
||||
lines.mappingBuffer.ref.version, lines.indexBuffer.ref.version,
|
||||
lines.groupBuffer.ref.version, lines.startBuffer.ref.version, lines.endBuffer.ref.version,
|
||||
lines.stripCount.ref.version, lines.stripBuffer.ref.version
|
||||
]);
|
||||
}
|
||||
|
||||
function fromArrays(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number): Lines {
|
||||
function fromArrays(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, strips: Uint32Array, lineCount: number, vertexCount: number, stripCount: number): Lines {
|
||||
|
||||
const boundingSphere = Sphere3D();
|
||||
let groupMapping: GroupMapping;
|
||||
@@ -111,11 +123,14 @@ export namespace Lines {
|
||||
const lines = {
|
||||
kind: 'lines' as const,
|
||||
lineCount,
|
||||
vertexCount,
|
||||
mappingBuffer: ValueCell.create(mappings),
|
||||
indexBuffer: ValueCell.create(indices),
|
||||
groupBuffer: ValueCell.create(groups),
|
||||
startBuffer: ValueCell.create(starts),
|
||||
endBuffer: ValueCell.create(ends),
|
||||
stripCount: ValueCell.create(stripCount),
|
||||
stripBuffer: ValueCell.create(strips),
|
||||
get boundingSphere() {
|
||||
const newHash = hashCode(lines);
|
||||
if (newHash !== currentHash) {
|
||||
@@ -145,24 +160,27 @@ export namespace Lines {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number, lines: Lines) {
|
||||
if (lineCount > lines.lineCount) {
|
||||
function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, strips: Uint32Array, lineCount: number, vertexCount: number, stripCount: number, lines: Lines) {
|
||||
if (lineCount > lines.lineCount || stripCount !== lines.stripCount.ref.value || stripCount > 0) {
|
||||
ValueCell.update(lines.mappingBuffer, mappings);
|
||||
ValueCell.update(lines.indexBuffer, indices);
|
||||
}
|
||||
lines.lineCount = lineCount;
|
||||
lines.vertexCount = vertexCount;
|
||||
ValueCell.update(lines.groupBuffer, groups);
|
||||
ValueCell.update(lines.startBuffer, starts);
|
||||
ValueCell.update(lines.endBuffer, ends);
|
||||
ValueCell.updateIfChanged(lines.stripCount, stripCount);
|
||||
ValueCell.update(lines.stripBuffer, strips);
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function transform(lines: Lines, t: Mat4) {
|
||||
const start = lines.startBuffer.ref.value;
|
||||
transformPositionArray(t, start, 0, lines.lineCount * 4);
|
||||
transformPositionArray(t, start, 0, lines.vertexCount);
|
||||
ValueCell.update(lines.startBuffer, start);
|
||||
const end = lines.endBuffer.ref.value;
|
||||
transformPositionArray(t, end, 0, lines.lineCount * 4);
|
||||
transformPositionArray(t, end, 0, lines.vertexCount);
|
||||
ValueCell.update(lines.endBuffer, end);
|
||||
}
|
||||
|
||||
@@ -172,6 +190,7 @@ export namespace Lines {
|
||||
...BaseGeometry.Params,
|
||||
sizeFactor: PD.Numeric(2, { min: 0, max: 10, step: 0.1 }),
|
||||
lineSizeAttenuation: PD.Boolean(false),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -212,8 +231,8 @@ export namespace Lines {
|
||||
const positionIt = createPositionIterator(lines, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -221,8 +240,9 @@ export namespace Lines {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.lineCount * 4, groupCount, instanceCount };
|
||||
const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.vertexCount, groupCount, instanceCount };
|
||||
|
||||
const invariantBoundingSphere = Sphere3D.clone(lines.boundingSphere);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount, 0);
|
||||
@@ -246,6 +266,7 @@ export namespace Lines {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -253,6 +274,10 @@ export namespace Lines {
|
||||
dLineSizeAttenuation: ValueCell.create(props.lineSizeAttenuation),
|
||||
uDoubleSided: ValueCell.create(true),
|
||||
dFlipSided: ValueCell.create(false),
|
||||
...createAnimationValues(props.animation),
|
||||
|
||||
stripCount: lines.stripCount,
|
||||
stripOffsets: lines.stripBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,6 +291,7 @@ export namespace Lines {
|
||||
BaseGeometry.updateValues(values, props);
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
|
||||
ValueCell.updateIfChanged(values.dLineSizeAttenuation, props.lineSizeAttenuation);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: LinesValues, lines: Lines) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -29,8 +29,14 @@ interface ColorSmoothingInput {
|
||||
itemSize: 4 | 3 | 1
|
||||
}
|
||||
|
||||
export function calcMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl?: WebGLContext, texture?: Texture) {
|
||||
export type ColorSmoothingOptions = {
|
||||
resolution: number,
|
||||
stride: number
|
||||
};
|
||||
|
||||
export function calcMeshColorSmoothing(input: ColorSmoothingInput, options: ColorSmoothingOptions, webgl?: WebGLContext, texture?: Texture) {
|
||||
const { colorType, vertexCount, groupCount, positionBuffer, instanceBuffer, transformBuffer, groupBuffer, itemSize } = input;
|
||||
const { resolution, stride } = options;
|
||||
|
||||
const isInstanceType = colorType.endsWith('Instance');
|
||||
const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
|
||||
@@ -70,7 +76,7 @@ export function calcMeshColorSmoothing(input: ColorSmoothingInput, resolution: n
|
||||
for (let i = 0; i < instanceCount; ++i) {
|
||||
// - use reordered index for access from GPU
|
||||
// - use serial index for access from CPU
|
||||
const instanceIndex = webgl ? instanceBuffer[i] : i;
|
||||
const instanceIndex = (webgl && isInstanceType) ? instanceBuffer[i] : 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);
|
||||
@@ -262,7 +268,7 @@ function isSupportedColorType(x: string): x is 'group' | 'groupInstance' {
|
||||
return x === 'group' || x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyMeshColorSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyMeshColorSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedColorType(values.dColorType.ref.value)) return;
|
||||
|
||||
const smoothingData = calcMeshColorSmoothing({
|
||||
@@ -278,7 +284,7 @@ export function applyMeshColorSmoothing(values: MeshValues, resolution: number,
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
itemSize: 3
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
|
||||
if (smoothingData.kind === 'volume') {
|
||||
ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
|
||||
@@ -297,7 +303,7 @@ function isSupportedOverpaintType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyMeshOverpaintSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyMeshOverpaintSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedOverpaintType(values.dOverpaintType.ref.value)) return;
|
||||
|
||||
const smoothingData = calcMeshColorSmoothing({
|
||||
@@ -313,7 +319,7 @@ export function applyMeshOverpaintSmoothing(values: MeshValues, resolution: numb
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
itemSize: 4
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
if (smoothingData.kind === 'volume') {
|
||||
ValueCell.updateIfChanged(values.dOverpaintType, smoothingData.type);
|
||||
ValueCell.update(values.tOverpaintGrid, smoothingData.texture);
|
||||
@@ -331,7 +337,7 @@ function isSupportedTransparencyType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyMeshTransparencySmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyMeshTransparencySmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedTransparencyType(values.dTransparencyType.ref.value)) return;
|
||||
|
||||
const smoothingData = calcMeshColorSmoothing({
|
||||
@@ -347,7 +353,7 @@ export function applyMeshTransparencySmoothing(values: MeshValues, resolution: n
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
itemSize: 1
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
if (smoothingData.kind === 'volume') {
|
||||
ValueCell.updateIfChanged(values.dTransparencyType, smoothingData.type);
|
||||
ValueCell.update(values.tTransparencyGrid, smoothingData.texture);
|
||||
@@ -365,7 +371,7 @@ function isSupportedEmissiveType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyMeshEmissiveSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyMeshEmissiveSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedEmissiveType(values.dEmissiveType.ref.value)) return;
|
||||
|
||||
const smoothingData = calcMeshColorSmoothing({
|
||||
@@ -381,7 +387,7 @@ export function applyMeshEmissiveSmoothing(values: MeshValues, resolution: numbe
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
itemSize: 1
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
if (smoothingData.kind === 'volume') {
|
||||
ValueCell.updateIfChanged(values.dEmissiveType, smoothingData.type);
|
||||
ValueCell.update(values.tEmissiveGrid, smoothingData.texture);
|
||||
@@ -399,7 +405,7 @@ function isSupportedSubstanceType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyMeshSubstanceSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyMeshSubstanceSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedSubstanceType(values.dSubstanceType.ref.value)) return;
|
||||
|
||||
const smoothingData = calcMeshColorSmoothing({
|
||||
@@ -415,7 +421,7 @@ export function applyMeshSubstanceSmoothing(values: MeshValues, resolution: numb
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
itemSize: 4
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
if (smoothingData.kind === 'volume') {
|
||||
ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
|
||||
ValueCell.update(values.tSubstanceGrid, smoothingData.texture);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { MeshValues } from '../../../mol-gl/renderable/mesh';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
@@ -29,7 +29,9 @@ import { arraySetAdd } from '../../../mol-util/array';
|
||||
import { degToRad } from '../../../mol-math/misc';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface Mesh {
|
||||
readonly kind: 'mesh',
|
||||
@@ -639,6 +641,7 @@ export namespace Mesh {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -681,7 +684,7 @@ export namespace Mesh {
|
||||
const positionIt = createPositionIterator(mesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -689,6 +692,7 @@ export namespace Mesh {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: mesh.triangleCount * 3, vertexCount: mesh.vertexCount, groupCount, instanceCount };
|
||||
|
||||
@@ -713,6 +717,7 @@ export namespace Mesh {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -725,10 +730,11 @@ export namespace Mesh {
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
|
||||
meta: ValueCell.create(mesh.meta),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -749,8 +755,8 @@ export namespace Mesh {
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: MeshValues, mesh: Mesh) {
|
||||
|
||||
@@ -20,13 +20,15 @@ import { Theme } from '../../../mol-theme/theme';
|
||||
import { PointsValues } from '../../../mol-gl/renderable/points';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
/** Point cloud */
|
||||
export interface Points {
|
||||
@@ -136,6 +138,7 @@ export namespace Points {
|
||||
sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
|
||||
pointSizeAttenuation: PD.Boolean(false),
|
||||
pointStyle: PD.Select('square', PD.objectToOptions(StyleTypes)),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -174,8 +177,8 @@ export namespace Points {
|
||||
const positionIt = createPositionIterator(points, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -183,6 +186,7 @@ export namespace Points {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: points.pointCount, vertexCount: points.pointCount, groupCount, instanceCount };
|
||||
|
||||
@@ -205,12 +209,14 @@ export namespace Points {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
uSizeFactor: ValueCell.create(props.sizeFactor),
|
||||
dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation),
|
||||
dPointStyle: ValueCell.create(props.pointStyle),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,6 +231,7 @@ export namespace Points {
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
|
||||
ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation);
|
||||
ValueCell.updateIfChanged(values.dPointStyle, props.pointStyle);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: PointsValues, points: Points) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -13,7 +13,7 @@ import { SizeTheme } from '../../mol-theme/size';
|
||||
import { Geometry } from './geometry';
|
||||
import { unpackRGBToInt, packIntToRGBArray } from '../../mol-util/number-packing';
|
||||
|
||||
export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance'
|
||||
export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 'vertex' | 'vertexInstance';
|
||||
|
||||
export type SizeData = {
|
||||
uSize: ValueCell<number>,
|
||||
@@ -22,12 +22,14 @@ export type SizeData = {
|
||||
dSizeType: ValueCell<string>,
|
||||
}
|
||||
|
||||
export function createSizes(locationIt: LocationIterator, sizeTheme: SizeTheme<any>, sizeData?: SizeData): SizeData {
|
||||
export function createSizes(locationIt: LocationIterator, positionIt: LocationIterator, sizeTheme: SizeTheme<any>, sizeData?: SizeData): SizeData {
|
||||
switch (Geometry.getGranularity(locationIt, sizeTheme.granularity)) {
|
||||
case 'uniform': return createUniformSize(locationIt, sizeTheme.size, sizeData);
|
||||
case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData);
|
||||
case 'group': return createGroupSize(locationIt, sizeTheme.size, sizeData);
|
||||
case 'groupInstance': return createGroupInstanceSize(locationIt, sizeTheme.size, sizeData);
|
||||
case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData);
|
||||
case 'vertex': return createVertexSize(positionIt, sizeTheme.size, sizeData);
|
||||
case 'vertexInstance': return createVertexInstanceSize(positionIt, sizeTheme.size, sizeData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +43,8 @@ export function getMaxSize(sizeData: SizeData): number {
|
||||
case 'instance':
|
||||
case 'group':
|
||||
case 'groupInstance':
|
||||
case 'vertex':
|
||||
case 'vertexInstance':
|
||||
let maxSize = 0;
|
||||
const array = sizeData.tSize.ref.value.array;
|
||||
for (let i = 0, il = array.length; i < il; i += 3) {
|
||||
@@ -134,4 +138,29 @@ export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: Lo
|
||||
packIntToRGBArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.index * 3);
|
||||
}
|
||||
return createTextureSize(sizes, 'groupInstance', sizeData);
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates size texture with size for each vertex */
|
||||
export function createVertexSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
|
||||
const { groupCount } = locationIt;
|
||||
const sizes = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext) {
|
||||
const v = locationIt.move();
|
||||
packIntToRGBArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.index * 3);
|
||||
}
|
||||
return createTextureSize(sizes, 'vertex', sizeData);
|
||||
}
|
||||
|
||||
/** Creates size texture with size for each vertex instance */
|
||||
export function createVertexInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
|
||||
const { groupCount, instanceCount } = locationIt;
|
||||
const count = instanceCount * groupCount;
|
||||
const sizes = createTextureImage(Math.max(1, count), 3, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext) {
|
||||
const v = locationIt.move();
|
||||
packIntToRGBArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.index * 3);
|
||||
}
|
||||
return createTextureSize(sizes, 'vertexInstance', sizeData);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { TextureImage, calculateInvariantBoundingSphere, calculateTransformBound
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { createSizes, getMaxSize } from '../size-data';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -27,7 +27,9 @@ import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface Spheres {
|
||||
readonly kind: 'spheres',
|
||||
@@ -247,6 +249,33 @@ export namespace Spheres {
|
||||
return lodLevels.map(l => getAdjustedStride(l, sizeFactor)).reverse();
|
||||
}
|
||||
|
||||
export const LodLevelsPresets: { [key in 'performance' | 'balanced' | 'quality' | 'ultra']: LodLevels } = {
|
||||
performance: [
|
||||
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
|
||||
],
|
||||
balanced: [
|
||||
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
|
||||
],
|
||||
quality: [
|
||||
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
|
||||
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
|
||||
],
|
||||
ultra: [
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
export const Params = {
|
||||
...BaseGeometry.Params,
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
@@ -262,6 +291,7 @@ export namespace Spheres {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
lodLevels: PD.ObjectList({
|
||||
minDistance: PD.Numeric(0),
|
||||
maxDistance: PD.Numeric(0),
|
||||
@@ -270,7 +300,8 @@ export namespace Spheres {
|
||||
scaleBias: PD.Numeric(3, { min: 0.1, max: 10, step: 0.1 }),
|
||||
}, o => `${o.stride}`, {
|
||||
...BaseGeometry.CullingLodCategory,
|
||||
defaultValue: [] as LodLevels
|
||||
defaultValue: [] as LodLevels,
|
||||
presets: Object.entries(LodLevelsPresets).map(([k, v]) => [v, k])
|
||||
})
|
||||
};
|
||||
export type Params = typeof Params
|
||||
@@ -310,8 +341,8 @@ export namespace Spheres {
|
||||
const positionIt = createPositionIterator(spheres, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -319,6 +350,7 @@ export namespace Spheres {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: spheres.sphereCount * 2 * 3, vertexCount: spheres.sphereCount * 6, groupCount, instanceCount };
|
||||
|
||||
@@ -345,6 +377,7 @@ export namespace Spheres {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
padding: ValueCell.create(padding),
|
||||
@@ -362,12 +395,13 @@ export namespace Spheres {
|
||||
uAlphaThickness: ValueCell.create(props.alphaThickness),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
|
||||
lodLevels: spheres.shaderData.lodLevels,
|
||||
centerBuffer: spheres.centerBuffer,
|
||||
groupBuffer: spheres.groupBuffer,
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,8 +425,8 @@ export namespace Spheres {
|
||||
ValueCell.updateIfChanged(values.uAlphaThickness, props.alphaThickness);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
|
||||
const lodLevels = getLodLevels(values.lodLevels.ref.value as LodLevelsValue);
|
||||
if (!areLodLevelsEqual(props.lodLevels, lodLevels)) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -76,7 +77,7 @@ export class FontAtlas {
|
||||
this.props = p;
|
||||
|
||||
// create measurements
|
||||
const fontSize = 32 * (p.fontQuality + 1);
|
||||
const fontSize = 64 * (p.fontQuality + 1);
|
||||
this.buffer = fontSize / 8;
|
||||
this.radius = fontSize / 3;
|
||||
this.lineHeight = Math.round(fontSize + 2 * this.buffer + this.radius);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -118,8 +119,9 @@ export namespace TextBuilder {
|
||||
|
||||
const xLeft = (-xShift - margin - 0.1) * scale;
|
||||
const xRight = (bWidth - xShift + margin + 0.1) * scale;
|
||||
const yTop = (bHeight - yShift + margin) * scale;
|
||||
const yBottom = (-yShift - margin) * scale;
|
||||
const yMid = 0.5 - yShift - outline * 0.5; // glyph vertical midpoint accounting for outline offset
|
||||
const yTop = (yMid + bHeight / 2 + margin) * scale;
|
||||
const yBottom = (yMid - bHeight / 2 - margin) * scale;
|
||||
|
||||
// background
|
||||
if (background) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -24,7 +25,7 @@ import { FontAtlasParams } from './font-atlas';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { clamp } from '../../../mol-math/interpolate';
|
||||
import { createRenderObject as _createRenderObject } from '../../../mol-gl/render-object';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -32,6 +33,7 @@ import { GroupMapping, createGroupMapping } from '../../util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
|
||||
type TextAttachment = (
|
||||
'bottom-left' | 'bottom-center' | 'bottom-right' |
|
||||
@@ -216,8 +218,8 @@ export namespace Text {
|
||||
const positionIt = createPositionIterator(text, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -225,6 +227,7 @@ export namespace Text {
|
||||
const emissive = createEmptyEmissive();
|
||||
const substance = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: text.charCount * 2 * 3, vertexCount: text.charCount * 4, groupCount, instanceCount };
|
||||
|
||||
@@ -252,6 +255,7 @@ export namespace Text {
|
||||
...emissive,
|
||||
...substance,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
aTexCoord: text.tcoordBuffer,
|
||||
@@ -327,16 +331,17 @@ export namespace Text {
|
||||
}
|
||||
|
||||
function getPadding(mappings: Float32Array, depths: Float32Array, charCount: number, scale: number) {
|
||||
let maxOffset = 0;
|
||||
let maxOffsetX = 0;
|
||||
let maxOffsetY = 0;
|
||||
let maxDepth = 0;
|
||||
for (let i = 0, il = charCount * 4; i < il; ++i) {
|
||||
const i2 = 2 * i;
|
||||
const ox = Math.abs(mappings[i2]);
|
||||
if (ox > maxOffset) maxOffset = ox;
|
||||
if (ox > maxOffsetX) maxOffsetX = ox;
|
||||
const oy = Math.abs(mappings[i2 + 1]);
|
||||
if (oy > maxOffset) maxOffset = oy;
|
||||
if (oy > maxOffsetY) maxOffsetY = oy;
|
||||
const d = Math.abs(depths[i]);
|
||||
if (d > maxDepth) maxDepth = d;
|
||||
}
|
||||
return Math.max(maxDepth, scale * maxOffset);
|
||||
return Math.max(maxDepth, scale * Math.sqrt(maxOffsetX * maxOffsetX + maxOffsetY * maxOffsetY));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -14,7 +14,7 @@ import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, Values, DefineSpec
|
||||
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 { Mat4, 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';
|
||||
@@ -246,18 +246,30 @@ interface ColorSmoothingInput extends AccumulateInput {
|
||||
invariantBoundingSphere: Sphere3D
|
||||
}
|
||||
|
||||
export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl: WebGLContext, texture?: Texture) {
|
||||
export type ColorSmoothingOptions = {
|
||||
resolution: number,
|
||||
stride: number
|
||||
};
|
||||
|
||||
export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, options: ColorSmoothingOptions, webgl: WebGLContext, texture?: Texture) {
|
||||
const { drawBuffers } = webgl.extensions;
|
||||
if (!drawBuffers) throw new Error('need WebGL draw buffers');
|
||||
|
||||
if (isTimingMode) webgl.timer.mark('calcTextureMeshColorSmoothing');
|
||||
const { gl, resources, state, extensions: { colorBufferHalfFloat, textureHalfFloat } } = webgl;
|
||||
const { resolution, stride } = options;
|
||||
|
||||
const isInstanceType = input.colorType.endsWith('Instance');
|
||||
const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
|
||||
const pad = 1 + resolution;
|
||||
const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
|
||||
|
||||
if (!isInstanceType) {
|
||||
input.instanceCount = 1;
|
||||
input.instanceBuffer = new Float32Array([0]);
|
||||
input.transformBuffer = new Float32Array(Mat4.id);
|
||||
}
|
||||
|
||||
const scaleFactor = 1 / resolution;
|
||||
const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor);
|
||||
const gridDim = Box3D.size(Vec3(), scaledBox);
|
||||
@@ -389,7 +401,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
|
||||
const type = isInstanceType ? 'volumeInstance' : 'volume';
|
||||
if (isTimingMode) webgl.timer.markEnd('calcTextureMeshColorSmoothing');
|
||||
|
||||
// printTextureImage(readTexture(webgl, texture), { scale: 0.75 });
|
||||
// printTextureImage(readTexture(webgl, texture), { scale: 0.75, id: `${texture.id}` });
|
||||
|
||||
return { texture, gridDim, gridTexDim: Vec2.create(width, height), gridTransform, type };
|
||||
}
|
||||
@@ -404,10 +416,10 @@ function isSupportedColorType(x: string): x is 'group' | 'groupInstance' {
|
||||
return x === 'group' || x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyTextureMeshColorSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyTextureMeshColorSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedColorType(values.dColorType.ref.value)) return;
|
||||
|
||||
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
|
||||
if (!webgl.namedTextures[ColorSmoothingRgbName]) {
|
||||
webgl.namedTextures[ColorSmoothingRgbName] = webgl.resources.texture('image-uint8', 'rgb', 'ubyte', 'nearest');
|
||||
@@ -427,7 +439,7 @@ export function applyTextureMeshColorSmoothing(values: TextureMeshValues, resolu
|
||||
colorType: values.dColorType.ref.value,
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
|
||||
ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
|
||||
ValueCell.update(values.tColorGrid, smoothingData.texture);
|
||||
@@ -440,10 +452,10 @@ function isSupportedOverpaintType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedOverpaintType(values.dOverpaintType.ref.value)) return;
|
||||
|
||||
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
|
||||
if (!webgl.namedTextures[ColorSmoothingRgbaName]) {
|
||||
webgl.namedTextures[ColorSmoothingRgbaName] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
|
||||
@@ -463,7 +475,7 @@ export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, re
|
||||
colorType: values.dOverpaintType.ref.value,
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
|
||||
ValueCell.updateIfChanged(values.dOverpaintType, smoothingData.type);
|
||||
ValueCell.update(values.tOverpaintGrid, smoothingData.texture);
|
||||
@@ -476,10 +488,10 @@ function isSupportedTransparencyType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedTransparencyType(values.dTransparencyType.ref.value)) return;
|
||||
|
||||
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
|
||||
if (!webgl.namedTextures[ColorSmoothingAlphaName]) {
|
||||
webgl.namedTextures[ColorSmoothingAlphaName] = webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'nearest');
|
||||
@@ -499,7 +511,7 @@ export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues,
|
||||
colorType: values.dTransparencyType.ref.value,
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
|
||||
ValueCell.updateIfChanged(values.dTransparencyType, smoothingData.type);
|
||||
ValueCell.update(values.tTransparencyGrid, smoothingData.texture);
|
||||
@@ -512,10 +524,10 @@ function isSupportedEmissiveType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedEmissiveType(values.dEmissiveType.ref.value)) return;
|
||||
|
||||
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
|
||||
if (!webgl.namedTextures[ColorSmoothingAlphaName]) {
|
||||
webgl.namedTextures[ColorSmoothingAlphaName] = webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'nearest');
|
||||
@@ -535,7 +547,7 @@ export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, res
|
||||
colorType: values.dEmissiveType.ref.value,
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
|
||||
ValueCell.updateIfChanged(values.dEmissiveType, smoothingData.type);
|
||||
ValueCell.update(values.tEmissiveGrid, smoothingData.texture);
|
||||
@@ -548,10 +560,10 @@ function isSupportedSubstanceType(x: string): x is 'groupInstance' {
|
||||
return x === 'groupInstance';
|
||||
}
|
||||
|
||||
export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
|
||||
if (!isSupportedSubstanceType(values.dSubstanceType.ref.value)) return;
|
||||
|
||||
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
|
||||
|
||||
if (!webgl.namedTextures[ColorSmoothingRgbaName]) {
|
||||
webgl.namedTextures[ColorSmoothingRgbaName] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
|
||||
@@ -571,7 +583,7 @@ export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, re
|
||||
colorType: values.dSubstanceType.ref.value,
|
||||
boundingSphere: values.boundingSphere.ref.value,
|
||||
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
|
||||
}, resolution, stride, webgl, colorTexture);
|
||||
}, options, webgl, colorTexture);
|
||||
|
||||
ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
|
||||
ValueCell.update(values.tSubstanceGrid, smoothingData.texture);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Cai Huiyu <szmun.caihy@gmail.com>
|
||||
@@ -15,7 +15,7 @@ import { createMarkers } from '../marker-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
|
||||
@@ -28,7 +28,9 @@ import { createEmptySubstance } from '../substance-data';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface TextureMesh {
|
||||
readonly kind: 'texture-mesh',
|
||||
@@ -130,6 +132,7 @@ export namespace TextureMesh {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -200,7 +203,7 @@ export namespace TextureMesh {
|
||||
const positionIt = Utils.createPositionIterator(textureMesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -208,6 +211,7 @@ export namespace TextureMesh {
|
||||
const emissive = createEmptyEmissive();
|
||||
const substance = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: textureMesh.vertexCount, vertexCount: textureMesh.vertexCount, groupCount, instanceCount };
|
||||
|
||||
@@ -234,6 +238,7 @@ export namespace TextureMesh {
|
||||
...emissive,
|
||||
...substance,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -246,10 +251,11 @@ export namespace TextureMesh {
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
|
||||
meta: ValueCell.create(textureMesh.meta),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -270,8 +276,8 @@ export namespace TextureMesh {
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {
|
||||
|
||||
79
src/mol-geo/geometry/wiggle-data.ts
Normal file
79
src/mol-geo/geometry/wiggle-data.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
|
||||
|
||||
export type WiggleType = 'instance' | 'groupInstance';
|
||||
|
||||
export type WiggleData = {
|
||||
tWiggle: ValueCell<TextureImage<Uint8Array>>
|
||||
uWiggleTexDim: ValueCell<Vec2>
|
||||
dWiggle: ValueCell<boolean>,
|
||||
wiggleAverage: ValueCell<number>,
|
||||
dWiggleType: ValueCell<string>,
|
||||
uWiggleStrength: ValueCell<number>,
|
||||
}
|
||||
|
||||
export function applyWiggleValue(array: Uint8Array, start: number, end: number, value: number) {
|
||||
for (let i = start; i < end; ++i) {
|
||||
array[i] = value * 255;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getWiggleAverage(array: Uint8Array, count: number): number {
|
||||
if (count === 0 || array.length < count) return 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < count; ++i) {
|
||||
sum += array[i];
|
||||
}
|
||||
return sum / (255 * count);
|
||||
}
|
||||
|
||||
export function clearWiggle(array: Uint8Array, start: number, end: number) {
|
||||
array.fill(0, start, end);
|
||||
}
|
||||
|
||||
export function createWiggle(count: number, type: WiggleType, wiggleData?: WiggleData): WiggleData {
|
||||
const wiggle = createTextureImage(Math.max(1, count), 1, Uint8Array, wiggleData && wiggleData.tWiggle.ref.value.array);
|
||||
if (wiggleData) {
|
||||
ValueCell.update(wiggleData.tWiggle, wiggle);
|
||||
ValueCell.update(wiggleData.uWiggleTexDim, Vec2.create(wiggle.width, wiggle.height));
|
||||
ValueCell.updateIfChanged(wiggleData.dWiggle, count > 0);
|
||||
ValueCell.updateIfChanged(wiggleData.wiggleAverage, getWiggleAverage(wiggle.array, count));
|
||||
ValueCell.updateIfChanged(wiggleData.dWiggleType, type);
|
||||
return wiggleData;
|
||||
} else {
|
||||
return {
|
||||
tWiggle: ValueCell.create(wiggle),
|
||||
uWiggleTexDim: ValueCell.create(Vec2.create(wiggle.width, wiggle.height)),
|
||||
dWiggle: ValueCell.create(count > 0),
|
||||
wiggleAverage: ValueCell.create(0),
|
||||
dWiggleType: ValueCell.create(type),
|
||||
uWiggleStrength: ValueCell.create(1),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const emptyWiggleTexture = { array: new Uint8Array(1), width: 1, height: 1 };
|
||||
export function createEmptyWiggle(wiggleData?: WiggleData): WiggleData {
|
||||
if (wiggleData) {
|
||||
ValueCell.update(wiggleData.tWiggle, emptyWiggleTexture);
|
||||
ValueCell.update(wiggleData.uWiggleTexDim, Vec2.create(1, 1));
|
||||
return wiggleData;
|
||||
} else {
|
||||
return {
|
||||
tWiggle: ValueCell.create(emptyWiggleTexture),
|
||||
uWiggleTexDim: ValueCell.create(Vec2.create(1, 1)),
|
||||
dWiggle: ValueCell.create(false),
|
||||
wiggleAverage: ValueCell.create(0),
|
||||
dWiggleType: ValueCell.create('groupInstance'),
|
||||
uWiggleStrength: ValueCell.create(1),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ describe('renderer', () => {
|
||||
scene.add(points);
|
||||
scene.commit();
|
||||
expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(10);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(11);
|
||||
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 5 : 0);
|
||||
expect(ctx.stats.resourceCounts.program).toBe(5);
|
||||
expect(ctx.stats.resourceCounts.shader).toBe(10);
|
||||
@@ -89,7 +89,7 @@ describe('renderer', () => {
|
||||
sceneDpoit.commit();
|
||||
|
||||
expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 12 : 15);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(28);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(31);
|
||||
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 15 : 0);
|
||||
expect(ctx.stats.resourceCounts.program).toBe(7);
|
||||
expect(ctx.stats.resourceCounts.shader).toBe(14);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { CylindersShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -32,9 +32,10 @@ export const CylindersSchema = {
|
||||
dSolidInterior: DefineSpec('boolean'),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
dDualColor: DefineSpec('boolean'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type CylindersSchema = typeof CylindersSchema
|
||||
export type CylindersValues = Values<CylindersSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, ValueSpec, AnimationSchema } from './schema';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { LinesShaderCode } from '../shader-code';
|
||||
|
||||
@@ -22,6 +22,10 @@ export const LinesSchema = {
|
||||
dLineSizeAttenuation: DefineSpec('boolean'),
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
stripCount: ValueSpec('number'),
|
||||
stripOffsets: ValueSpec('uint32'),
|
||||
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type LinesSchema = typeof LinesSchema
|
||||
export type LinesValues = Values<LinesSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema, ValueSpec, UniformSpec, GlobalDefineSchema, GlobalDefineValues, GlobalDefines } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema, ValueSpec, UniformSpec, GlobalDefineSchema, GlobalDefineValues, GlobalDefines, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { MeshShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -27,9 +27,10 @@ export const MeshSchema = {
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
meta: ValueSpec('unknown')
|
||||
meta: ValueSpec('unknown'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
} as const;
|
||||
export type MeshSchema = typeof MeshSchema
|
||||
export type MeshValues = Values<MeshSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, AnimationSchema } from './schema';
|
||||
import { PointsShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -18,6 +18,8 @@ export const PointsSchema = {
|
||||
aPosition: AttributeSpec('float32', 3, 0),
|
||||
dPointSizeAttenuation: DefineSpec('boolean'),
|
||||
dPointStyle: DefineSpec('string', ['square', 'circle', 'fuzzy']),
|
||||
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type PointsSchema = typeof PointsSchema
|
||||
export type PointsValues = Values<PointsSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -179,6 +179,9 @@ export const GlobalUniformSchema = {
|
||||
uMarkingDepthTest: UniformSpec('b'),
|
||||
uMarkingType: UniformSpec('i'),
|
||||
uPickType: UniformSpec('i'),
|
||||
|
||||
uTime: UniformSpec('f'),
|
||||
uEnableAnimation: UniformSpec('b'),
|
||||
} as const;
|
||||
export type GlobalUniformSchema = typeof GlobalUniformSchema
|
||||
export type GlobalUniformValues = Values<GlobalUniformSchema>
|
||||
@@ -315,6 +318,17 @@ export const ClippingSchema = {
|
||||
export type ClippingSchema = typeof ClippingSchema
|
||||
export type ClippingValues = Values<ClippingSchema>
|
||||
|
||||
export const WiggleSchema = {
|
||||
uWiggleTexDim: UniformSpec('v2'),
|
||||
tWiggle: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
|
||||
dWiggle: DefineSpec('boolean'),
|
||||
wiggleAverage: ValueSpec('number'),
|
||||
dWiggleType: DefineSpec('string', ['instance', 'groupInstance']),
|
||||
uWiggleStrength: UniformSpec('f', 'material'),
|
||||
} as const;
|
||||
export type WiggleSchema = typeof WiggleSchema
|
||||
export type WiggleValues = Values<WiggleSchema>
|
||||
|
||||
export const BaseSchema = {
|
||||
dGeometryType: DefineSpec('string', ['cylinders', 'directVolume', 'image', 'lines', 'mesh', 'points', 'spheres', 'text', 'textureMesh']),
|
||||
|
||||
@@ -325,6 +339,7 @@ export const BaseSchema = {
|
||||
...EmissiveSchema,
|
||||
...SubstanceSchema,
|
||||
...ClippingSchema,
|
||||
...WiggleSchema,
|
||||
|
||||
dClipObjectCount: DefineSpec('number'),
|
||||
dClipVariant: DefineSpec('string', ['instance', 'pixel']),
|
||||
@@ -386,3 +401,24 @@ export const BaseSchema = {
|
||||
} as const;
|
||||
export type BaseSchema = typeof BaseSchema
|
||||
export type BaseValues = Values<BaseSchema>
|
||||
|
||||
//
|
||||
|
||||
export const InteriorSchema = {
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
} as const;
|
||||
export type InteriorSchema = typeof InteriorSchema
|
||||
export type InteriorValues = Values<InteriorSchema>
|
||||
|
||||
export const AnimationSchema = {
|
||||
uWiggleSpeed: UniformSpec('f', 'material'),
|
||||
uWiggleAmplitude: UniformSpec('f', 'material'),
|
||||
uWiggleFrequency: UniformSpec('f', 'material'),
|
||||
uWiggleMode: UniformSpec('i', 'material'),
|
||||
uTumbleSpeed: UniformSpec('f', 'material'),
|
||||
uTumbleAmplitude: UniformSpec('f', 'material'),
|
||||
uTumbleFrequency: UniformSpec('f', 'material'),
|
||||
} as const;
|
||||
export type AnimationSchema = typeof AnimationSchema
|
||||
export type AnimationValues = Values<AnimationSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, Values, InternalSchema, SizeSchema, InternalValues, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, TextureSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, Values, InternalSchema, SizeSchema, InternalValues, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, TextureSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { SpheresShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -30,12 +30,13 @@ export const SpheresSchema = {
|
||||
uAlphaThickness: UniformSpec('f'),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
|
||||
lodLevels: ValueSpec('unknown'),
|
||||
centerBuffer: ValueSpec('float32'),
|
||||
groupBuffer: ValueSpec('float32'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type SpheresSchema = typeof SpheresSchema
|
||||
export type SpheresValues = Values<SpheresSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema, ValueSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema, ValueSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { MeshShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -27,9 +27,10 @@ export const TextureMeshSchema = {
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
meta: ValueSpec('unknown')
|
||||
meta: ValueSpec('unknown'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type TextureMeshSchema = typeof TextureMeshSchema
|
||||
export type TextureMeshValues = Values<TextureMeshSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -82,6 +82,13 @@ export function printTextureImage(textureImage: TextureImage<any>, options: Part
|
||||
} else {
|
||||
data.set(array);
|
||||
}
|
||||
} else if (itemSize === 3) {
|
||||
for (let i = 0, il = width * height; i < il; ++i) {
|
||||
data[i * 4] = array[i * 3];
|
||||
data[i * 4 + 1] = array[i * 3 + 1];
|
||||
data[i * 4 + 2] = array[i * 3 + 2];
|
||||
data[i * 4 + 3] = 255;
|
||||
}
|
||||
} else {
|
||||
console.warn(`itemSize '${itemSize}' not supported`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -63,6 +63,7 @@ interface Renderer {
|
||||
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean, forceToTransparency?: boolean) => void
|
||||
clearDepth: (packed?: boolean) => void
|
||||
update: (camera: ICamera, scene: Scene) => void
|
||||
setTime: (time: number) => void
|
||||
|
||||
renderPick: (group: Scene.Group, camera: ICamera, variant: 'pick' | 'depth', pickType: PickType) => void
|
||||
renderDepth: (group: Scene.Group, camera: ICamera) => void
|
||||
@@ -121,6 +122,8 @@ export const RendererParams = {
|
||||
}] }),
|
||||
ambientColor: PD.Color(Color.fromNormalizedRgb(1.0, 1.0, 1.0)),
|
||||
ambientIntensity: PD.Numeric(0.4, { min: 0.0, max: 2.0, step: 0.01 }),
|
||||
|
||||
enableAnimation: PD.Boolean(true, { description: 'Enable time-based animations.' }),
|
||||
};
|
||||
export type RendererProps = PD.Values<typeof RendererParams>
|
||||
|
||||
@@ -277,6 +280,9 @@ namespace Renderer {
|
||||
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
|
||||
uCelSteps: ValueCell.create(p.celSteps),
|
||||
uExposure: ValueCell.create(p.exposure),
|
||||
|
||||
uTime: ValueCell.create(0),
|
||||
uEnableAnimation: ValueCell.create(p.enableAnimation),
|
||||
};
|
||||
const globalUniformList = Object.entries(globalUniforms);
|
||||
|
||||
@@ -829,6 +835,9 @@ namespace Renderer {
|
||||
renderWboitTransparent,
|
||||
renderDpoitTransparent,
|
||||
|
||||
setTime: (time: number) => {
|
||||
ValueCell.updateIfChanged(globalUniforms.uTime, time);
|
||||
},
|
||||
setProps: (props: Partial<RendererProps>) => {
|
||||
if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) {
|
||||
p.backgroundColor = props.backgroundColor;
|
||||
@@ -904,6 +913,11 @@ namespace Renderer {
|
||||
Vec3.scale(ambientColor, Color.toArrayNormalized(p.ambientColor, ambientColor, 0), p.ambientIntensity);
|
||||
ValueCell.update(globalUniforms.uAmbientColor, ambientColor);
|
||||
}
|
||||
|
||||
if (props.enableAnimation !== undefined && props.enableAnimation !== p.enableAnimation) {
|
||||
p.enableAnimation = props.enableAnimation;
|
||||
ValueCell.update(globalUniforms.uEnableAnimation, p.enableAnimation);
|
||||
}
|
||||
},
|
||||
setViewport: (x: number, y: number, width: number, height: number) => {
|
||||
state.viewport(x, y, width, height);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 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>
|
||||
@@ -92,12 +92,16 @@ interface Scene extends Object3D {
|
||||
readonly markerAverage: number
|
||||
/** Emissive average of primitive renderables */
|
||||
readonly emissiveAverage: number
|
||||
/** Wiggle average of primitive renderables */
|
||||
readonly wiggleAverage: number
|
||||
/** Opacity average of primitive renderables */
|
||||
readonly opacityAverage: number
|
||||
/** Transparency minimum, excluding fully opaque, of primitive renderables */
|
||||
readonly transparencyMin: number
|
||||
/** Is `true` if any primitive renderable (possibly) has any opaque part */
|
||||
readonly hasOpaque: boolean
|
||||
/** Is `true` if any primitive renderable has animation enabled */
|
||||
readonly hasAnimation: boolean
|
||||
}
|
||||
|
||||
namespace Scene {
|
||||
@@ -119,15 +123,19 @@ namespace Scene {
|
||||
|
||||
let markerAverageDirty = true;
|
||||
let emissiveAverageDirty = true;
|
||||
let wiggleAverageDirty = true;
|
||||
let opacityAverageDirty = true;
|
||||
let transparencyMinDirty = true;
|
||||
let hasOpaqueDirty = true;
|
||||
let hasAnimationDirty = true;
|
||||
|
||||
let markerAverage = 0;
|
||||
let emissiveAverage = 0;
|
||||
let wiggleAverage = 0;
|
||||
let opacityAverage = 0;
|
||||
let transparencyMin = 0;
|
||||
let hasOpaque = false;
|
||||
let hasAnimation = false;
|
||||
|
||||
const object3d = Object3D.create();
|
||||
const { view, position, direction, up } = object3d;
|
||||
@@ -185,9 +193,11 @@ namespace Scene {
|
||||
renderables.sort(renderableSort);
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
wiggleAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
hasAnimationDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -211,9 +221,11 @@ namespace Scene {
|
||||
boundingSphereVisibleDirty = true;
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
wiggleAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
hasAnimationDirty = true;
|
||||
visibleHash = newVisibleHash;
|
||||
return true;
|
||||
} else {
|
||||
@@ -245,6 +257,18 @@ namespace Scene {
|
||||
return count > 0 ? emissiveAverage / count : 0;
|
||||
}
|
||||
|
||||
function calculateWiggleAverage() {
|
||||
if (primitives.length === 0) return 0;
|
||||
let count = 0;
|
||||
let wiggleAverage = 0;
|
||||
for (let i = 0, il = primitives.length; i < il; ++i) {
|
||||
if (!primitives[i].state.visible) continue;
|
||||
wiggleAverage += primitives[i].values.wiggleAverage.ref.value;
|
||||
count += 1;
|
||||
}
|
||||
return count > 0 ? wiggleAverage / count : 0;
|
||||
}
|
||||
|
||||
function calculateOpacityAverage() {
|
||||
if (primitives.length === 0) return 0;
|
||||
let count = 0;
|
||||
@@ -301,6 +325,22 @@ namespace Scene {
|
||||
return false;
|
||||
}
|
||||
|
||||
function calculateHasAnimation() {
|
||||
for (let i = 0, il = primitives.length; i < il; ++i) {
|
||||
const p = primitives[i];
|
||||
if (!p.state.visible) continue;
|
||||
|
||||
if ((p.values.uWiggleAmplitude?.ref.value > 0 || p.values.wiggleAverage.ref.value > 0) &&
|
||||
p.values.uWiggleSpeed?.ref.value > 0 &&
|
||||
p.values.uWiggleFrequency?.ref.value > 0) return true;
|
||||
|
||||
if (p.values.uTumbleAmplitude?.ref.value > 0 &&
|
||||
p.values.uTumbleSpeed?.ref.value > 0 &&
|
||||
p.values.uTumbleFrequency?.ref.value > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
view, position, direction, up,
|
||||
|
||||
@@ -341,9 +381,11 @@ namespace Scene {
|
||||
}
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
wiggleAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
hasAnimationDirty = true;
|
||||
},
|
||||
add: (o: GraphicsRenderObject) => commitQueue.add(o),
|
||||
remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
|
||||
@@ -401,6 +443,13 @@ namespace Scene {
|
||||
}
|
||||
return emissiveAverage;
|
||||
},
|
||||
get wiggleAverage() {
|
||||
if (wiggleAverageDirty) {
|
||||
wiggleAverage = calculateWiggleAverage();
|
||||
wiggleAverageDirty = false;
|
||||
}
|
||||
return wiggleAverage;
|
||||
},
|
||||
get opacityAverage() {
|
||||
if (opacityAverageDirty) {
|
||||
opacityAverage = calculateOpacityAverage();
|
||||
@@ -422,6 +471,13 @@ namespace Scene {
|
||||
}
|
||||
return hasOpaque;
|
||||
},
|
||||
get hasAnimation() {
|
||||
if (hasAnimationDirty) {
|
||||
hasAnimation = calculateHasAnimation();
|
||||
hasAnimationDirty = false;
|
||||
}
|
||||
return hasAnimation;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -62,6 +62,7 @@ import { clip_instance } from './shader/chunks/clip-instance.glsl';
|
||||
import { clip_pixel } from './shader/chunks/clip-pixel.glsl';
|
||||
import { color_frag_params } from './shader/chunks/color-frag-params.glsl';
|
||||
import { color_vert_params } from './shader/chunks/color-vert-params.glsl';
|
||||
import { common_animation } from './shader/chunks/common-animation.glsl';
|
||||
import { common_clip } from './shader/chunks/common-clip.glsl';
|
||||
import { common_frag_params } from './shader/chunks/common-frag-params.glsl';
|
||||
import { common_vert_params } from './shader/chunks/common-vert-params.glsl';
|
||||
@@ -97,6 +98,7 @@ const ShaderChunks: { [k: string]: string } = {
|
||||
clip_pixel,
|
||||
color_frag_params,
|
||||
color_vert_params,
|
||||
common_animation,
|
||||
common_clip,
|
||||
common_frag_params,
|
||||
common_vert_params,
|
||||
@@ -207,7 +209,7 @@ export const CylindersShaderCode = ShaderCode('cylinders', cylinders_vert, cylin
|
||||
|
||||
import { text_vert } from './shader/text.vert';
|
||||
import { text_frag } from './shader/text.frag';
|
||||
export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { drawBuffers: 'optional' }, {}, ignoreDefineUnlit);
|
||||
export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { fragDepth: 'optional', drawBuffers: 'optional' }, {}, ignoreDefineUnlit);
|
||||
|
||||
import { lines_vert } from './shader/lines.vert';
|
||||
import { lines_frag } from './shader/lines.frag';
|
||||
|
||||
@@ -78,7 +78,7 @@ export const apply_light_color = `
|
||||
}
|
||||
#pragma unroll_loop_end
|
||||
|
||||
outgoingLight += physicalMaterial.diffuseColor * luminance(uAmbientColor);
|
||||
outgoingLight += physicalMaterial.diffuseColor * uAmbientColor;
|
||||
#else
|
||||
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user