mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 05:44:23 +08:00
Compare commits
301 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 | ||
|
|
e859f497f1 | ||
|
|
6a9fed56f3 | ||
|
|
d7c2505852 | ||
|
|
754dfeab91 | ||
|
|
d3b02df5b9 | ||
|
|
3d95ed729c | ||
|
|
9cbb4414e0 | ||
|
|
79fcfe50bc | ||
|
|
216d16456b | ||
|
|
822aaa99b0 | ||
|
|
2c683ab77d | ||
|
|
2ef5af6881 | ||
|
|
36f18be042 | ||
|
|
f093a3ab37 | ||
|
|
74cd42117b | ||
|
|
bb4a4e6102 | ||
|
|
24a3167f9b | ||
|
|
214e1c20ca | ||
|
|
33cab6ddad | ||
|
|
f4b2826bc7 | ||
|
|
ebaa9f2e56 | ||
|
|
812b75a034 | ||
|
|
3b02a5f5ec | ||
|
|
657d2eb1c5 | ||
|
|
25d87dd14d | ||
|
|
d2605e6e3d | ||
|
|
b21ebe0f55 | ||
|
|
2693fe8b7e | ||
|
|
45279a6520 | ||
|
|
22f9b1a7a1 | ||
|
|
8325a58e25 | ||
|
|
0acc508a8f | ||
|
|
2af0cd9d6f | ||
|
|
304858fcba | ||
|
|
ade027911c | ||
|
|
a97e647f7a | ||
|
|
008bed0233 | ||
|
|
bb4c04f3b9 | ||
|
|
62997e5972 | ||
|
|
a20e7bb40d | ||
|
|
2acfac4c85 | ||
|
|
a1a9d87a54 | ||
|
|
1ab71cc487 | ||
|
|
a8b19f5f3c | ||
|
|
4661a4a5f0 | ||
|
|
2c40abc808 | ||
|
|
10d7bcf4c0 | ||
|
|
5f8e4e6913 | ||
|
|
94fa9f124a | ||
|
|
3e70251f38 | ||
|
|
66ed6cfa94 | ||
|
|
d82b6e8d0d | ||
|
|
5a5f6867b9 | ||
|
|
5cd5fc09f5 | ||
|
|
17528d5ca2 | ||
|
|
e658a11947 |
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
|
||||
140
CHANGELOG.md
140
CHANGELOG.md
@@ -4,6 +4,146 @@ 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)
|
||||
- MolViewSpec
|
||||
- Fix `MVSAnnotationStructureComponent` not updating properly when parent structure changes
|
||||
|
||||
## [v5.6.0] - 2026-01-18
|
||||
- Handle Hex codes that are submitted with alpha channels by ignoring the alpha channel (#1746)
|
||||
- Only show "already registered transformer" warnings in non-production builds
|
||||
- Fix `label_seq_id` assignment in PDB parser to use 1-based linear indexing (#1730) if:
|
||||
- when insertion codes are present
|
||||
- `SEQRES` records are present
|
||||
- Viewer app
|
||||
- Add `action: 'focus'` support to `Viewer.structureInteractivity`
|
||||
- Add `viewportFocusBehavior: 'secondary-zoom'`
|
||||
- MolViewSpec
|
||||
- Validation treats `undefined` same as missing value
|
||||
- Increase default size of `carbohydrate` representation
|
||||
- `color_from_uri` and `color_from_source` take `selector` parameter
|
||||
- Add `keepCameraOrientation` option for loading functions
|
||||
- `label_from_*` and `tooltip_from_*` take `text_format` parameter
|
||||
- `label_from_*` take `group_by_fields` parameter
|
||||
- Tweak Gaussian Density smoothness default range (less artefacts)
|
||||
- Support `includeParent` for Gaussian Surface (disables GPU support)
|
||||
- Support floodfill before surface extraction (`off`, `interior`, `exterior`)
|
||||
- For Isosurface, Molecular Surface, Gaussian Surface
|
||||
- Fix `to_mmCIF` writing duplicate categories under certain conditions (#1738)
|
||||
- Add stable random number generator (PCG)
|
||||
- ME grayscale colors; dot offset; SSAO hemisphere vectors
|
||||
- Use blue noise for SSAO hemisphere vectors
|
||||
- Fix SSAO darkening when sampling background/offscreen pixels
|
||||
- Adding structure wireframe visuals on molecular and gaussian surfaces
|
||||
- Fix caching of `__srcIndexArray__`
|
||||
- Prevent self-occlusion on quaternary amine
|
||||
- Fix outline postprocessing artifacts (black bands) on membrane layers at grazing view angles in Illustrative mode (#1749)
|
||||
- Remove fence from `Canvas3D.render` to not interfer with `requestAnimationFrame`
|
||||
- Fix boundingSphere reuse in structure visuals (was triggering extra calculation)
|
||||
- Use PDB seqres record to deduce entity information
|
||||
- Add lipid components names used in amber ff
|
||||
|
||||
## [v5.5.0] - 2025-12-22
|
||||
- Viewer app
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
@@ -33,11 +33,11 @@ npm run build
|
||||
For a watch task to automatically rebuild the source code on changes, run
|
||||
|
||||
```
|
||||
npm run watch
|
||||
npm run dev
|
||||
```
|
||||
|
||||
or if working just with the Viewer app for better performance
|
||||
|
||||
```
|
||||
npm run watch-viewer
|
||||
npm run dev:viewer
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
# Building a Custom Library
|
||||
|
||||
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page.
|
||||
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page using the `esbuild` tool.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -32,6 +32,15 @@ There are 4 basic ways of instantiating the Mol* plugin.
|
||||
- Clone & build the GitHub repository
|
||||
- This option allows for quite straightforward extension customization, e.g., not including movie export, which reduces the bundle size by ~0.5MB
|
||||
|
||||
### Bundle size
|
||||
|
||||
By default, the `Viewer` includes all the available extensions. This increases the bundle size significantly, especially by including the `mp4-export`, which is responsible for almost `0.5MB` of compressed bundle size.
|
||||
It is quite easy to reduce this bundle size by cloning the Mol\* repository, editing [extensions.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/options.ts) and rebuilding it with `npm run build:apps`. The new build will be available
|
||||
in the `build/viewer` directory (the JS file you will find there is uncompressed, but your hosting setup should include automatic gzip compression, significantly reducing the size).
|
||||
|
||||
Alternatively, you can explore building your own "viewer" using the base Mol\* library. For this, see the options below.
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
```HTML
|
||||
|
||||
@@ -1,59 +1,44 @@
|
||||
# Selections
|
||||
|
||||
|
||||
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the following method you can select structural data.
|
||||
## Basic Concepts
|
||||
|
||||
### Selecting directly from the `hierarchy` manager
|
||||
### Location
|
||||
|
||||
One can select a subcomponent's data directly from the plugin manager.
|
||||
The selection model in Mol\* is based on a generic concept called *location*. A location is a pointer to a selectable element within a scene. For example:
|
||||
|
||||
```typescript
|
||||
import { Structure } from '../mol-model/structure';
|
||||
- A structure element location (an atom or a coarse element) is an object composed of `{ structure: Structure, unit: Unit, element: UnitIndex }` (you can think of a `Unit` as a generalized chain)
|
||||
- A bond location is very similar to structure element, requiring pointers to two units and elements
|
||||
- A "shape" (generally a mesh) location consists of pointer to the parent shape and a group of triangles
|
||||
|
||||
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
|
||||
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
|
||||
### Loci
|
||||
|
||||
plugin.managers.camera.focusLoci(ligandLoci);
|
||||
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
|
||||
```
|
||||
Structures and other renderable elements generally consist of many locations and simply using a list of locations would be
|
||||
prohibitively expensive (e.g., large selections in structures with hundreds of thousands of atoms).
|
||||
|
||||
## Selection callbacks
|
||||
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
|
||||
This is why Mol\* introduces
|
||||
the concept of `Loci` — a compressed representation of multiple locations. Instead of having a list of structure element locations (`{ structure: Structure, unit: Unit, element: UnitIndex }[]`), the representation becomes (simplified) `{ structure: Structure, unit: Unit, elements: OrderedSet<UnitIndex> }`. The ordered set can be further compressed for continuous ranges, keeping only the index of the 1st and last element.
|
||||
|
||||
Here's an example of passing in a React "set" function to update selected residue positions.
|
||||
```typescript
|
||||
import {
|
||||
Structure,
|
||||
StructureProperties,
|
||||
} from "molstar/lib/mol-model/structure"
|
||||
// setSelected is assumed to be a "set" function returned by useState
|
||||
// (selected: any[]) => void
|
||||
plugin.behaviors.interaction.click.subscribe(
|
||||
(event: InteractivityManager.ClickEvent) => {
|
||||
const selections = Array.from(
|
||||
plugin.managers.structure.selection.entries.values()
|
||||
);
|
||||
// This bit can be customized to record any piece information you want
|
||||
const localSelected: any[] = [];
|
||||
for (const { structure } of selections) {
|
||||
if (!structure) continue;
|
||||
Structure.eachAtomicHierarchyElement(structure, {
|
||||
residue: (loc) => {
|
||||
const position = StructureProperties.residue.label_seq_id(loc);
|
||||
localSelected.push({ position });
|
||||
},
|
||||
});
|
||||
}
|
||||
setSelected(localSelected);
|
||||
}
|
||||
)
|
||||
```
|
||||
### Bundle
|
||||
|
||||
### `Molscript` language
|
||||
Locations and loci point to the raw JavaScript data structures representing the underlying molecules, making them not serializable in JSON. A *bundle* is a serializable version of the loci.
|
||||
|
||||
Molscript is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
|
||||
### Structure Queries
|
||||
|
||||
### Querying a structure for a specific chain and residue range (select residues with 12<res_id<200 of chain with auth_asym_id==A) :
|
||||
Defining selections directly using the loci would be very cumbersome. For this reason, Mol\* includes the [MolQl query language](https://molql.org) to help define selections.
|
||||
|
||||
|
||||
## Selection Methods
|
||||
|
||||
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the methods you can use to create selections.
|
||||
|
||||
### MolQL (`mol-script`) language
|
||||
|
||||
[MolQL](https://molql.org) (`mol-script`) is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
|
||||
|
||||
**Example:** Querying a structure for a specific chain and residue range
|
||||
|
||||
Select residues with `12<res_id<200 of chain with auth_asym_id=A`
|
||||
|
||||
```typescript
|
||||
import { compileIdListSelection } from 'molstar/lib/mol-script/util/id-list'
|
||||
@@ -62,12 +47,12 @@ const query = compileIdListSelection('A 12-200', 'auth');
|
||||
window.molstar?.managers.structure.selection.fromCompiledQuery('add',query);
|
||||
```
|
||||
|
||||
## Selection Queries
|
||||
### Selection Queries
|
||||
|
||||
Another way to create a selection is via a `SelectionQuery` object. This is a more programmatic way to create a selection. The following example shows how to select a chain and a residue range using a `SelectionQuery` object.
|
||||
This relies on the concept of `Expression` which is basically a intermediate representation between a Molscript statement and a selection query.
|
||||
|
||||
### Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object:
|
||||
**Example:** Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object
|
||||
|
||||
```typescript
|
||||
import { MolScriptBuilder as MS, MolScriptBuilder } from 'molstar/lib/mol-script/language/builder';
|
||||
@@ -107,7 +92,7 @@ var sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
||||
let loci = StructureSelection.toLociWithSourceUnits(sel);
|
||||
```
|
||||
|
||||
## Query Functions
|
||||
### Query Functions
|
||||
|
||||
Instead of building expressions, query functions can be created directly, e.g.:
|
||||
|
||||
@@ -125,7 +110,7 @@ const selection = query(new QueryContext(structure));
|
||||
// ...
|
||||
```
|
||||
|
||||
## Selection Schema
|
||||
### Selection Schema
|
||||
|
||||
For simple selections, the `StructureElement.Schema` can be used to reference elements within a protein structure using mmCIF `atom_site` field names, e.g.:
|
||||
|
||||
@@ -143,6 +128,63 @@ const loci = StructureElement.Loci.fromSchema(structure, residues);
|
||||
|
||||
Usually, a code editor such as VS Code will auto-suggest all the available field names.
|
||||
|
||||
### Using the `hierarchy` manager
|
||||
|
||||
It is possible to select a subcomponent's data directly from the plugin manager.
|
||||
|
||||
```typescript
|
||||
import { Structure } from '../mol-model/structure';
|
||||
|
||||
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
|
||||
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
|
||||
|
||||
plugin.managers.camera.focusLoci(ligandLoci);
|
||||
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
|
||||
```
|
||||
|
||||
## Selection Events
|
||||
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
|
||||
|
||||
Here's an example of passing in a React "set" function to update selected residue positions.
|
||||
```typescript
|
||||
import {
|
||||
Structure,
|
||||
StructureProperties,
|
||||
} from "molstar/lib/mol-model/structure"
|
||||
// setSelected is assumed to be a "set" function returned by useState
|
||||
// (selected: any[]) => void
|
||||
plugin.behaviors.interaction.click.subscribe(
|
||||
(event: InteractivityManager.ClickEvent) => {
|
||||
const selections = Array.from(
|
||||
plugin.managers.structure.selection.entries.values()
|
||||
);
|
||||
// This bit can be customized to record any piece information you want
|
||||
const localSelected: any[] = [];
|
||||
for (const { structure } of selections) {
|
||||
if (!structure) continue;
|
||||
Structure.eachAtomicHierarchyElement(structure, {
|
||||
residue: (loc) => {
|
||||
const position = StructureProperties.residue.label_seq_id(loc);
|
||||
localSelected.push({ position });
|
||||
},
|
||||
});
|
||||
}
|
||||
setSelected(localSelected);
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
|
||||
### `Viewer` app
|
||||
|
||||
The `Viewer` app provides the `structureInteractivity` function which allows easy selection/highlighting of the loaded structure. For example:
|
||||
|
||||
```ts
|
||||
viewer.structureInteractivity({
|
||||
elements: { beg_auth_seq_id: 10, end_auth_seq_id: 50 },
|
||||
action: 'select',
|
||||
});
|
||||
```
|
||||
4238
package-lock.json
generated
4238
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.5.0",
|
||||
"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.50.0",
|
||||
"@typescript-eslint/parser": "^8.50.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.3.1",
|
||||
"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.1",
|
||||
"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.27",
|
||||
"@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,10 +22,10 @@ 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';
|
||||
import { PCG } from '../../../mol-data/util/hash-functions';
|
||||
|
||||
function getHueRange(hue: number, variability: number) {
|
||||
let min = hue - variability;
|
||||
@@ -37,10 +38,11 @@ function getHueRange(hue: number, variability: number) {
|
||||
|
||||
function getGrayscaleColors(count: number, luminance: number, variability: number) {
|
||||
const out: Color[] = [];
|
||||
const pcg = new PCG();
|
||||
for (let i = 0; i < count; ++ i) {
|
||||
const l = saturate(luminance / 100);
|
||||
const v = saturate(variability / 180) * Math.random();
|
||||
const s = Math.random() > 0.5 ? 1 : -1;
|
||||
const v = saturate(variability / 180) * pcg.float();
|
||||
const s = pcg.float() > 0.5 ? 1 : -1;
|
||||
const d = Math.abs(l + s * v) % 1;
|
||||
out[i] = Color.fromNormalizedRgb(d, d, d);
|
||||
}
|
||||
@@ -172,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),
|
||||
@@ -279,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>;
|
||||
|
||||
@@ -316,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>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { loadMVSData, loadMVSX } from '../../extensions/mvs/components/formats';
|
||||
import { loadMVS, MolstarLoadingExtension } from '../../extensions/mvs/load';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { StringLike } from '../../mol-io/common/string-like';
|
||||
import { Structure, StructureElement, StructureSelection } from '../../mol-model/structure';
|
||||
import { Structure, StructureElement } from '../../mol-model/structure';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { OpenFiles } from '../../mol-plugin-state/actions/file';
|
||||
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
|
||||
@@ -38,7 +38,6 @@ import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { MolScriptBuilder } from '../../mol-script/language/builder';
|
||||
import { Expression } from '../../mol-script/language/expression';
|
||||
import { Script } from '../../mol-script/script';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
@@ -49,9 +48,12 @@ import { DefaultViewerOptions, ViewerOptions } from './options';
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { consoleStats, isDebugMode, isProductionMode, isTimingMode, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
|
||||
|
||||
import { decodeColor } from '../../mol-util/color/utils';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ViewerAutoPreset } from './presets';
|
||||
import { decodeColor } from '../../mol-util/color/utils';
|
||||
import { CameraFocusOptions } from '../../mol-plugin-state/manager/camera';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { NoPrimaryFocusLociBindings } from '../../mol-plugin/behavior/dynamic/camera';
|
||||
|
||||
export class Viewer {
|
||||
private _events = new PluginComponent();
|
||||
@@ -80,6 +82,15 @@ export class Viewer {
|
||||
b.transformer !== PluginBehaviors.Camera.FocusLoci
|
||||
&& b.transformer !== PluginBehaviors.Representation.FocusLoci
|
||||
);
|
||||
} else if (o.viewportFocusBehavior === 'secondary-zoom') {
|
||||
baseBehaviors = baseBehaviors.filter(b =>
|
||||
b.transformer !== PluginBehaviors.Camera.FocusLoci
|
||||
&& b.transformer !== PluginBehaviors.Representation.FocusLoci
|
||||
);
|
||||
|
||||
baseBehaviors.push(PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci, {
|
||||
bindings: NoPrimaryFocusLociBindings
|
||||
}));
|
||||
}
|
||||
|
||||
const spec: PluginUISpec = {
|
||||
@@ -472,7 +483,7 @@ export class Viewer {
|
||||
return { model, coords, preset };
|
||||
}
|
||||
|
||||
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
if (format === 'mvsj') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
|
||||
const mvsData = MVSData.fromMVSJ(StringLike.toString(data));
|
||||
@@ -491,7 +502,7 @@ export class Viewer {
|
||||
/** Load MolViewSpec from `data`.
|
||||
* If `format` is 'mvsj', `data` must be a string or a Uint8Array containing a UTF8-encoded string.
|
||||
* If `format` is 'mvsx', `data` must be a Uint8Array or a string containing base64-encoded binary data prefixed with 'base64,'. */
|
||||
loadMvsData(data: string | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
loadMvsData(data: string | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
return loadMVSData(this.plugin, data, format, options);
|
||||
}
|
||||
|
||||
@@ -518,17 +529,19 @@ export class Viewer {
|
||||
|
||||
/**
|
||||
* Triggers structure element selection or highlighting based on the provided
|
||||
* MolScript expression or StructureElement schema.
|
||||
* MolScript expression or StructureElement schema. Focus action will only apply to the
|
||||
* first structure that matches the criteria.
|
||||
*
|
||||
* If neither `expression` nor `elements` are provided, all selections/highlights
|
||||
* will be cleared based on the specified `action`.
|
||||
*/
|
||||
structureInteractivity({ expression, elements, action, applyGranularity = false, filterStructure }: {
|
||||
structureInteractivity({ expression, elements, action, applyGranularity = false, filterStructure, focusOptions }: {
|
||||
expression?: (queryBuilder: typeof MolScriptBuilder) => Expression,
|
||||
elements?: StructureElement.Schema,
|
||||
action: 'highlight' | 'select',
|
||||
action: 'highlight' | 'select' | 'focus',
|
||||
applyGranularity?: boolean,
|
||||
filterStructure?: (structure: Structure) => boolean
|
||||
filterStructure?: (structure: Structure) => boolean,
|
||||
focusOptions?: Partial<CameraFocusOptions>
|
||||
}) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
@@ -548,13 +561,16 @@ export class Viewer {
|
||||
if (filterStructure && !filterStructure(s.obj.data)) continue;
|
||||
|
||||
const loci = expression
|
||||
? StructureSelection.toLociWithSourceUnits(Script.getStructureSelection(expression, s.obj.data))
|
||||
: StructureElement.Schema.toLoci(s.obj.data, elements!);
|
||||
? StructureElement.Loci.fromExpression(s.obj.data, expression)
|
||||
: StructureElement.Loci.fromSchema(s.obj.data, elements!);
|
||||
|
||||
if (action === 'select') {
|
||||
plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
|
||||
} else if (action === 'highlight') {
|
||||
plugin.managers.interactivity.lociHighlights.highlight({ loci }, applyGranularity);
|
||||
} else if (action === 'focus' && !StructureElement.Loci.isEmpty(loci)) {
|
||||
plugin.managers.camera.focusLoci(loci, focusOptions);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -37,17 +37,25 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="controls">
|
||||
<button onclick="selectResidues()">Select Residues 10-50</button>
|
||||
<button onclick="clearSelection()">Clear Selection</button>
|
||||
<button onmouseenter="interactivy('highlight')" onmouseleave="interactivy('clear-highlight')" onclick="interactivy('select')">Select Residues 45-50</button>
|
||||
<button onmouseenter="interactivy('highlight')" onmouseleave="interactivy('clear-highlight')" onclick="interactivy('focus')">Focus</button>
|
||||
<button onclick="interactivy('clear-select')">Clear Selection</button>
|
||||
<div id="selection-info"></div>
|
||||
</div>
|
||||
<script type="text/javascript" src="molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
function selectResidues() {
|
||||
viewer.structureInteractivity({
|
||||
elements: { beg_auth_seq_id: 10, end_auth_seq_id: 50 },
|
||||
action: 'select',
|
||||
});
|
||||
function interactivy(action) {
|
||||
if (action === 'clear-highlight') {
|
||||
viewer.structureInteractivity({ action: 'highlight' });
|
||||
} else if (action === 'clear-select') {
|
||||
viewer.structureInteractivity({ action: 'select' });
|
||||
} else if (action === 'highlight' || action === 'select' || action === 'focus') {
|
||||
viewer.structureInteractivity({
|
||||
elements: { beg_auth_seq_id: 45, end_auth_seq_id: 50 },
|
||||
action,
|
||||
focusOptions: { extraRadius: 3 }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
@@ -64,9 +72,10 @@
|
||||
|
||||
viewportShowExpand: true,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowControls: false,
|
||||
viewportShowAnimation: false,
|
||||
viewportFocusBehavior: 'disabled',
|
||||
viewportBackgroundColor: 'black',
|
||||
viewportFocusBehavior: 'secondary-zoom',
|
||||
viewportBackgroundColor: '#111318',
|
||||
|
||||
pdbProvider: 'rcsb',
|
||||
emdbProvider: 'rcsb',
|
||||
|
||||
@@ -51,7 +51,10 @@ export const DefaultViewerOptions = {
|
||||
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
|
||||
viewportFocusBehavior: 'default' as 'default' | 'disabled',
|
||||
// default: zoom & show structure interaction
|
||||
// secondary-zoom: zoom only, doesn't use primary mouse button
|
||||
// disabled: no automatic zoom or interaction on focus
|
||||
viewportFocusBehavior: 'default' as 'default' | 'secondary-zoom' | 'disabled',
|
||||
viewportBackgroundColor: undefined as string | undefined,
|
||||
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020 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>
|
||||
* @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/');
|
||||
@@ -33,6 +33,14 @@ async function ensureLipidsAvailable() { await ensureAvailable(MARTINI_LIPIDS_PA
|
||||
|
||||
const extraLipids = ['DMPC'];
|
||||
const v2lipids = ['DAPC', 'DBPC', 'DFPC', 'DGPC', 'DIPC', 'DLPC', 'DNPC', 'DOPC', 'DPPC', 'DRPC', 'DTPC', 'DVPC', 'DXPC', 'DYPC', 'LPPC', 'PAPC', 'PEPC', 'PGPC', 'PIPC', 'POPC', 'PRPC', 'PUPC', 'DAPE', 'DBPE', 'DFPE', 'DGPE', 'DIPE', 'DLPE', 'DNPE', 'DOPE', 'DPPE', 'DRPE', 'DTPE', 'DUPE', 'DVPE', 'DXPE', 'DYPE', 'LPPE', 'PAPE', 'PGPE', 'PIPE', 'POPE', 'PQPE', 'PRPE', 'PUPE', 'DAPS', 'DBPS', 'DFPS', 'DGPS', 'DIPS', 'DLPS', 'DNPS', 'DOPS', 'DPPS', 'DRPS', 'DTPS', 'DUPS', 'DVPS', 'DXPS', 'DYPS', 'LPPS', 'PAPS', 'PGPS', 'PIPS', 'POPS', 'PQPS', 'PRPS', 'PUPS', 'DAPG', 'DBPG', 'DFPG', 'DGPG', 'DIPG', 'DLPG', 'DNPG', 'DOPG', 'DPPG', 'DRPG', 'DTPG', 'DVPG', 'DXPG', 'DYPG', 'LPPG', 'PAPG', 'PGPG', 'PIPG', 'POPG', 'PRPG', 'DAPA', 'DBPA', 'DFPA', 'DGPA', 'DIPA', 'DLPA', 'DNPA', 'DOPA', 'DPPA', 'DRPA', 'DTPA', 'DVPA', 'DXPA', 'DYPA', 'LPPA', 'PAPA', 'PGPA', 'PIPA', 'POPA', 'PRPA', 'PUPA', 'DPP', 'DPPI', 'PAPI', 'PIPI', 'POP', 'POPI', 'PUPI', 'PVP', 'PVPI', 'PADG', 'PIDG', 'PODG', 'PUDG', 'PVDG', 'APC', 'CPC', 'IPC', 'LPC', 'OPC', 'PPC', 'TPC', 'UPC', 'VPC', 'BNSM', 'DBSM', 'DPSM', 'DXSM', 'PGSM', 'PNSM', 'POSM', 'PVSM', 'XNSM', 'DPCE', 'DXCE', 'PNCE', 'XNCE'];
|
||||
const amberLipids = [
|
||||
// acyl chains
|
||||
'PA', 'ST', 'OL', 'LEO', 'LEN', 'AR', 'DHA',
|
||||
// head groups
|
||||
'PC', 'PE', 'PS', 'PH-', 'P2-', 'PGR', 'PGS', 'PI',
|
||||
// other
|
||||
'CHL'
|
||||
];
|
||||
|
||||
async function run(out: string) {
|
||||
await ensureLipidsAvailable();
|
||||
@@ -55,13 +63,17 @@ async function run(out: string) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
for (const v of amberLipids) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
const lipidNames = JSON.stringify(lipids.array);
|
||||
|
||||
if (out) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2024 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 lipid params file. Names extracted from Martini FF lipids itp.
|
||||
* Code-generated lipid params file. Names from Martini FF and Amber.
|
||||
*
|
||||
* @author molstar/lipid-params cli
|
||||
*/
|
||||
|
||||
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);
|
||||
@@ -39,7 +38,7 @@ function print(volume: Volume) {
|
||||
}
|
||||
|
||||
async function doMesh(volume: Volume, filename: string) {
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5), wrap: 'auto' })).run();
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5), wrap: 'auto', floodfill: 'off' })).run();
|
||||
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
|
||||
|
||||
// Export the mesh in OBJ format.
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { CameraFogParams, Canvas3DParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { CameraFogParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { TrackballControlsParams } from '../../mol-canvas3d/controls/trackball';
|
||||
import { BackgroundParams } from '../../mol-canvas3d/passes/background';
|
||||
import { BloomParams } from '../../mol-canvas3d/passes/bloom';
|
||||
@@ -15,27 +15,23 @@ import { OutlineParams } from '../../mol-canvas3d/passes/outline';
|
||||
import { ShadowParams } from '../../mol-canvas3d/passes/shadow';
|
||||
import { SsaoParams } from '../../mol-canvas3d/passes/ssao';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { getFocusSnapshot, getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { StateObjectSelector, StateTransform } from '../../mol-state';
|
||||
import { fovAdjustedPosition } from '../../mol-util/camera';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { deepClone } from '../../mol-util/object';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { decodeColor } from './helpers/utils';
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { SnapshotMetadata } from './mvs-data';
|
||||
import { MVSAnimationNode } from './tree/animation/animation-tree';
|
||||
import { MolstarNode, MolstarNodeParams } from './tree/molstar/molstar-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
import { Vector3 } from './tree/mvs/param-types';
|
||||
|
||||
|
||||
const DefaultFocusOptions = {
|
||||
minRadius: 1,
|
||||
extraRadius: 0,
|
||||
};
|
||||
const DefaultCanvasBackgroundColor = ColorNames.white;
|
||||
|
||||
|
||||
@@ -74,31 +70,20 @@ export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: Mols
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is undefined).
|
||||
* Orient the camera based on a focus node params. **/
|
||||
export async function setFocus(plugin: PluginContext, focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]) {
|
||||
const snapshot = getFocusSnapshot(plugin, {
|
||||
...snapshotFocusInfoFromMvsFocuses(focuses),
|
||||
minRadius: DefaultFocusOptions.minRadius,
|
||||
});
|
||||
if (!snapshot) return;
|
||||
resetSceneRadiusFactor(plugin);
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
|
||||
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]): PluginState.SnapshotFocusInfo {
|
||||
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector | undefined, params: MolstarNodeParams<'focus'> & { center?: Vector3 } }[], ignoreOrientation: boolean): PluginState.SnapshotFocusInfo {
|
||||
const lastFocus = (focuses.length > 0) ? focuses[focuses.length - 1] : undefined;
|
||||
const direction = lastFocus?.params.direction ?? MVSTreeSchema.nodes.focus.params.fields.direction.default;
|
||||
const up = lastFocus?.params.up ?? MVSTreeSchema.nodes.focus.params.fields.up.default;
|
||||
return {
|
||||
targets: focuses.map<PluginState.SnapshotFocusTargetInfo>(f => ({
|
||||
targetRef: f.target.ref === '-=root=-' ? undefined : f.target.ref, // need to treat root separately so it does not include invisible structure parts etc.
|
||||
targetRef: f.target?.ref === StateTransform.RootRef ? undefined : f.target?.ref, // need to treat root separately so it does not include invisible structure parts etc.
|
||||
center: f.params.center ? Vec3.create(...f.params.center) : undefined,
|
||||
radius: f.params.radius ?? undefined,
|
||||
radiusFactor: f.params.radius_factor,
|
||||
extraRadius: f.params.radius_extent,
|
||||
})),
|
||||
direction: Vec3.create(...direction),
|
||||
up: Vec3.create(...up),
|
||||
direction: ignoreOrientation ? undefined : Vec3.create(...direction),
|
||||
up: ignoreOrientation ? undefined : Vec3.create(...up),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,24 +96,34 @@ function adjustSceneRadiusFactor(plugin: PluginContext, cameraTarget: Vec3 | und
|
||||
plugin.canvas3d?.setProps({ sceneRadiusFactor });
|
||||
}
|
||||
|
||||
/** Reset `sceneRadiusFactor` property to the default value */
|
||||
function resetSceneRadiusFactor(plugin: PluginContext) {
|
||||
const sceneRadiusFactor = Canvas3DParams.sceneRadiusFactor.defaultValue;
|
||||
plugin.canvas3d?.setProps({ sceneRadiusFactor });
|
||||
}
|
||||
|
||||
/** Create object for PluginState.Snapshot.camera based on tree loading context and MVS snapshot metadata */
|
||||
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }): PluginState.Snapshot['camera'] {
|
||||
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, options: { previousTransitionDurationMs?: number, ignoreCameraOrientation?: boolean }): PluginState.Snapshot['camera'] {
|
||||
const camera: PluginState.Snapshot['camera'] = {
|
||||
transitionStyle: 'animate',
|
||||
transitionDurationInMs: metadata.previousTransitionDurationMs ?? 0,
|
||||
transitionDurationInMs: options.previousTransitionDurationMs ?? 0,
|
||||
};
|
||||
if (context.camera.cameraParams !== undefined) {
|
||||
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
|
||||
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, context.camera.cameraParams);
|
||||
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
|
||||
const cam = context.camera.cameraParams;
|
||||
if (options.ignoreCameraOrientation) {
|
||||
camera.focus = snapshotFocusInfoFromMvsFocuses([{
|
||||
target: undefined,
|
||||
params: {
|
||||
center: cam.target,
|
||||
radius: Vec3.distance(cam.target as number[] as Vec3, cam.position as number[] as Vec3) / 2,
|
||||
direction: MVSTreeSchema.nodes.focus.params.fields.direction.default, // will be ignored
|
||||
up: MVSTreeSchema.nodes.focus.params.fields.up.default, // will be ignored
|
||||
radius_factor: 1, // will be ignored
|
||||
radius_extent: 0, // will be ignored
|
||||
},
|
||||
}], true);
|
||||
// This will not work exactly when viewport height>width because of how focusing works (could be solved by adjusting radius by aspect ration, but that would mess up cropping, and wouldn't work properly when aspect ration changes after loading)
|
||||
} else {
|
||||
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
|
||||
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, cam);
|
||||
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
|
||||
}
|
||||
} else {
|
||||
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses);
|
||||
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses, options.ignoreCameraOrientation ?? false);
|
||||
}
|
||||
return camera;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -9,16 +9,19 @@ import { TextBuilder } from '../../../../mol-geo/geometry/text/text-builder';
|
||||
import { Structure } from '../../../../mol-model/structure';
|
||||
import { ComplexTextVisual, ComplexVisual } from '../../../../mol-repr/structure/complex-visual';
|
||||
import * as Original from '../../../../mol-repr/structure/visual/label-text';
|
||||
import { ElementIterator, eachSerialElement, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
|
||||
import { eachSerialElement, ElementIterator, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
|
||||
import { VisualUpdateState } from '../../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../../mol-repr/visual';
|
||||
import { Theme } from '../../../../mol-theme/theme';
|
||||
import { arrayEqual } from '../../../../mol-util';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { omitObjectKeys } from '../../../../mol-util/object';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { FormatTemplate } from '../../../../mol-util/string-format';
|
||||
import { textPropsForSelection } from '../../helpers/label-text';
|
||||
import { groupRows } from '../../helpers/selections';
|
||||
import { getMVSAnnotationForStructure } from '../annotation-prop';
|
||||
import { MVSAnnotationRow } from '../../helpers/schemas';
|
||||
import { GroupedArray } from '../../helpers/utils';
|
||||
import { getMVSAnnotationForStructure, MVSAnnotation } from '../annotation-prop';
|
||||
|
||||
|
||||
/** Parameter definition for "label-text" visual in "MVS Annotation Label" representation */
|
||||
@@ -26,6 +29,8 @@ export type MVSAnnotationLabelTextParams = typeof MVSAnnotationLabelTextParams
|
||||
export const MVSAnnotationLabelTextParams = {
|
||||
annotationId: PD.Text('', { description: 'Reference to "Annotation" custom model property', isEssential: true }),
|
||||
fieldName: PD.Text('label', { description: 'Annotation field (column) from which to take label contents', isEssential: true }),
|
||||
textFormat: PD.Text('{}', { description: 'Formatting template for the label text. Supports simplified f-string syntax. May reference multiple annotation fields. If value in any field is not defined, label will not be displayed.', isEssential: true }),
|
||||
groupByFields: PD.ObjectList({ fieldName: PD.Text(), }, obj => obj.fieldName, { defaultValue: [{ fieldName: 'group_id' }], description: 'Set of annotation fields for grouping annotation rows into label instances (i.e. annotation rows with the same values in all group-by fields will yield one label instance). Annotation row with undefined value in any group-by field is considered a separate label instance.', isEssential: true }),
|
||||
...omitObjectKeys(Original.LabelTextParams, ['level', 'chainScale', 'residueScale', 'elementScale']),
|
||||
borderColor: { ...Original.LabelTextParams.borderColor, defaultValue: ColorNames.black },
|
||||
};
|
||||
@@ -42,7 +47,11 @@ export function MVSAnnotationLabelTextVisual(materialId: number): ComplexVisual<
|
||||
getLoci: getSerialElementLoci,
|
||||
eachLocation: eachSerialElement,
|
||||
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<MVSAnnotationLabelTextParams>, currentProps: PD.Values<MVSAnnotationLabelTextParams>) => {
|
||||
state.createGeometry = newProps.annotationId !== currentProps.annotationId || newProps.fieldName !== currentProps.fieldName;
|
||||
state.createGeometry =
|
||||
newProps.annotationId !== currentProps.annotationId
|
||||
|| newProps.fieldName !== currentProps.fieldName
|
||||
|| newProps.textFormat !== currentProps.textFormat
|
||||
|| !arrayEqual(newProps.groupByFields, currentProps.groupByFields);
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
@@ -50,16 +59,32 @@ export function MVSAnnotationLabelTextVisual(materialId: number): ComplexVisual<
|
||||
function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme, props: MVSAnnotationLabelTextProps, text?: Text): Text {
|
||||
const { annotation, model } = getMVSAnnotationForStructure(structure, props.annotationId);
|
||||
const rows = annotation?.getRows() ?? [];
|
||||
const { count, offsets, grouped } = groupRows(rows);
|
||||
const builder = TextBuilder.create(props, count, count / 2, text);
|
||||
for (let iGroup = 0; iGroup < count; iGroup++) {
|
||||
const iFirstRowInGroup = grouped[offsets[iGroup]];
|
||||
const labelText = annotation!.getValueForRow(iFirstRowInGroup, props.fieldName);
|
||||
const groups = GroupedArray.groupIndices(rows, rowGroupingFunction(annotation!, props.groupByFields.map(x => x.fieldName)));
|
||||
const builder = TextBuilder.create(props, groups.count, groups.count / 2, text);
|
||||
const template = FormatTemplate(props.textFormat);
|
||||
for (let iGroup = 0; iGroup < groups.count; iGroup++) {
|
||||
const rowIndicesInGroup = GroupedArray.getGroup(groups, iGroup);
|
||||
const labelText = template.format(field => annotation!.getValueForRow(rowIndicesInGroup[0], field || props.fieldName));
|
||||
if (!labelText) continue;
|
||||
const rowsInGroup = grouped.slice(offsets[iGroup], offsets[iGroup + 1]).map(j => rows[j]);
|
||||
const rowsInGroup = rowIndicesInGroup.map(i => rows[i]);
|
||||
const p = textPropsForSelection(structure, rowsInGroup, model);
|
||||
if (!p) continue;
|
||||
builder.add(labelText, p.center[0], p.center[1], p.center[2], p.depth, p.scale, p.group);
|
||||
}
|
||||
return builder.getText();
|
||||
}
|
||||
|
||||
function rowGroupingFunction(annotation: MVSAnnotation, groupByFields: string[]): (row: MVSAnnotationRow, i: number) => string | undefined {
|
||||
if (groupByFields.length === 1) {
|
||||
const groupByField = groupByFields[0];
|
||||
return (row, i) => annotation.getValueForRow(i, groupByField);
|
||||
}
|
||||
if (groupByFields.length === 0) {
|
||||
return () => '';
|
||||
}
|
||||
return (row, i) => {
|
||||
const values = groupByFields.map(field => annotation.getValueForRow(i, field));
|
||||
if (values.includes(undefined)) return undefined;
|
||||
return values.join('\t');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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>
|
||||
*/
|
||||
@@ -45,11 +45,11 @@ export const MVSAnnotationStructureComponent = MVSTransform({
|
||||
to: SO.Molecule.Structure,
|
||||
params: MVSAnnotationStructureComponentParams,
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return createMVSAnnotationStructureComponent(a.data, params);
|
||||
apply({ a, params, cache }) {
|
||||
return createMVSAnnotationStructureComponent(a.data, params, cache as MVSComponentCache);
|
||||
},
|
||||
update: ({ a, b, oldParams, newParams }) => {
|
||||
return updateMVSAnnotationStructureComponent(a.data, b, oldParams, newParams);
|
||||
update: ({ a, b, oldParams, newParams, cache }) => {
|
||||
return updateMVSAnnotationStructureComponent(a.data, b, oldParams, newParams, cache as MVSComponentCache);
|
||||
},
|
||||
dispose({ b }) {
|
||||
b?.data.customPropertyDescriptors.dispose();
|
||||
@@ -75,8 +75,11 @@ export function createMVSAnnotationSubstructure(structure: Structure, params: MV
|
||||
}
|
||||
}
|
||||
|
||||
interface MVSComponentCache { source?: Structure }
|
||||
|
||||
/** Create a substructure PSO based on `MVSAnnotationStructureComponentProps` */
|
||||
export function createMVSAnnotationStructureComponent(structure: Structure, params: MVSAnnotationStructureComponentProps) {
|
||||
export function createMVSAnnotationStructureComponent(structure: Structure, params: MVSAnnotationStructureComponentProps, cache: MVSComponentCache) {
|
||||
cache.source = structure;
|
||||
const component = createMVSAnnotationSubstructure(structure, params);
|
||||
|
||||
if (params.nullIfEmpty && component.elementCount === 0) return StateObject.Null;
|
||||
@@ -102,15 +105,23 @@ export function createMVSAnnotationStructureComponent(structure: Structure, para
|
||||
}
|
||||
|
||||
/** Update a substructure PSO based on `MVSAnnotationStructureComponentProps` */
|
||||
export function updateMVSAnnotationStructureComponent(a: Structure, b: SO.Molecule.Structure, oldParams: MVSAnnotationStructureComponentProps, newParams: MVSAnnotationStructureComponentProps) {
|
||||
const change = !deepEqual(newParams, oldParams);
|
||||
const needsRecreate = !deepEqual(omitObjectKeys(newParams, ['label']), omitObjectKeys(oldParams, ['label']));
|
||||
if (!change) {
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
export function updateMVSAnnotationStructureComponent(a: Structure, b: SO.Molecule.Structure, oldParams: MVSAnnotationStructureComponentProps, newParams: MVSAnnotationStructureComponentProps, cache: MVSComponentCache) {
|
||||
const structureChanged = !cache.source || !Structure.areEquivalent(a, cache.source);
|
||||
cache.source = a;
|
||||
if (structureChanged) {
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
}
|
||||
if (!needsRecreate) {
|
||||
|
||||
const coreParamsChanged = !deepEqual(omitObjectKeys(newParams, ['label']), omitObjectKeys(oldParams, ['label']));
|
||||
if (coreParamsChanged) {
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
}
|
||||
|
||||
const labelChanged = newParams.label !== oldParams.label;
|
||||
if (labelChanged) {
|
||||
b.label = newParams.label || b.label;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
}
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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>
|
||||
*/
|
||||
@@ -11,6 +11,7 @@ import { Loci } from '../../../mol-model/loci';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { FormatTemplate } from '../../../mol-util/string-format';
|
||||
import { filterDefined } from '../helpers/utils';
|
||||
import { MVSAnnotationsProvider } from './annotation-prop';
|
||||
|
||||
@@ -21,6 +22,7 @@ export const MVSAnnotationTooltipsParams = {
|
||||
{
|
||||
annotationId: PD.Text('', { description: 'Reference to "MVS Annotation" custom model property' }),
|
||||
fieldName: PD.Text('tooltip', { description: 'Annotation field (column) from which to take color values' }),
|
||||
textFormat: PD.Text('{}', { description: 'Formatting template for tooltip text. Supports simplified f-string syntax. May reference multiple annotation fields. If value in any field is not defined, tooltip will not be displayed.' }),
|
||||
},
|
||||
obj => `${obj.annotationId}:${obj.fieldName}`
|
||||
),
|
||||
@@ -59,7 +61,9 @@ export const MVSAnnotationTooltipsLabelProvider = {
|
||||
const tooltipProps = MVSAnnotationTooltipsProvider.get(location.structure).value;
|
||||
if (!tooltipProps || tooltipProps.tooltips.length === 0) return undefined;
|
||||
const annotations = MVSAnnotationsProvider.get(location.unit.model).value;
|
||||
const texts = tooltipProps.tooltips.map(p => annotations?.getAnnotation(p.annotationId)?.getValueForLocation(location, p.fieldName));
|
||||
const texts = tooltipProps.tooltips.map(p =>
|
||||
FormatTemplate(p.textFormat).format(field => annotations?.getAnnotation(p.annotationId)?.getValueForLocation(location, field || p.fieldName))
|
||||
);
|
||||
return filterDefined(texts).join(' | ');
|
||||
default:
|
||||
return undefined;
|
||||
|
||||
@@ -61,6 +61,7 @@ export const ParseMVSX = MVSTransform({
|
||||
export const LoadMvsDataParams = {
|
||||
appendSnapshots: PD.Boolean(false, { description: 'If true, add snapshots from MVS into current snapshot list; if false, replace the snapshot list.' }),
|
||||
keepCamera: PD.Boolean(false, { description: 'If true, any camera positioning from the MVS state will be ignored and the current camera position will be kept.' }),
|
||||
keepCameraOrientation: PD.Boolean(false, { description: 'If true, any camera orientation from the MVS state will be ignored and the current camera orientation will be kept (camera target position will be loaded from MVS). keepCamera option overrides this.' }),
|
||||
applyExtensions: PD.Boolean(true, { description: 'If true, apply builtin MVS-loading extensions (not a part of standard MVS specification).' }),
|
||||
};
|
||||
|
||||
@@ -71,7 +72,7 @@ export const LoadMvsData = StateAction.build({
|
||||
params: LoadMvsDataParams,
|
||||
})(({ a, params }, plugin: PluginContext) => Task.create('Load MVS Data', async () => {
|
||||
const { mvsData, sourceUrl } = a.data;
|
||||
await loadMVS(plugin, mvsData, { appendSnapshots: params.appendSnapshots, keepCamera: params.keepCamera, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
|
||||
await loadMVS(plugin, mvsData, { appendSnapshots: params.appendSnapshots, keepCamera: params.keepCamera, keepCameraOrientation: params.keepCameraOrientation, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
|
||||
}));
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ElementSet = {
|
||||
/** Return a substructure of `structure` defined by `selector` */
|
||||
export function substructureFromSelector(structure: Structure, selector: Selector): Structure {
|
||||
const pso = (selector.name === 'annotation') ?
|
||||
createMVSAnnotationStructureComponent(structure, { ...selector.params, label: '', nullIfEmpty: false })
|
||||
createMVSAnnotationStructureComponent(structure, { ...selector.params, label: '', nullIfEmpty: false }, {})
|
||||
: createStructureComponent(structure, { type: selector, label: '', nullIfEmpty: false }, { source: structure });
|
||||
return PluginStateObject.Molecule.Structure.is(pso) ? pso.data : Structure.Empty;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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>
|
||||
*/
|
||||
|
||||
import { range } from '../../../../mol-util/array';
|
||||
import { MVSAnnotationRow } from '../schemas';
|
||||
import { groupRows } from '../selections';
|
||||
import { GroupedArray } from '../utils';
|
||||
|
||||
|
||||
describe('groupRows', () => {
|
||||
it('groupRows', async () => {
|
||||
describe('GroupedArray', () => {
|
||||
it('GroupedArray.groupIndices', async () => {
|
||||
const rows = [
|
||||
{ label: 'A' }, { label: 'B', group_id: 1 }, { label: 'C', group_id: 'x' }, { label: 'D', group_id: 1 },
|
||||
{ label: 'E' }, { label: 'F' }, { label: 'G', group_id: 'x' }, { label: 'H', group_id: 'x' },
|
||||
] as any as MVSAnnotationRow[];
|
||||
const g = groupRows(rows);
|
||||
const g = GroupedArray.groupIndices(rows, row => row.group_id);
|
||||
const groupedIndices = range(g.count).map(i => g.grouped.slice(g.offsets[i], g.offsets[i + 1]));
|
||||
const groupedRows = groupedIndices.map(group => group.map(j => rows[j]));
|
||||
expect(groupedRows).toEqual([
|
||||
@@ -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>
|
||||
@@ -447,43 +447,3 @@ export function rowsToExpression(rows: readonly MVSAnnotationRow[]): Expression
|
||||
items: rows as StructureElement.SchemaItem[]
|
||||
});
|
||||
}
|
||||
|
||||
/** Data structure for an array divided into contiguous groups */
|
||||
interface GroupedArray<T> {
|
||||
/** Number of groups */
|
||||
count: number,
|
||||
/** Get size of i-th group as `offsets[i+1]-offsets[i]`.
|
||||
* Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
offsets: number[],
|
||||
/** Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
grouped: T[],
|
||||
}
|
||||
|
||||
/** Return row indices grouped by `row.group_id`. Rows with `row.group_id===undefined` are treated as separate groups. */
|
||||
export function groupRows(rows: readonly MVSAnnotationRow[]): GroupedArray<number> {
|
||||
let counter = 0;
|
||||
const groupMap = new Map<string, number>();
|
||||
const groups: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const group_id = rows[i].group_id;
|
||||
if (!isDefined(group_id)) {
|
||||
groups.push(counter++);
|
||||
} else {
|
||||
const groupIndex = groupMap.get(group_id);
|
||||
if (groupIndex === undefined) {
|
||||
groupMap.set(group_id, counter);
|
||||
groups.push(counter);
|
||||
counter++;
|
||||
} else {
|
||||
groups.push(groupIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rowIndices = range(rows.length).sort((i, j) => groups[i] - groups[j]);
|
||||
const offsets: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (i === 0 || groups[rowIndices[i]] !== groups[rowIndices[i - 1]]) offsets.push(i);
|
||||
}
|
||||
offsets.push(rowIndices.length);
|
||||
return { count: offsets.length - 1, offsets, grouped: rowIndices };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { hashString } from '../../../mol-data/util';
|
||||
import { StateObject } from '../../../mol-state';
|
||||
import { range } from '../../../mol-util/array';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { decodeColor as _decodeColor } from '../../../mol-util/color/utils';
|
||||
|
||||
@@ -149,4 +150,50 @@ export function getMVSReferenceObject<T extends StateObject.Ctor>(type: T[], dep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Data structure for an array divided into contiguous groups */
|
||||
export interface GroupedArray<T> {
|
||||
/** Number of groups */
|
||||
count: number,
|
||||
/** Get size of i-th group as `offsets[i+1]-offsets[i]`.
|
||||
* Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
offsets: number[],
|
||||
/** Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
grouped: T[],
|
||||
}
|
||||
|
||||
export const GroupedArray = {
|
||||
getGroup<T>(groupedArray: GroupedArray<T>, iGroup: number): T[] {
|
||||
return groupedArray.grouped.slice(groupedArray.offsets[iGroup], groupedArray.offsets[iGroup + 1]);
|
||||
},
|
||||
/** Return element indices grouped by `group_by(element, index)`. Elements with `group_by(element, index)===undefined` are treated as separate groups. */
|
||||
groupIndices<T>(elements: readonly T[], group_by: (element: T, index: number) => string | undefined): GroupedArray<number> {
|
||||
let counter = 0;
|
||||
const groupMap = new Map<string, number>();
|
||||
const groups: number[] = [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const groupId = group_by(elements[i], i);
|
||||
if (!isDefined(groupId)) {
|
||||
groups.push(counter++);
|
||||
} else {
|
||||
const groupIndex = groupMap.get(groupId);
|
||||
if (groupIndex === undefined) {
|
||||
groupMap.set(groupId, counter);
|
||||
groups.push(counter);
|
||||
counter++;
|
||||
} else {
|
||||
groups.push(groupIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
const elementIndices = range(elements.length).sort((i, j) => groups[i] - groups[j]);
|
||||
const offsets: number[] = [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (i === 0 || groups[elementIndices[i]] !== groups[elementIndices[i - 1]]) offsets.push(i);
|
||||
}
|
||||
offsets.push(elementIndices.length);
|
||||
return { count: offsets.length - 1, offsets, grouped: elementIndices };
|
||||
},
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
@@ -19,7 +19,7 @@ import { ColorListEntry } from '../../mol-util/color/color';
|
||||
import { canonicalJsonString } from '../../mol-util/json';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { MVSAnnotationColorThemeProps, MVSAnnotationColorThemeProvider, MVSCategoricalPaletteProps, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
import { MVSAnnotationLabelProps, MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
import { MVSAnnotationSpec } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponentProps } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProps } from './components/annotation-tooltips-prop';
|
||||
@@ -196,7 +196,7 @@ export function collectAnnotationTooltips(tree: MolstarSubtree<'structure'>, con
|
||||
if (node.kind === 'tooltip_from_uri' || node.kind === 'tooltip_from_source') {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
if (annotationId) {
|
||||
annotationTooltips.push({ annotationId, fieldName: node.params.field_name });
|
||||
annotationTooltips.push({ annotationId, fieldName: node.params.field_name, textFormat: node.params.text_format });
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -338,11 +338,13 @@ export function prettyNameFromSelector(selector?: MolstarNodeParams<'component'>
|
||||
|
||||
/** Create props for `StructureRepresentation3D` transformer from a label_from_* node. */
|
||||
export function labelFromXProps(node: MolstarNode<'label_from_uri' | 'label_from_source'>, context: MolstarLoadingContext): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
const annotationId = context.annotationMap.get(node)!;
|
||||
const fieldName = node.params.field_name;
|
||||
const textFormat = node.params.text_format;
|
||||
const groupBy = node.params.group_by_fields ?? [node.params.field_remapping.group_id ?? 'group_id'];
|
||||
const nearestReprNode = context.nearestReprMap?.get(node);
|
||||
return {
|
||||
type: { name: MVSAnnotationLabelRepresentationProvider.name, params: { annotationId, fieldName } },
|
||||
type: { name: MVSAnnotationLabelRepresentationProvider.name, params: { annotationId, fieldName, textFormat, groupByFields: groupBy.map(x => ({ fieldName: x })) } satisfies Partial<MVSAnnotationLabelProps> },
|
||||
colorTheme: colorThemeForNode(nearestReprNode, context),
|
||||
};
|
||||
}
|
||||
@@ -376,7 +378,8 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
};
|
||||
case 'ball_and_stick':
|
||||
return {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: (params.size_factor ?? 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'line':
|
||||
return {
|
||||
@@ -390,7 +393,8 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
};
|
||||
case 'carbohydrate':
|
||||
return {
|
||||
type: { name: 'carbohydrate', params: { alpha, sizeFactor: params.size_factor ?? 1 } },
|
||||
type: { name: 'carbohydrate', params: { alpha, sizeFactor: 1.75 } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'surface': {
|
||||
return {
|
||||
@@ -401,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');
|
||||
}
|
||||
@@ -520,7 +533,7 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
c => {
|
||||
const theme = colorThemeForNode(c, context);
|
||||
if (!theme) return undefined;
|
||||
return { theme, selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) };
|
||||
return { theme, selection: componentPropsFromSelector(c.params.selector) };
|
||||
}
|
||||
).filter(t => !!t);
|
||||
return {
|
||||
@@ -555,11 +568,7 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
}
|
||||
|
||||
function appliesColorToWholeRepr(node: MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>): boolean {
|
||||
if (node.kind === 'color') {
|
||||
return !isDefined(node.params.selector) || node.params.selector === 'all';
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return !isDefined(node.params.selector) || node.params.selector === 'all';
|
||||
}
|
||||
|
||||
const FALLBACK_COLOR = decodeColor(DefaultColor)!;
|
||||
|
||||
@@ -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';
|
||||
@@ -46,6 +47,8 @@ export interface MVSLoadOptions {
|
||||
appendSnapshots?: boolean,
|
||||
/** Ignore any camera positioning from the MVS state and keep the current camera position instead, ignore any camera positioning when generating snapshots. */
|
||||
keepCamera?: boolean,
|
||||
/** Follow camera target position from the MVS state but keep the current camera direction and up. (`keepCamera` option overrides this) */
|
||||
keepCameraOrientation?: boolean,
|
||||
/** Specifies a set of MVS-loading extensions (not a part of standard MVS specification). If undefined, apply all builtin extensions. If `[]`, do not apply builtin extensions. */
|
||||
extensions?: MolstarLoadingExtension<any>[],
|
||||
/** Run some sanity checks and print potential issues to the console. */
|
||||
@@ -175,17 +178,24 @@ function molstarTreeToEntry(
|
||||
tree: MolstarTree,
|
||||
animation: MVSAnimationNode<'animation'> | undefined,
|
||||
metadata: SnapshotMetadata & { previousTransitionDurationMs?: number },
|
||||
options: { keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }
|
||||
options: { keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }
|
||||
) {
|
||||
const context = MolstarLoadingContext.create();
|
||||
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { replaceExisting: true, extensions: options?.extensions ?? BuiltinLoadingExtensions });
|
||||
snapshot.canvas3d = {
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, animation) : undefined,
|
||||
};
|
||||
if (!options?.keepCamera) {
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
|
||||
if (options?.keepCamera) {
|
||||
// do nothing
|
||||
} else if (options.keepCameraOrientation) {
|
||||
// load camera target, keep orientation
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs, ignoreCameraOrientation: true });
|
||||
} else {
|
||||
// fully load camera
|
||||
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;
|
||||
@@ -297,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':
|
||||
@@ -494,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V
|
||||
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
|
||||
* Return description of validation issues, if `value` has wrong type. */
|
||||
export function fieldValidationIssues<F extends Field>(field: F, value: any): string[] | undefined {
|
||||
if (value === undefined && !field.required) return undefined; // Value undefined treated as if field not even present (unlike null)
|
||||
const validation = field.type.decode(value);
|
||||
if (validation._tag === 'Right') {
|
||||
return undefined;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -13,6 +13,8 @@ import { MVSClipParams, MVSRepresentationParams, MVSVolumeRepresentationParams }
|
||||
import { ColorT, ComponentExpressionT, ComponentSelectorT, LabelAttachments, Matrix, Palette, ParseFormatT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
|
||||
|
||||
|
||||
const SelectorT = union(ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT));
|
||||
|
||||
const _DataFromUriParams = {
|
||||
/** URL of the annotation resource. */
|
||||
uri: RequiredField(str, 'URL of the annotation resource.'),
|
||||
@@ -139,7 +141,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Defines what part of the parent structure should be included in this component. */
|
||||
selector: RequiredField(union(ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)), 'Defines what part of the parent structure should be included in this component.'),
|
||||
selector: RequiredField(SelectorT, 'Defines what part of the parent structure should be included in this component.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to create a component defined by an external annotation resource. */
|
||||
@@ -195,7 +197,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
color: OptionalField(ColorT, DefaultColor, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
/** Defines to what part of the representation this color should be applied. */
|
||||
selector: OptionalField(union(ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)), 'all', 'Defines to what part of the representation this color should be applied.'),
|
||||
selector: OptionalField(SelectorT, 'all', 'Defines to what part of the representation this color should be applied.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
|
||||
@@ -208,6 +210,8 @@ export const MVSTreeSchema = TreeSchema({
|
||||
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
|
||||
/** Customize mapping of annotation values to colors. */
|
||||
palette: OptionalField(nullable(Palette), null, 'Customize mapping of annotation values to colors.'),
|
||||
/** Defines to what part of the representation this coloring should be applied. */
|
||||
selector: OptionalField(SelectorT, 'all', 'Defines to what part of the representation this coloring should be applied.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
@@ -220,6 +224,8 @@ export const MVSTreeSchema = TreeSchema({
|
||||
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
|
||||
/** Customize mapping of annotation values to colors. */
|
||||
palette: OptionalField(nullable(Palette), null, 'Customize mapping of annotation values to colors.'),
|
||||
/** Defines to what part of the representation this coloring should be applied. */
|
||||
selector: OptionalField(SelectorT, 'all', 'Defines to what part of the representation this coloring should be applied.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to apply clipping to a visual representation. */
|
||||
@@ -254,6 +260,10 @@ export const MVSTreeSchema = TreeSchema({
|
||||
..._DataFromUriParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
|
||||
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
|
||||
/** Formatting template for the label text. Supports simplified f-string syntax. */
|
||||
text_format: OptionalField(str, '{}', 'Formatting template for the label text. Supports simplified f-string syntax.'),
|
||||
/** Set of annotation fields for grouping annotation rows into label instances (i.e. annotation rows with the same values in all group-by fields will yield one label instance). Annotation row with undefined value in any group-by field is considered a separate label instance. */
|
||||
group_by_fields: OptionalField(nullable(list(str)), null, 'Set of annotation fields for grouping annotation rows into label instances (i.e. annotation rows with the same values in all group-by fields will yield one label instance). Annotation row with undefined value in any group-by field is considered a separate label instance.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
@@ -264,6 +274,10 @@ export const MVSTreeSchema = TreeSchema({
|
||||
..._DataFromSourceParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
|
||||
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
|
||||
/** Formatting template for the label text. Supports simplified f-string syntax. */
|
||||
text_format: OptionalField(str, '{}', 'Formatting template for the label text. Supports simplified f-string syntax.'),
|
||||
/** Set of annotation fields for grouping annotation rows into label instances (i.e. annotation rows with the same values in all group-by fields will yield one label instance). Annotation row with undefined value in any group-by field is considered a separate label instance. */
|
||||
group_by_fields: OptionalField(nullable(list(str)), null, 'Set of annotation fields for grouping annotation rows into label instances (i.e. annotation rows with the same values in all group-by fields will yield one label instance). Annotation row with undefined value in any group-by field is considered a separate label instance.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component). */
|
||||
@@ -283,6 +297,8 @@ export const MVSTreeSchema = TreeSchema({
|
||||
..._DataFromUriParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
|
||||
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
|
||||
/** Formatting template for the tooltip text. Supports simplified f-string syntax. */
|
||||
text_format: OptionalField(str, '{}', 'Formatting template for the tooltip text. Supports simplified f-string syntax.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
@@ -293,6 +309,8 @@ export const MVSTreeSchema = TreeSchema({
|
||||
..._DataFromSourceParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
|
||||
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
|
||||
/** Formatting template for the tooltip text. Supports simplified f-string syntax. */
|
||||
text_format: OptionalField(str, '{}', 'Formatting template for the tooltip text. Supports simplified f-string syntax.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to set the camera focus to a component (zoom in). */
|
||||
|
||||
@@ -28,7 +28,7 @@ const ValidationColorTable: [string, Color][] = [
|
||||
['One Issue', ValidationColors[2]],
|
||||
['Two Issues', ValidationColors[3]],
|
||||
['Three Or More Issues', ValidationColors[4]],
|
||||
['Not Applicable', ValidationColors[9]]
|
||||
['Not Applicable', ValidationColors[0]],
|
||||
];
|
||||
|
||||
export const StructureQualityReportColorThemeParams = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -564,7 +565,6 @@ namespace Canvas3D {
|
||||
}
|
||||
}),
|
||||
xrManager.sessionChanged.subscribe(() => {
|
||||
fenceSync = null;
|
||||
resizeRequested = true;
|
||||
if (xrManager.session) {
|
||||
saveNonXRProps();
|
||||
@@ -646,8 +646,6 @@ namespace Canvas3D {
|
||||
return changed;
|
||||
}
|
||||
|
||||
let fenceSync: WebGLSync | null = null;
|
||||
|
||||
function render(force: boolean, xrFrame?: XRFrame) {
|
||||
if (webgl.isContextLost) return false;
|
||||
if (webgl.xr.session && !xrFrame) return false;
|
||||
@@ -664,14 +662,6 @@ namespace Canvas3D {
|
||||
y > drs.height || y + height < 0
|
||||
) return false;
|
||||
|
||||
if (fenceSync !== null && !xrFrame) {
|
||||
if (webgl.checkSyncStatus(fenceSync)) {
|
||||
fenceSync = null;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (xrFrame) {
|
||||
setXRProps();
|
||||
p.transparentBackground = xrPassthrough;
|
||||
@@ -685,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) {
|
||||
@@ -735,10 +726,6 @@ namespace Canvas3D {
|
||||
}
|
||||
}
|
||||
|
||||
if (didRender && !xrFrame) {
|
||||
fenceSync = webgl.getFenceSync();
|
||||
}
|
||||
|
||||
return didRender;
|
||||
}
|
||||
|
||||
@@ -768,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
|
||||
@@ -1093,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 },
|
||||
@@ -1102,7 +1089,6 @@ namespace Canvas3D {
|
||||
|
||||
const contextLostSub = contextLost?.subscribe(() => {
|
||||
isContextLost = true;
|
||||
fenceSync = null;
|
||||
pickHelper.dirty = true;
|
||||
});
|
||||
|
||||
@@ -1230,10 +1216,6 @@ namespace Canvas3D {
|
||||
reprRenderObjects.clear();
|
||||
scene.clear();
|
||||
helper.debug.clear();
|
||||
if (fenceSync !== null) {
|
||||
webgl.deleteSync(fenceSync);
|
||||
fenceSync = null;
|
||||
}
|
||||
requestDraw();
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
},
|
||||
@@ -1368,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);
|
||||
@@ -1419,6 +1400,7 @@ namespace Canvas3D {
|
||||
get interaction() {
|
||||
return interactionHelper.events;
|
||||
},
|
||||
debugRegistry: helper.debug,
|
||||
xr,
|
||||
dispose: () => {
|
||||
contextLostSub?.unsubscribe();
|
||||
@@ -1446,11 +1428,6 @@ namespace Canvas3D {
|
||||
rayHelper.dispose();
|
||||
xrManager.dispose();
|
||||
|
||||
if (fenceSync !== null) {
|
||||
webgl.deleteSync(fenceSync);
|
||||
fenceSync = null;
|
||||
}
|
||||
|
||||
reprCount.complete();
|
||||
interactionEvent.complete();
|
||||
didDraw.complete();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -110,6 +110,7 @@ export class ImagePass {
|
||||
Camera.copySnapshot(this._camera.state, this.camera.state);
|
||||
Viewport.set(this._camera.viewport, 0, 0, this._width, this._height);
|
||||
this._camera.update();
|
||||
this.renderer.setOcclusionTest(null);
|
||||
|
||||
const ctx = { renderer: this.renderer, camera: this._camera, scene: this.scene, helper: this.helper };
|
||||
if (this.illuminationPass.supported && this.props.illumination.enabled) {
|
||||
|
||||
@@ -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>
|
||||
@@ -27,6 +27,7 @@ import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
import { PCG } from '../../mol-data/util/hash-functions';
|
||||
|
||||
export const SsaoParams = {
|
||||
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
|
||||
@@ -483,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');
|
||||
@@ -681,26 +700,73 @@ function getBlurKernel(kernelSize: number): number[] {
|
||||
return kernel;
|
||||
}
|
||||
|
||||
const RandomHemisphereVector: Vec3[] = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const pcg = new PCG();
|
||||
function getRandomHemisphereVector(): Vec3 {
|
||||
const v = Vec3();
|
||||
v[0] = Math.random() * 2.0 - 1.0;
|
||||
v[1] = Math.random() * 2.0 - 1.0;
|
||||
v[2] = Math.random();
|
||||
Vec3.normalize(v, v);
|
||||
Vec3.scale(v, v, Math.random());
|
||||
RandomHemisphereVector.push(v);
|
||||
while (true) {
|
||||
const x = pcg.float() * 2 - 1;
|
||||
const y = pcg.float() * 2 - 1;
|
||||
if (x * x + y * y < 1) {
|
||||
const z = 2 * Math.sqrt(1 - x * x - y * y) * (pcg.float() < 0.5 ? -1 : 1);
|
||||
Vec3.set(v, x, y, z);
|
||||
Vec3.normalize(v, v);
|
||||
Vec3.scale(v, v, pcg.float());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (v[2] < 0) v[2] = -v[2];
|
||||
return v;
|
||||
}
|
||||
|
||||
function generateBlueNoiseVectors(count: number, out: Vec3[]) {
|
||||
if (out.length >= count) return out;
|
||||
if (out.length === 0) out.push(getRandomHemisphereVector());
|
||||
|
||||
const candidateCount = Math.max(10, Math.min(30, Math.floor(count / 10)));
|
||||
|
||||
for (let i = out.length; i < count; i++) {
|
||||
let bestCandidate: Vec3;
|
||||
let bestDistance = -1;
|
||||
|
||||
for (let j = 0; j < candidateCount; j++) {
|
||||
const candidate = getRandomHemisphereVector();
|
||||
|
||||
let minDistance = Infinity;
|
||||
for (const existingVector of out) {
|
||||
const distance = Vec3.distance(candidate, existingVector);
|
||||
minDistance = Math.min(minDistance, distance);
|
||||
}
|
||||
|
||||
if (minDistance > bestDistance) {
|
||||
bestDistance = minDistance;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
out.push(bestCandidate!);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
let _RandomHemisphereVectors: Vec3[] = [];
|
||||
function getRandomHemisphereVectors(count: number): Vec3[] {
|
||||
if (_RandomHemisphereVectors.length < count) {
|
||||
_RandomHemisphereVectors = generateBlueNoiseVectors(count, _RandomHemisphereVectors);
|
||||
}
|
||||
return _RandomHemisphereVectors;
|
||||
}
|
||||
|
||||
function getSamples(nSamples: number): number[] {
|
||||
const rhv = getRandomHemisphereVectors(nSamples);
|
||||
const samples = [];
|
||||
for (let i = 0; i < nSamples; i++) {
|
||||
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
|
||||
scale = 0.1 + scale * (1.0 - 0.1);
|
||||
|
||||
samples.push(RandomHemisphereVector[i][0] * scale);
|
||||
samples.push(RandomHemisphereVector[i][1] * scale);
|
||||
samples.push(RandomHemisphereVector[i][2] * scale);
|
||||
samples.push(rhv[i][0] * scale);
|
||||
samples.push(rhv[i][1] * scale);
|
||||
samples.push(rhv[i][2] * scale);
|
||||
}
|
||||
|
||||
return samples;
|
||||
|
||||
@@ -542,4 +542,60 @@ export function murmurHash3_128(key: string, seed: number): string {
|
||||
(h3 >>> 0).toString(16).padStart(8, '0') +
|
||||
(h4 >>> 0).toString(16).padStart(8, '0')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PCG pseudo-random number generator
|
||||
* See https://www.pcg-random.org/ and https://jcgt.org/published/0009/03/02/
|
||||
*/
|
||||
export class PCG {
|
||||
private state: number;
|
||||
|
||||
constructor(seed = 26699) {
|
||||
this.state = seed >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 32-bit unsigned integer
|
||||
*/
|
||||
int(): number {
|
||||
const oldstate = this.state;
|
||||
|
||||
this.state = Math.imul(this.state, 1664525) + 1013904223;
|
||||
this.state = this.state >>> 0;
|
||||
|
||||
const xorshifted = ((oldstate >>> 18) ^ oldstate) >>> 5;
|
||||
const rot = oldstate >>> 27;
|
||||
const result = (xorshifted >>> rot) | (xorshifted << (32 - rot));
|
||||
return result >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Float in [0, 1)
|
||||
*/
|
||||
float(): number {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -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',
|
||||
@@ -64,6 +66,7 @@ export interface Cylinders {
|
||||
readonly groupMapping: GroupMapping
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
}
|
||||
|
||||
export namespace Cylinders {
|
||||
@@ -132,6 +135,9 @@ export namespace Cylinders {
|
||||
setBoundingSphere(sphere: Sphere3D) {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(cylinders);
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(cylinders);
|
||||
}
|
||||
};
|
||||
return cylinders;
|
||||
@@ -176,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
|
||||
@@ -217,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();
|
||||
@@ -226,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 };
|
||||
|
||||
@@ -254,6 +262,7 @@ export namespace Cylinders {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
padding: ValueCell.create(padding),
|
||||
@@ -268,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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -291,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) {
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -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();
|
||||
|
||||
@@ -55,6 +56,7 @@ export interface DirectVolume {
|
||||
readonly boundingSphere: Sphere3D
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
|
||||
readonly meta: {
|
||||
/** Called to restore when a webgl context is lost */
|
||||
@@ -114,6 +116,9 @@ export namespace DirectVolume {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(directVolume);
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(directVolume);
|
||||
},
|
||||
meta: {}
|
||||
};
|
||||
return directVolume;
|
||||
@@ -223,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();
|
||||
@@ -231,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 };
|
||||
@@ -251,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;
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -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,
|
||||
@@ -73,6 +74,9 @@ interface Image {
|
||||
readonly boundingSphere: Sphere3D
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
|
||||
readonly meta: { [k: string]: unknown }
|
||||
}
|
||||
|
||||
namespace Image {
|
||||
@@ -133,6 +137,10 @@ namespace Image {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(image);
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(image);
|
||||
},
|
||||
meta: {}
|
||||
};
|
||||
return image;
|
||||
}
|
||||
@@ -193,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();
|
||||
@@ -201,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 };
|
||||
|
||||
@@ -217,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 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>
|
||||
*/
|
||||
@@ -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,19 +51,25 @@ 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 */
|
||||
readonly groupMapping: GroupMapping
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -68,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) {
|
||||
@@ -94,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;
|
||||
@@ -110,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) {
|
||||
@@ -136,29 +152,35 @@ export namespace Lines {
|
||||
setBoundingSphere(sphere: Sphere3D) {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(lines);
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(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);
|
||||
}
|
||||
|
||||
@@ -168,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
|
||||
|
||||
@@ -208,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();
|
||||
@@ -217,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);
|
||||
@@ -242,6 +266,7 @@ export namespace Lines {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -249,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -262,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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
@@ -56,6 +58,7 @@ export interface Mesh {
|
||||
readonly groupMapping: GroupMapping
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
|
||||
readonly meta: { [k: string]: unknown }
|
||||
}
|
||||
@@ -120,6 +123,9 @@ export namespace Mesh {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(mesh);
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(mesh);
|
||||
},
|
||||
meta: {}
|
||||
};
|
||||
return mesh;
|
||||
@@ -635,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
|
||||
|
||||
@@ -677,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();
|
||||
@@ -685,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 };
|
||||
|
||||
@@ -709,6 +717,7 @@ export namespace Mesh {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -721,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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -745,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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 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>
|
||||
*/
|
||||
@@ -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 {
|
||||
@@ -46,6 +48,7 @@ export interface Points {
|
||||
readonly groupMapping: GroupMapping
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
}
|
||||
|
||||
export namespace Points {
|
||||
@@ -99,6 +102,9 @@ export namespace Points {
|
||||
setBoundingSphere(sphere: Sphere3D) {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(points);
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(points);
|
||||
}
|
||||
};
|
||||
return points;
|
||||
@@ -132,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
|
||||
|
||||
@@ -170,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();
|
||||
@@ -179,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 };
|
||||
|
||||
@@ -201,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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,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);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user