Compare commits

...

154 Commits

Author SHA1 Message Date
Alexander Rose
da97cd20aa 5.7.0 2026-02-28 13:43:11 -08:00
Alexander Rose
ca6d73e048 changelog 2026-02-28 13:41:30 -08:00
Alexander Rose
88b79deefa package updates
- set min node to v22
- added globals for eslint v10
2026-02-28 13:40:38 -08:00
Alexander Rose
d756e2e195 schema updates 2026-02-28 13:22:39 -08:00
Alexander Rose
2ce126a8f5 update cif-core schema 2026-02-28 13:19:05 -08:00
Alexander Rose
01e95dada0 ignore some lint commits in git blame 2026-02-28 13:16:59 -08:00
Alexander Rose
1c024f0943 Merge pull request #1778 from molstar/polyhedron
Polyhedron
2026-02-22 12:32:22 -08:00
Alexander Rose
5901e3d6a1 Merge branch 'master' into polyhedron 2026-02-22 12:29:02 -08:00
Alexander Rose
0cfe1cec66 Merge pull request #1784 from molstar/guard-xr-policy
Guard against `xr-spatial-tracking` blocked in `Permissions-Policy`
2026-02-22 12:28:44 -08:00
Alexander Rose
c1930e4142 Merge pull request #1771 from molstar/basic-streamlines
Basic streamlines
2026-02-22 12:13:39 -08:00
Alexander Rose
71375d908f Guard against xr-spatial-tracking blocked in Permissions-Policy 2026-02-22 11:31:08 -08:00
Alexander Rose
728b87d4e4 Merge branch 'master' of https://github.com/molstar/molstar into polyhedron 2026-02-21 20:58:32 -08:00
Alexander Rose
9c17698a8a rename visual to include "coordination" 2026-02-21 20:57:43 -08:00
Alexander Rose
625381c446 exclude hydrogens 2026-02-21 20:57:20 -08:00
Alexander Rose
da949a245e Merge pull request #1776 from molstar/element-parsing-fixes
Element parsing fixes
2026-02-19 20:40:44 -08:00
Alexander Rose
7000bdd15d Merge branch 'master' into element-parsing-fixes 2026-02-19 20:40:28 -08:00
Alexander Rose
adcf6a6fa8 Merge pull request #1777 from molstar/metal-coordination-style
Add `metalCoordination` style param
2026-02-19 20:40:07 -08:00
Alexander Rose
b70af9f178 Merge branch 'master' into metal-coordination-style 2026-02-19 20:39:40 -08:00
Alexander Rose
e5bdcfd781 Merge pull request #1781 from molstar/fix-unit-symmetry-groups
Fix `unitSymmetryGroups` for representations with `includeParent`
2026-02-18 20:46:15 -08:00
Alexander Rose
6049705224 Fix unitSymmetryGroups for representations with includeParent enabled 2026-02-16 19:57:56 -08:00
Alexander Rose
273d50d403 cleanup coordination data model 2026-02-15 20:59:52 -08:00
Alexander Rose
333ea724d6 Merge pull request #1779 from giagitom/text-improvements
Additional text improvements
2026-02-15 13:32:20 -08:00
giagitom
e96dca91ef Add author to text.ts 2026-02-15 13:36:34 +01:00
giagitom
41a0048f64 Simplify text frag shader, remove near-clip discard, fix bounding sphere padding 2026-02-15 13:33:44 +01:00
Alexander Rose
5e97b05bd2 Add Polyhedron representation showing coordination sites 2026-02-15 00:10:00 -08:00
Alexander Rose
ebc6b2acce Add Structure.coordination sites 2026-02-15 00:09:46 -08:00
Alexander Rose
8372408d9c Add convexHull helper 2026-02-15 00:09:17 -08:00
Alexander Rose
2c6822f5ab Add metalCoordination style param (dashed, solid) for bonds 2026-02-15 00:06:00 -08:00
Alexander Rose
7efbf46e7a Merge pull request #1774 from giagitom/text-improvements
Text label improvements
2026-02-14 23:34:01 -08:00
Alexander Rose
b6d6a518d3 Add more element-pair thresholds for bonding (Ag-S, CoSb, Ga-F) 2026-02-14 23:21:17 -08:00
Alexander Rose
2d690268f9 Handle additional elements in guessElementSymbol* (As, Li, Ga) 2026-02-14 23:20:54 -08:00
Alexander Rose
e0c794b557 Detect metal-coordination when parsing pdb 2026-02-14 23:20:25 -08:00
Alexander Rose
f91f445631 Fix parsing of single charge type_symbols (e.g., N+) in cif-core 2026-02-14 23:20:05 -08:00
giagitom
1cc367c8d8 Fix head rotation for clip-space billboard offset 2026-02-15 01:09:42 +01:00
giagitom
8c6969206d Update copyright years to 2026 2026-02-15 00:59:48 +01:00
Alexander Rose
c0479e3d46 Merge branch 'master' of https://github.com/molstar/molstar into basic-streamlines 2026-02-14 13:41:03 -08:00
Alexander Rose
22e92b38c6 Merge pull request #1775 from molstar/vertex-size
Add `vertex` and `vertexInstance` granularity for size
2026-02-14 13:40:21 -08:00
giagitom
5741709023 Hard discard near clip, revert attachment, cleanup nearFade 2026-02-14 21:11:56 +01:00
Alexander Rose
2265fc02cc per-point tube size 2026-02-14 08:56:04 -08:00
Alexander Rose
64180bef36 add min separation 2026-02-14 08:54:12 -08:00
Alexander Rose
be3caef6e9 Add vertex and vertexInstance granularity for size
- Geometry export: Support vertex-based sizing
- Add `transform` and `domain` parameters to volume-value size theme
2026-02-14 08:26:48 -08:00
giagitom
71a2f71866 Text label improvements
- Fix label attachment inversion (top/bottom and left/right)
- Improve label background vertical centering
- Add label near-clip fade out
- Handle label depth variant for correct transparent background and near fade
- 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)
- Use clip-space billboard rendering to avoid perspective distortion
2026-02-14 10:53:49 +01:00
David Sehnal
3c6152054e fix TextCtrl (#1773)
* fix TextCtrl

* header
2026-02-12 17:53:02 +01:00
Alexander Rose
080d649bf9 Streamlines support
- Add basic calculation method
 - Add custom-volume-property
 - Add representation with lines and tube-mesh visuals
2026-02-08 19:28:19 -08:00
Alexander Rose
2852b09c77 Merge pull request #1770 from molstar/line-strips 2026-02-08 15:09:12 -08:00
Paul Pillot
5e53467541 Tmalign perf tweaks (#1756)
* add computeCenter util fn

This is more efficient than using the CentroidHelper patterns.

Time improvement: 135ms --> 113ms on 4JV6/3T5I benchmark

* kabsch received array of indices

instead of array of coordinates which requires to build intermediary arrays in each caller.

* Reuse of Vec3 objects

- Avoid Vec3.create in loops
- use Vec3.squaredDistance instead of repeated double index access
- For simplifying TS manipulations, type xa, ya as Vec3 arrays.

* Reuse Positions

MinimeRmsdPositions.empty was called twice in kabsch fn. A unique call is now done with the full length and the same object is reused.
The length property is passed to ensure that only the portion that contains the required coordinates is used.

* simpler hypothenuse implementation

* missing break statements

- when score has not improved, we don't need to run again with the same parameters
- when the number of residues passing the cutoff is lower than 3, we don't need to run again with stricter cutoffs

* hint at float array

* update headers

* cache transformed coordinates, align indices

* replace ndpwScore with ndpwStructure

ndpwScore was creating an intermediate 2D array which is already cached in ndpwStructure

* trimmedKabschWithTransformedCoordinates

avoid recomputing transformed coordinates (as in trimmedKabsch) when they are already available

* cache rmsdResult and rmsdState

bestTransform must not keep a reference to result.bTransform because its value would change each time kabsch function is run.

* swap shortest structure to B

All transforms are made against the coordinates for structureB when yt is computed.

* reafactor: generalize usage of incremental sequence

Previous implementation was relying on tmpAlignA to hold the indices. It was conflictin in `tryGaplessThreading` with the call to `extendFromSeed`.
A distinct buffer has been created.

* refactor: preallocate dp arrays in constructor, uniform usage of typed arrays

* wrap tm-align in a task + useOverlay

* Skip fragment based strategy if similarity is good

Fragment based alignment takes up most of the computation time due to the O(n3) complexity of aligning fragments of different length. When the alignment is already good, this procedure does not yield improved results (observed also by the original authors).

The threshold for "similar structure" is set to a TM-score of 0.5 according to litterature.
Because it's possible to align 2 structures of very different length, and because the TM-score is normalized using the sequence length, it's important to change the reference for computation from a random choice (sequence A), to the minimal sequence length. This is consistent with the equations from the original TM-align publication (Lmin is used in scoring).

Note that the code already ensures that the shortest sequence is B (less iterations), but this commit makes the min sequence length explicit.
2026-02-08 14:49:14 +01:00
Alexander Rose
42dc579ddb changelog 2026-02-07 15:21:06 -08:00
Alexander Rose
890c758585 Geometry export
- Fix vertex-based coloring for non-mesh geometries
- Support line-strips
2026-02-07 15:20:58 -08:00
Alexander Rose
e6c77069df add frenet-frames helper 2026-02-07 15:20:14 -08:00
Alexander Rose
e7ecf98f13 add line-strips to lines geo 2026-02-07 15:19:25 -08:00
Alexander Rose
70ad32f62d Merge pull request #1769 from molstar/split-vol-visual
split volume visual code
2026-02-07 13:52:28 -08:00
Alexander Rose
69fe452055 split volume visual code 2026-02-07 10:43:12 -08:00
Alexander Rose
9edeb84f4e default to linear 2026-02-01 22:54:14 -08:00
Alexander Rose
e1db3114c8 Fix missing usePalette support in MeshExporter 2026-02-01 22:50:50 -08:00
Alexander Rose
8724badcb6 fix volume valueRef getData to select roots 2026-02-01 22:03:28 -08:00
Alexander Rose
d413f74526 Merge pull request #1764 from molstar/pqr-support
PQR support
2026-02-01 21:59:45 -08:00
Alexander Rose
6752108c5f changelog 2026-02-01 21:55:21 -08:00
Alexander Rose
9302fdadb9 undo adding pqr to mvs 2026-02-01 21:22:44 -08:00
Alexander Rose
f7048c7535 Merge branch 'master' of https://github.com/molstar/molstar into pqr-support 2026-02-01 21:20:57 -08:00
Alexander Rose
3252a3f0f3 Merge pull request #1765 from molstar/custom-volume-property 2026-02-01 12:48:28 -08:00
Alexander Rose
6805194d48 file headers 2026-01-31 17:33:52 -08:00
Alexander Rose
acf0dceb47 add CustomVolumeProperty 2026-01-31 17:30:43 -08:00
Alexander Rose
c53f500da6 add pqr support 2026-01-31 17:15:17 -08:00
midlik
defc04278e MVSData.toMVSX (#1763) 2026-01-29 16:51:54 +01:00
Alexander Rose
aa4d5e78a7 Merge pull request #1757 from giagitom/improve-outlines
disable transparent outline when near to solid mesh
2026-01-26 20:05:29 -08:00
Alexander Rose
df3a432afd Merge branch 'master' into improve-outlines 2026-01-25 22:38:19 -08:00
Alexander Rose
1b339d18cc Merge pull request #1762 from molstar/trackball-animation-axis
add axis param to trackball spin & rock animation
2026-01-25 22:37:24 -08:00
Alexander Rose
c4650c91a8 Merge branch 'master' into trackball-animation-axis 2026-01-25 22:37:15 -08:00
Alexander Rose
e3c4f19563 Merge pull request #1761 from molstar/fix-color-smoothing 2026-01-25 22:26:35 -08:00
Alexander Rose
85780a5d6a add axis param to trackball spin & rock animation 2026-01-24 12:11:50 -08:00
Alexander Rose
aab70e2ff0 Color smoothing fixes (#1747)
- use correct instance for non instance-type
- never transform for non instance-type
- add extra radius to gaussian surface boundingsphere
2026-01-24 12:09:04 -08:00
dsehnal
e859f497f1 5.6.1 2026-01-23 15:42:44 +01:00
dsehnal
6a9fed56f3 changelog 2026-01-23 15:40:54 +01:00
dsehnal
d7c2505852 npm audit 2026-01-23 15:40:31 +01:00
midlik
754dfeab91 Fix MVSAnnotationStructureComponent not updating properly (#1759)
* Fix `MVSAnnotationStructureComponent` not updating properly when parent structure changes

* lint

* fix build
2026-01-23 13:55:41 +01:00
Alexander Rose
d3b02df5b9 Disable occlusion culling in ImagePass (#1758) 2026-01-20 06:53:26 -08:00
giagitom
3d95ed729c disable transparent outline when near to solid mesh 2026-01-19 19:59:07 +01:00
Alexander Rose
9cbb4414e0 5.6.0 2026-01-18 12:11:39 -08:00
Alexander Rose
79fcfe50bc changelog 2026-01-18 11:44:42 -08:00
Alexander Rose
216d16456b package updates 2026-01-18 11:43:06 -08:00
Alexander Rose
822aaa99b0 Merge pull request #1755 from molstar/seqres-entity-lipids
Improve SEQRES use and more lipid names
2026-01-18 10:53:07 -08:00
Alexander Rose
2c683ab77d Merge pull request #1754 from molstar/fix-bounding-sphere-reuse
fix boundingSphere reuse in structure visuals
2026-01-18 10:52:44 -08:00
Alexander Rose
2ef5af6881 Merge pull request #1753 from molstar/remove-fence-canvas3d
remove fence from Canvas3D.render
2026-01-18 10:47:29 -08:00
Alexander Rose
36f18be042 add lipid components names used in amber ff 2026-01-17 18:34:32 -08:00
Alexander Rose
f093a3ab37 use PDB seqres record to deduce entity information 2026-01-17 18:31:43 -08:00
Alexander Rose
74cd42117b fix boundingSphere reuse in structure visuals
- was triggering extra calculation
- add hasBoundingSphere method to geometries
2026-01-17 17:34:53 -08:00
Alexander Rose
bb4a4e6102 remove fence from Canvas3D.render
- causes issues when the CPU is busy
- interfers with `requestAnimationFrame`
2026-01-17 17:08:13 -08:00
Alexander Rose
24a3167f9b Merge branch 'master' of https://github.com/molstar/molstar 2026-01-17 16:55:07 -08:00
Alexander Rose
214e1c20ca ignore missing plugin-state-animation 2026-01-17 16:55:02 -08:00
Alexander Rose
33cab6ddad Merge pull request #1752 from sbittrich/los
Non-covalent interactions: Prevent self-occlusion on quaternary amines
2026-01-17 16:48:55 -08:00
Alexander Rose
f4b2826bc7 Merge branch 'master' into los 2026-01-17 16:45:34 -08:00
Alexander Rose
ebaa9f2e56 use eachIntraBondedAtom (@sbittrich) 2026-01-17 16:44:36 -08:00
Alexander Rose
812b75a034 Merge pull request #1750 from sbittrich/master
Fix outline postprocessing artifacts
2026-01-17 16:34:20 -08:00
Alexander Rose
3b02a5f5ec Merge pull request #1742 from giagitom/add-wireframe-structure-visuals
Add structure wireframe visuals on molecular and gaussian surfaces
2026-01-17 13:13:12 -08:00
Alexander Rose
657d2eb1c5 Merge branch 'master' into add-wireframe-structure-visuals 2026-01-17 13:10:01 -08:00
Alexander Rose
25d87dd14d params tweaks 2026-01-17 13:09:24 -08:00
Sebastian
d2605e6e3d Merge remote-tracking branch 'upstream/master' into los
# Conflicts:
#	CHANGELOG.md
2026-01-16 14:52:47 +01:00
Sebastian
b21ebe0f55 Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	CHANGELOG.md
2026-01-16 14:51:56 +01:00
Sebastian Bittrich
2693fe8b7e actually cache __srcIndexArray__ (#1751)
Co-authored-by: Sebastian <bittrich@pharmai-discovery.com>
2026-01-16 13:15:08 +01:00
Sebastian
45279a6520 nci: prevent self-occlusion on quat amines 2026-01-16 11:36:30 +01:00
Sebastian
22f9b1a7a1 fix outline postprocessing artifacts (closes #1749) 2026-01-16 09:15:04 +01:00
zachcp
8325a58e25 20261110 hex fix clean (#1746) 2026-01-12 08:14:14 +01:00
midlik
0acc508a8f MVS text_format and group_by_fields (#1744)
* MVS: text_format

* MVS: group_by_fields params for label_from_*

* refactor

* Update CHANGELOG

* Refactor fstrings (FormatTemplate)

* Move fstrings (FormatTemplate) to src/mol-util/formatting.ts

* FormatTemplate: fix parsing of fillChar ':'

* file rename
2026-01-10 12:50:37 +01:00
giagitom
2af0cd9d6f Add structure wireframe visuals on molecular and gaussian surfaces 2026-01-04 12:57:27 +01:00
Alexander Rose
304858fcba Merge pull request #1741 from giagitom/improve-ssao-boundaries
Improve ssao boundaries
2026-01-03 09:24:07 -08:00
giagitom
ade027911c improve occlusion evaluation at boundaries 2026-01-03 17:13:33 +01:00
David Sehnal
a97e647f7a Fix to_mmCIF writing duplicate categories under certain conditions (#1739) 2026-01-03 09:59:47 +01:00
Alexander Rose
008bed0233 fix transparent ssao handling 2026-01-02 17:33:07 -08:00
Alexander Rose
bb4c04f3b9 fix ssao darkening
- instead of clamping offscreening pixels, skip samples
2026-01-02 09:48:40 -08:00
Alexander Rose
62997e5972 Merge pull request #1737 from molstar/pcg
stable random numbers (PCG)
2025-12-31 12:16:54 -08:00
Alexander Rose
a20e7bb40d Fix SSAO darkening when sampling background/offscreen pixels 2025-12-31 12:14:35 -08:00
Alexander Rose
2acfac4c85 changelog 2025-12-31 11:35:04 -08:00
Alexander Rose
a1a9d87a54 fix missing scaling 2025-12-30 23:11:53 -08:00
Alexander Rose
1ab71cc487 calculate random hemisphere vectors on demand 2025-12-30 22:39:48 -08:00
Alexander Rose
a8b19f5f3c use blue noise for ssao sample vectors
- properly sample on sphere (not in box)
2025-12-30 21:41:09 -08:00
David Sehnal
4661a4a5f0 Only show "already registered transformer" warnings in non-production builds (#1732) 2025-12-30 16:00:19 +01:00
David Sehnal
2c40abc808 Fix label_seq_id assignment in PDB parser (#1731)
* Fix `label_seq_id` assignment in PDB parser when insertion codes are present

* tweak

* changelog

* typo

* SEQRES detection in PDB parser
2025-12-30 15:59:40 +01:00
Alexander Rose
10d7bcf4c0 stable random numbers (PCG)
- ME grayscale colors
- dot offset
- SSAO hemisphere vectors
2025-12-29 20:08:10 -08:00
Alexander Rose
5f8e4e6913 Merge pull request #1735 from molstar/volsurf-12-25
volume & surface improvements
2025-12-28 21:10:32 -08:00
Alexander Rose
94fa9f124a pr feedback optim 2025-12-28 21:02:31 -08:00
David Sehnal
3e70251f38 Viewer QoL (#1734)
* Viewer QoL

* docs
2025-12-28 19:26:33 +01:00
Alexander Rose
66ed6cfa94 type fix 2025-12-27 14:39:49 -08:00
Alexander Rose
d82b6e8d0d changelog 2025-12-27 14:33:30 -08:00
Alexander Rose
5a5f6867b9 support volume floodfill before surface extraction
- for isosurface, molecular-surface, gaussian-surface
2025-12-27 14:29:05 -08:00
Alexander Rose
5cd5fc09f5 support includeParent for gaussian-surface
- disables GPU support
2025-12-27 14:15:28 -08:00
Alexander Rose
17528d5ca2 tweak gaussian-density smothness default range 2025-12-27 14:10:07 -08:00
midlik
e658a11947 MVS misc fixes (#1733)
* MVS - fix carbohydrate sizing

* MVS: color_from_uri and color_from_source take selector parameter

* MVS: Add `keepCameraOrientation` option for loading functions

* MVS: Remove dead code

* MVS: minor refactor (SelectorT)

* PR1733 feedback
2025-12-24 14:41:49 +01:00
dsehnal
4ac6f5c202 5.5.0 2025-12-22 12:50:36 +01:00
dsehnal
5726515707 changelog 2025-12-22 12:48:58 +01:00
Alexander Rose
f2ee7d1470 fix unit hash collision (#1729)
* fix unit hash collision

* cleanup

* name tweak
2025-12-22 09:53:29 +01:00
Alexander Rose
4140412e06 schema updates 2025-12-21 16:16:02 -08:00
Alexander Rose
44ed142521 package updates 2025-12-21 16:11:31 -08:00
Alexander Rose
1ae0bbc150 Merge pull request #1727 from molstar/geo-interior-coloring
per-geometry interior coloring
2025-12-21 16:03:43 -08:00
Alexander Rose
8213611293 fix bumpiness for solid interior 2025-12-21 16:00:17 -08:00
Alexander Rose
2697634a9f Merge branch 'master' of https://github.com/molstar/molstar into geo-interior-coloring 2025-12-21 15:07:42 -08:00
Alexander Rose
d7ba9e0c61 typo 2025-12-21 15:07:01 -08:00
Alexander Rose
c99c4342b7 Merge pull request #1728 from molstar/improvements-251221
General Improvements
2025-12-21 15:01:29 -08:00
Alexander Rose
f410e27d1a use camelCase 2025-12-21 14:57:46 -08:00
Alexander Rose
e6d54412cf typo 2025-12-21 14:51:48 -08:00
Alexander Rose
6238684819 tweak residue-charge coloring
- add charmm/amber protonation variants
- add common modified residues from CCD
- remove unrelated links
2025-12-21 14:33:10 -08:00
dsehnal
ea07cd89de better canvas background handling 2025-12-21 16:15:56 +01:00
dsehnal
a7330f40d7 MVS getCurrentMVSSnapshot util 2025-12-21 15:38:57 +01:00
dsehnal
92c55ffe35 Tweak ResidueChargeColorTheme params 2025-12-21 12:10:29 +01:00
David Sehnal
c21ba08fc7 Viewer improvements (#1725)
* separate exxtensions file

* subscribe method on the Viewer object

* export lib

* Add viewer.structureInteraction

* MVS tryGetPrimitivesFromLoci util

* param select indicator

* tweaks

* mapped control icon

* docs and tweaks

* viewer color themes

* tweak

* typo

* pr feedback
2025-12-21 11:35:49 +01:00
Alexander Rose
ba3a716900 interior coloring
- replace global with per-geometry system
2025-12-20 16:21:49 -08:00
Alexander Rose
3133dc1543 Fix flipSided for meshes 2025-12-20 13:55:06 -08:00
dsehnal
fe2541f9e8 tweak package.json 2025-12-19 13:19:30 +01:00
ddelalamo-takeda
27af73f97f Add TM-align structure alignment algorithm (#1723)
* Delete docs/docs/plugin/superposition.md

* Add TM-align structure alignment algorithm

Implement TM-align for structure-based protein alignment, providing an
alternative to sequence-based superposition methods. TM-align finds optimal
structural alignments regardless of sequence similarity using TM-score.

New features:
- Core TM-align algorithm (src/mol-math/linear-algebra/3d/tm-align.ts)
  - TM-score calculation with length-independent normalization
  - Dynamic programming for optimal alignment
  - Gapless threading and fragment-based initialization
  - Multiple refinement passes for accuracy (~97.7% of US-align reference)

- High-level wrapper (src/mol-model/structure/structure/util/tm-align.ts)
  - tmAlign() function accepting StructureElement.Loci inputs
  - Returns transformation matrix, TM-scores, RMSD, and alignment

- BasicWrapper API (src/examples/basic-wrapper/)
  - tmAlign(pdbId1, chain1, pdbId2, chain2, color1?, color2?)
  - sequenceAlign() - sequence-based superposition
  - loadStructures() - load without alignment
  - Example HTML pages demonstrating usage

- UI integration (src/mol-plugin-ui/structure/superposition.tsx)
  - TM-align option in superposition panel

References:
Zhang Y, Skolnick J. Nucl Acids Res 33, 2302-9 (2005)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Delete src/examples/basic-wrapper/index.html

* changing reference test case

* Restoring src/examples/basic-wrapper/index.html

* Addressing comments in pull request

* Authorship added

---------

Co-authored-by: Diego del Alamo <diego.delalamo@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 13:18:11 +01:00
Lukas Polak
e9a442ca6e Add Residue Charge color scheme (#1722)
* Add charged residue color scheme

* Changelog + file header

* Address PR comments

* Remove residue-name.ts contribution info

* PR suggestion

* Changelog reference PR
2025-12-19 13:15:51 +01:00
Alexander Rose
e86e282bb4 Fix missing gl.flush for async picking 2025-12-16 21:24:23 -08:00
Alexander Rose
213506dff0 Fix program not compiled for sync picking 2025-12-16 21:23:22 -08:00
Alexander Rose
bc7aa7c9aa Fix webgl1 shader syntax 2025-12-16 21:21:58 -08:00
Alexander Rose
b234bf8890 Fix molecular-surface when probe diameter smaller then resolution 2025-12-14 10:30:21 -08:00
Alexander Rose
36b4dcf7a8 Fix molecular-surface "auto" quality params not hidden 2025-12-13 22:07:11 -08:00
220 changed files with 9276 additions and 2284 deletions

14
.git-blame-ignore-revs Normal file
View 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

View File

@@ -5,6 +5,124 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [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
- Move viewer extensions, options, and presets to a separate file
- Add `molstar.lib` export providing access to a wide range of functionality previously not available from the compiled bundle
- Add `Viewer.subscribe` method that keeps track of subscribed plugin events and disposes them together with the parent viewer
- Add `Viewer.structureInteractivity` that makes it easy to highlight/select elements on the loaded structure
- Add `viewportBackgroundColor` and `viewportFocusBehavior` options
- Add `mvs.html` example to showcase the new functionality combined with MolViewSpec
- Add dark and blue color theme support (import `theme/dark.css` or `theme/blue.css` instead of the default `molstar.css`)
- MolViewSpec extension
- Add `tryGetPrimitivesFromLoci` that makes it easier to access primitive element data from hover/click interactions
- Add `getCurrentMVSSnapshot` to obtain source data for the currently displayed snapshot
- Add TM-align structure-based protein alignment algorithm
- New `TMAlign` namespace in `mol-math/linear-algebra/3d/tm-align.ts`
- New `tmAlign` function in `mol-model/structure/structure/util/tm-align.ts`
- Returns TM-score, RMSD, alignment mapping, and transformation matrix
- Molecular Surface
- Fix "auto" quality params not hidden
- Fix calculation when probe diameter is smaller then resolution
- Fix webgl1 shader syntax
- Fix program not compiled for sync picking
- Fix missing `gl.flush` for async picking (needed for Safari)
- Add Residue Charge color scheme (#1722)
- Add dropdown indicator for mapped parameter definitions and adjust "more options" icon
- Fix `flipSided` for meshes
- [Breaking] Interior coloring
- Remove global `interiorDarkening`, `interiorColorFlag`, `interiorColor`
- Add per-geometry `interiorColor`, `interiorSubstance`
- Add `label/auth_comp_id` to `StructureProperties.residue`
- Previously, this has been only been present on `.atom` (since residue name can alter on per-atom basis), but this has been a bit confusing for the general use-case
- Move canvas "checkered background" logic to `canvas3d.ts` and only apply it when `transparentBackground` is on
- This prevents ugly flickering during plugin initialization
- Fix unit hash collision issues (#1721)
## [v5.4.2] - 2025-12-07
- Fix postprocessing issues with SSAO and outlines for large structures (#1387)
- Reduce automatic quality on standalone HMD devices

View File

@@ -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**

1
breaking-v6-changes.md Normal file
View File

@@ -0,0 +1 @@
- Remove `checkeredCanvasBackground` from `PluginContext` and `PluginContainer`

View File

@@ -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
1 audit.block_doi
14 chemical_formula.weight chemical_formula.iupac
15 atom_type.symbol chemical_formula.weight
16 atom_type.description atom_type.symbol
17 atom_type.description
18 atom_type_scat.dispersion_real
19 atom_type_scat.dispersion_imag
20 atom_type_scat.source
26 cell.length_b space_group_symop.operation_xyz
27 cell.length_c cell.length_a
28 cell.angle_alpha cell.length_b
29 cell.length_c
30 cell.angle_alpha
31 cell.angle_beta
32 cell.angle_gamma
33 cell.volume

View File

@@ -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
```

View File

@@ -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

View File

@@ -15,10 +15,33 @@ There are 4 basic ways of instantiating the Mol* plugin.
## ``Viewer`` wrapper
- The most basic usage is to use the ``Viewer`` wrapper. This is best suited for use cases that do not require much custom behavior and are mostly about just displaying a structure.
- See ``Viewer`` class is defined in [src/apps/viewer/app.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/app.ts) for available methods and options.
- The most basic usage is to use the ``Viewer`` wrapper. This is best suited for use cases that do not require custom behavior and are mostly about just displaying a structure.
- See ``Viewer`` class is defined in [src/apps/viewer/app.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/app.ts) for available methods
- See [options.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/options.ts) for available plugin options
- See [embedded.html](https://github.com/molstar/molstar/blob/master/src/apps/viewer/embedded.html) and [mvs.html](https://github.com/molstar/molstar/blob/master/src/apps/viewer/mvs.html) for example usage
- Importing `molstar.js` will expose `molstar.lib` namespace that allow accessing various functionality without a bundler such as WebPack or esbuild. See the `mvs` example above for basic usage.
- Alternative color themes can be used by importing `theme/dark.css` (or `light/blue`) instead of `molstar.css`
Example usage without using WebPack:
### molstar.js and molstar.css sources
- Download `molstar` NPM package and use the files from `build/viewer` diractory
- Use `jsdelivr` CDN
- `<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.js" />`
- `<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.css" />`
- `@latest` can be replaced by a specific Mol* version, e.g., `@5.4.2`
- 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
<style>
@@ -35,7 +58,7 @@ Example usage without using WebPack:
- the folder build/viewer after cloning and building the molstar package
- from the build/viewer folder in the Mol* NPM package
-->
<link rel="stylesheet" type="text/css" href="molstar.css" />
<link rel="stylesheet" type="text/css" href="./molstar.css" />
<script type="text/javascript" src="./molstar.js"></script>
<div id="app"></div>
@@ -62,13 +85,15 @@ Example usage without using WebPack:
</script>
```
When using WebPack (or possibly other build tool) with the Mol* NPM package installed, the viewer class can be imported using
### Using WebPack/esbuild/...
When using WebPack (or other bundler) with the Mol* NPM package installed, the viewer class can be imported using
```ts
import { Viewer } from 'molstar/build/viewer/molstar'
import { Viewer } from 'molstar/lib/apps/viewer/app'
function initViewer(target: string | HTMLElement) {
return new Viewer(target, { /* options */})
return Viewer.create(target, { /* options */}) // returns a Promise
}
```
@@ -139,6 +164,8 @@ export function MolStarWrapper() {
// In debug mode of react's strict mode, this code will
// be called twice in a row, which might result in unexpected behavior.
useEffect(() => {
// By default, react will call each useEffect twice if using Strict mode in
// debug build, it is recommended to disable strict mode for this reason if possible
async function init() {
window.molstar = await createPluginUI({
target: parent.current as HTMLDivElement,

View File

@@ -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` &mdash; 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',
});
```

View File

@@ -0,0 +1,152 @@
# Structure Superposition
Mol* provides utilities for superposing protein structures, including both sequence-independent (RMSD-based) and structure-based (TM-align) methods.
## RMSD-based Superposition
The basic superposition method uses the Kabsch algorithm to minimize RMSD between corresponding atoms:
```typescript
import { superpose } from 'molstar/lib/mol-model/structure/structure/util/superposition';
import { StructureSelection, QueryContext } from 'molstar/lib/mol-model/structure';
import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
// Create a query for C-alpha atoms
const caQuery = compile<StructureSelection>(MS.struct.generator.atomGroups({
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
}));
// Get selections from two structures
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(structure1)));
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(structure2)));
// Compute superposition (returns transformation matrices)
const transforms = superpose([sel1, sel2]);
// transforms[0].bTransform contains the Mat4 to superpose structure2 onto structure1
```
## TM-align Superposition
TM-align is a structure-based alignment algorithm that produces the TM-score, a length-independent metric for comparing protein structures. Unlike RMSD, TM-score is normalized to [0, 1] and is more robust for comparing proteins of different sizes.
### Basic Usage
```typescript
import { tmAlign } from 'molstar/lib/mol-model/structure/structure/util/tm-align';
import { StructureElement } from 'molstar/lib/mol-model/structure';
// Get C-alpha Loci from two structures (see selection examples above)
const loci1: StructureElement.Loci = /* ... */;
const loci2: StructureElement.Loci = /* ... */;
// Run TM-align
const result = tmAlign(loci1, loci2);
console.log('TM-score (normalized by structure 1):', result.tmScoreA);
console.log('TM-score (normalized by structure 2):', result.tmScoreB);
console.log('RMSD:', result.rmsd);
console.log('Aligned residues:', result.alignedLength);
// result.bTransform is a Mat4 to transform structure2 onto structure1
```
### TM-align Result
The `tmAlign` function returns a `TMAlignResult` object with the following properties:
| Property | Type | Description |
|----------|------|-------------|
| `bTransform` | `Mat4` | Transformation matrix to superpose structure B onto A |
| `tmScoreA` | `number` | TM-score normalized by length of structure A |
| `tmScoreB` | `number` | TM-score normalized by length of structure B |
| `rmsd` | `number` | RMSD of aligned residue pairs (in Angstroms) |
| `alignedLength` | `number` | Number of aligned residue pairs |
| `sequenceIdentity` | `number` | Sequence identity of aligned residues (0-1) |
| `alignmentA` | `number[]` | Indices of aligned residues in structure A |
| `alignmentB` | `number[]` | Indices of aligned residues in structure B |
### Understanding TM-score
The TM-score is calculated as:
$$\text{TM-score} = \frac{1}{L} \sum_{i=1}^{L_{ali}} \frac{1}{1 + (d_i/d_0)^2}$$
Where:
- $L$ is the length of the reference protein
- $L_{ali}$ is the number of aligned residues
- $d_i$ is the distance between the $i$-th pair of aligned residues after superposition
- $d_0 = 1.24 \sqrt[3]{L - 15} - 1.8$ is a length-dependent normalization factor
**TM-score interpretation:**
- TM-score > 0.5: Generally indicates proteins with the same fold
- TM-score > 0.17: Generally indicates proteins with random structural similarity
### Low-level API
For direct coordinate-based alignment without structures, use the `TMAlign` namespace:
```typescript
import { TMAlign } from 'molstar/lib/mol-math/linear-algebra/3d/tm-align';
// Create position arrays
const posA = TMAlign.Positions.empty(lengthA);
const posB = TMAlign.Positions.empty(lengthB);
// Fill in coordinates
for (let i = 0; i < lengthA; i++) {
posA.x[i] = /* x coordinate */;
posA.y[i] = /* y coordinate */;
posA.z[i] = /* z coordinate */;
}
// ... similarly for posB
// Compute alignment
const result = TMAlign.compute({ a: posA, b: posB });
```
### Complete Example: Aligning Two PDB Structures
```typescript
import { PluginContext } from 'molstar/lib/mol-plugin/context';
import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
import { StructureSelection, QueryContext, StructureElement } from 'molstar/lib/mol-model/structure';
import { tmAlign } from 'molstar/lib/mol-model/structure/structure/util/tm-align';
import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
async function alignStructures(plugin: PluginContext, structure1: any, structure2: any) {
// Query for C-alpha atoms in chain A
const caQuery = compile<StructureSelection>(MS.struct.generator.atomGroups({
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), 'A']),
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
}));
// Get structure data
const data1 = structure1.cell?.obj?.data;
const data2 = structure2.cell?.obj?.data;
// Create selections
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(data1)));
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(data2)));
// Run TM-align
const result = tmAlign(sel1, sel2);
// Apply transformation to structure2
const b = plugin.state.data.build().to(structure2)
.insert(StateTransforms.Model.TransformStructureConformation, {
transform: { name: 'matrix', params: { data: result.bTransform, transpose: false } }
});
await plugin.runTask(plugin.state.data.updateTree(b));
return result;
}
```
## References
- Zhang Y, Skolnick J. "TM-align: a protein structure alignment algorithm based on the TM-score." *Nucleic Acids Research* 33, 2302-2309 (2005). DOI: [10.1093/nar/gki524](https://doi.org/10.1093/nar/gki524)
- Kabsch W. "A solution for the best rotation to relate two sets of vectors." *Acta Crystallographica* A32, 922-923 (1976).

View File

@@ -33,6 +33,7 @@ nav:
- Examples: plugin/examples.md
- Custom Library: 'plugin/custom-library.md'
- Selections: 'plugin/selections.md'
- Superposition: 'plugin/superposition.md'
- Viewer State: 'plugin/viewer-state.md'
- Data State: 'plugin/data-state.md'
- File Formats: 'plugin/file-formats.md'

1330
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.4.2",
"version": "5.7.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 .",
@@ -123,7 +123,8 @@
"Chetan Mishra <chetan.s115@gmail.com>",
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
"Kim Juho <juho_kim@outlook.com>",
"Victoria Doshchenko <doshchenko.victoria@gmail.com>"
"Victoria Doshchenko <doshchenko.victoria@gmail.com>",
"Diego del Alamo <diego.delalamo@gmail.com>"
],
"license": "MIT",
"devDependencies": {
@@ -131,27 +132,28 @@
"@types/gl": "^6.0.5",
"@types/jest": "^30.0.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.26",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"@types/webxr": "^0.5.24",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"benchmark": "^2.1.4",
"concurrently": "^9.2.1",
"cpx2": "^8.0.0",
"css-loader": "^7.1.2",
"esbuild": "^0.27.1",
"css-loader": "^7.1.4",
"esbuild": "^0.27.3",
"esbuild-jest-transform": "^2.0.1",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^9.39.1",
"fs-extra": "^11.3.2",
"esbuild-sass-plugin": "^3.6.0",
"eslint": "^10.0.2",
"fs-extra": "^11.3.3",
"globals": "^17.3.0",
"http-server": "^14.1.1",
"jest": "^30.2.0",
"jpeg-js": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.94.2",
"simple-git": "^3.30.0",
"sass": "^1.97.3",
"simple-git": "^3.32.3",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
},
@@ -160,12 +162,12 @@
"@types/benchmark": "^2.1.5",
"@types/compression": "1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^20.19.25",
"@types/node": "^22.19.13",
"@types/node-fetch": "^2.6.13",
"@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",
@@ -175,7 +177,7 @@
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"rxjs": "^7.8.2",
"swagger-ui-dist": "^5.30.3",
"swagger-ui-dist": "^5.32.0",
"tslib": "^2.8.1",
"util.promisify": "^1.1.3"
},

View File

@@ -13,7 +13,7 @@ import * as os from 'os';
const Apps = [
// Apps
{ kind: 'app', name: 'viewer' },
{ kind: 'app', name: 'viewer', themes: ['light', 'dark', 'blue'] },
{ kind: 'app', name: 'docking-viewer' },
{ kind: 'app', name: 'mesoscale-explorer' },
{ kind: 'app', name: 'mvs-stories', globalName: 'mvsStories', filename: 'mvs-stories.js' },
@@ -132,7 +132,6 @@ function getPaths(app) {
async function createBundle(app) {
const { name, kind } = app;
const { prefix, entry, outfile } = getPaths(app);
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
const ctx = await esbuild.context({
entryPoints: [entry],
@@ -173,6 +172,41 @@ async function createBundle(app) {
if (!isProduction) await ctx.watch();
}
async function createTheme(appName, themeName) {
// const { prefix, entry, outfile } = getPaths(app);
const ctx = await esbuild.context({
entryPoints: [resolveEntryPath(`./src/apps/${appName}/theme/${themeName}.ts`)],
tsconfig: './tsconfig.json',
bundle: true,
minify: isProduction,
minifyIdentifiers: false,
sourcemap: false,
outfile: `./build/${appName}/theme/${themeName}.js`,
plugins: [
// fileLoaderPlugin({ out: prefix }),
sassPlugin({
type: 'css',
silenceDeprecations: ['import'],
logger: {
warn: (msg) => console.warn(msg),
debug: () => { },
}
}),
],
color: true,
logLevel: 'info',
define: {
'process.env.NODE_ENV': JSON.stringify(NODE_ENV_PRD ? 'production' : 'development'),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
},
});
await ctx.rebuild();
if (!isProduction) await ctx.watch();
}
function findBrowserTests(names) {
const dir = path.resolve('./src', 'tests', 'browser');
let files = fs.readdirSync(dir).filter(file => file.endsWith('.ts')).map(file => file.replace('.ts', ''));
@@ -230,6 +264,7 @@ const args = argParser.parse_args();
const isProduction = !!args.prd;
const includeSourceMap = !args.no_src_map;
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
const VERSION = isProduction ? JSON.parse(fs.readFileSync('./package.json', 'utf8')).version : '(dev build)';
const TIMESTAMP = Date.now();
@@ -261,7 +296,14 @@ async function main() {
const promises = [];
console.log(isProduction ? 'Building apps...' : 'Initial build...');
for (const app of apps) promises.push(createBundle(app));
for (const app of apps) {
promises.push(createBundle(app));
if (app.themes) {
for (const theme of app.themes) {
promises.push(createTheme(app.name, theme));
}
}
}
for (const example of examples) promises.push(createBundle(example));
for (const browserTest of browserTests) promises.push(createBundle(browserTest));

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 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>
@@ -36,6 +36,12 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
visuals: [merge ? 'structure-element-sphere' : 'element-sphere'],
interior: {
color: Color.fromNormalizedRgb(0, 0, 0),
colorStrength: 0.15,
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
substanceStrength: 1,
}
},
},
colorTheme: {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -50,6 +50,12 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
clipPrimitive: true,
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
interior: {
color: Color.fromNormalizedRgb(0, 0, 0),
colorStrength: 0.15,
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
substanceStrength: 1,
}
},
},
colorTheme: {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -40,6 +40,12 @@ function getSpacefillParams(color: Color, scaleFactor: number, graphics: Graphic
clipPrimitive: true,
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
interior: {
color: Color.fromNormalizedRgb(0, 0, 0),
colorStrength: 0.15,
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
substanceStrength: 1,
}
},
},
colorTheme: {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -35,6 +35,12 @@ function getSpacefillParams(color: Color, graphics: GraphicsMode) {
clipPrimitive: true,
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
interior: {
color: Color.fromNormalizedRgb(0, 0, 0),
colorStrength: 0.15,
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
substanceStrength: 1,
}
},
},
colorTheme: {

View File

@@ -25,6 +25,7 @@ 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);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -46,8 +46,6 @@ function adjustPluginProps(ctx: PluginContext) {
dimColor: Color(0xffffff),
dimStrength: 1,
markerPriority: 2,
interiorColorFlag: false,
interiorDarkening: 0.15,
exposure: 1.1,
xrayEdgeFalloff: 3,
},

View File

@@ -7,34 +7,20 @@
* @author Adam Midlik <midlik@gmail.com>
*/
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { Backgrounds } from '../../extensions/backgrounds';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
import { GeometryExport } from '../../extensions/geo-export';
import { MAQualityAssessment, MAQualityAssessmentConfig, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
import { ModelExport } from '../../extensions/model-export';
import { Mp4Export } from '../../extensions/mp4-export';
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { loadMVSData, loadMVSX } from '../../extensions/mvs/components/formats';
import { loadMVS, MolstarLoadingExtension } from '../../extensions/mvs/load';
import { MVSData } from '../../extensions/mvs/mvs-data';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBValidationReport } from '../../extensions/rcsb';
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { ZenodoImport } from '../../extensions/zenodo';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { StringLike } from '../../mol-io/common/string-like';
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';
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { PluginComponent } from '../../mol-plugin-state/component';
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
import { BuiltInTopologyFormat } from '../../mol-plugin-state/formats/topology';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
@@ -42,98 +28,39 @@ import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { createPluginUI } from '../../mol-plugin-ui';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginBehaviors } from '../../mol-plugin/behavior';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { PluginSpec } from '../../mol-plugin/spec';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginState } from '../../mol-plugin/state';
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
import { MolScriptBuilder } from '../../mol-script/language/builder';
import { Expression } from '../../mol-script/language/expression';
import { StateObjectSelector } from '../../mol-state';
import { Task } from '../../mol-task';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import '../../mol-util/polyfill';
import { ObjectKeys } from '../../mol-util/type-helpers';
import { OpenFiles } from '../../mol-plugin-state/actions/file';
import { StringLike } from '../../mol-io/common/string-like';
import { ExtensionMap } from './extensions';
import { DefaultViewerOptions, ViewerOptions } from './options';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { consoleStats, setDebugMode, setProductionMode, setTimingMode, isProductionMode, isDebugMode, isTimingMode } from '../../mol-util/debug';
export { consoleStats, isDebugMode, isProductionMode, isTimingMode, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
const CustomFormats = [
['g3d', G3dProvider] as const
];
export const ExtensionMap = {
'backgrounds': PluginSpec.Behavior(Backgrounds),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
'assembly-symmetry': PluginSpec.Behavior(AssemblySymmetry),
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
'g3d': PluginSpec.Behavior(G3DFormat),
'model-export': PluginSpec.Behavior(ModelExport),
'mp4-export': PluginSpec.Behavior(Mp4Export),
'geo-export': PluginSpec.Behavior(GeometryExport),
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
'mvs': PluginSpec.Behavior(MolViewSpec),
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
};
const DefaultViewerOptions = {
customFormats: CustomFormats as [string, DataFormatProvider][],
extensions: ObjectKeys(ExtensionMap),
disabledExtensions: [] as string[],
layoutIsExpanded: true,
layoutShowControls: true,
layoutShowRemoteState: true,
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
layoutShowSequence: true,
layoutShowLog: true,
layoutShowLeftPanel: true,
collapseLeftPanel: false,
collapseRightPanel: false,
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
pixelScale: PluginConfig.General.PixelScale.defaultValue,
pickScale: PluginConfig.General.PickScale.defaultValue,
transparency: PluginConfig.General.Transparency.defaultValue,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
illumination: false,
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowToggleFullscreen: PluginConfig.Viewport.ShowToggleFullscreen.defaultValue,
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
config: [] as [PluginConfigItem, any][],
};
type ViewerOptions = typeof DefaultViewerOptions;
import { decodeColor } from '../../mol-util/color/utils';
import '../../mol-util/polyfill';
import { ViewerAutoPreset } from './presets';
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 {
constructor(public plugin: PluginUIContext) {
private _events = new PluginComponent();
public readonly plugin: PluginUIContext;
constructor(plugin: PluginUIContext) {
this.plugin = plugin;
}
static async create(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
@@ -148,11 +75,31 @@ export class Viewer {
const defaultSpec = DefaultPluginUISpec();
const disabledExtension = new Set(o.disabledExtensions ?? []);
let baseBehaviors = defaultSpec.behaviors;
if (o.viewportFocusBehavior === 'disabled') {
baseBehaviors = baseBehaviors.filter(b =>
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 = {
canvas3d: {
...defaultSpec.canvas3d,
},
actions: defaultSpec.actions,
behaviors: [
...defaultSpec.behaviors,
...baseBehaviors,
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
],
animations: [...defaultSpec.animations || []],
@@ -228,10 +175,23 @@ export class Viewer {
plugin.builders.structure.representation.registerPreset(ViewerAutoPreset);
}
});
plugin.canvas3d?.setProps({ illumination: { enabled: o.illumination } });
if (o.viewportBackgroundColor) {
const backgroundColor = decodeColor(o.viewportBackgroundColor);
if (typeof backgroundColor === 'number') {
plugin.canvas3d?.setProps({ renderer: { backgroundColor } });
}
}
return new Viewer(plugin);
}
/**
* Allows subscribing to rxjs observables in the context of the viewer.
* All subscriptions will be disposed of when the viewer is destroyed.
*/
subscribe = this._events.subscribe.bind(this._events);
setRemoteSnapshot(id: string) {
const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
return PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
@@ -523,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));
@@ -542,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);
}
@@ -567,7 +527,56 @@ export class Viewer {
this.plugin.layout.events.updated.next(void 0);
}
/**
* Triggers structure element selection or highlighting based on the provided
* 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, focusOptions }: {
expression?: (queryBuilder: typeof MolScriptBuilder) => Expression,
elements?: StructureElement.Schema,
action: 'highlight' | 'select' | 'focus',
applyGranularity?: boolean,
filterStructure?: (structure: Structure) => boolean,
focusOptions?: Partial<CameraFocusOptions>
}) {
const plugin = this.plugin;
if (!expression && !elements) {
if (action === 'select') {
plugin.managers.interactivity.lociSelects.deselectAll();
} else if (action === 'highlight') {
plugin.managers.interactivity.lociHighlights.clearHighlights();
}
return;
}
const structures = this.plugin.state.data.selectQ(Q => Q.rootsOfType(PluginStateObject.Molecule.Structure));
for (const s of structures) {
if (!s.obj?.data) continue;
if (filterStructure && !filterStructure(s.obj.data)) continue;
const loci = expression
? 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;
}
}
}
dispose() {
this._events.dispose();
this.plugin.dispose();
}
}
@@ -594,44 +603,4 @@ export interface LoadTrajectoryParams {
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInCoordinatesFormat },
coordinatesLabel?: string,
preset?: keyof PresetTrajectoryHierarchy
}
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-viewer-auto',
display: {
name: 'Automatic (w/ Annotation)', group: 'Annotation',
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
},
isApplicable(a) {
return (
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
);
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const structure = structureCell?.obj?.data;
if (!structureCell || !structure) return {};
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
} else {
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
}
}
});
export const PluginExtensions = {
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
mvs: { MVSData, loadMVS, loadMVSData },
modelArchive: {
qualityAssessment: {
config: MAQualityAssessmentConfig
}
}
};
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2018-2025 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 Adam Midlik <midlik@gmail.com>
*/
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { AssemblySymmetry } from '../../extensions/assembly-symmetry';
import { Backgrounds } from '../../extensions/backgrounds';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { G3DFormat } from '../../extensions/g3d/format';
import { GeometryExport } from '../../extensions/geo-export';
import { MAQualityAssessment, MAQualityAssessmentConfig } from '../../extensions/model-archive/quality-assessment/behavior';
import { ModelExport } from '../../extensions/model-export';
import { Mp4Export } from '../../extensions/mp4-export';
import { loadMVS } from '../../extensions/mvs';
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { loadMVSData } from '../../extensions/mvs/components/formats';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBValidationReport } from '../../extensions/rcsb';
import { SbNcbrPartialCharges, SbNcbrTunnels } from '../../extensions/sb-ncbr';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { ZenodoImport } from '../../extensions/zenodo';
import { PluginSpec } from '../../mol-plugin/spec';
import { MVSData } from '../../extensions/mvs/mvs-data';
import * as MVSUtil from '../../extensions/mvs/util';
export const ExtensionMap = {
// Mol* built-in extensions
'mvs': PluginSpec.Behavior(MolViewSpec),
'backgrounds': PluginSpec.Behavior(Backgrounds),
'model-export': PluginSpec.Behavior(ModelExport),
'mp4-export': PluginSpec.Behavior(Mp4Export),
'geo-export': PluginSpec.Behavior(GeometryExport),
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
// 3rd party extensions
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'assembly-symmetry': PluginSpec.Behavior(AssemblySymmetry),
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
'g3d': PluginSpec.Behavior(G3DFormat), // TODO: consider removing this for Mol* 6.0
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
};
export const PluginExtensions = {
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
mvs: {
MVSData,
createBuilder: MVSData.createBuilder,
loadMVS,
loadMVSData,
util: {
...MVSUtil
}
},
modelArchive: {
qualityAssessment: {
config: MAQualityAssessmentConfig
}
}
};

View File

@@ -1,12 +1,16 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import './mvs.html';
import './embedded.html';
import './favicon.ico';
import './index.html';
import '../../mol-plugin-ui/skin/light.scss';
export * from './lib';
export * from './extensions';
export * from './app';
export * from './presets';

58
src/apps/viewer/lib.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as Structure from '../../mol-model/structure';
import { DataLoci, EveryLoci, Loci } from '../../mol-model/loci';
import { Volume } from '../../mol-model/volume';
import { Shape, ShapeGroup } from '../../mol-model/shape';
import * as LinearAlgebra3D from '../../mol-math/linear-algebra/3d';
import { PluginContext } from '../../mol-plugin/context';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { DefaultPluginSpec, PluginSpec } from '../../mol-plugin/spec';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { StateActions } from '../../mol-plugin-state/actions';
import { PluginExtensions } from './extensions';
export const lib = {
structure: {
...Structure,
},
volume: {
Volume,
},
shape: {
Shape,
ShapeGroup,
},
loci: {
Loci,
DataLoci,
EveryLoci,
},
math: {
LinearAlgebra: {
...LinearAlgebra3D,
}
},
plugin: {
PluginContext,
PluginConfig,
PluginBehavior,
PluginSpec,
PluginStateObject,
PluginStateTransform,
StateTransforms,
StateActions,
DefaultPluginSpec,
DefaultPluginUISpec,
},
extensions: {
...PluginExtensions
}
};

179
src/apps/viewer/mvs.html Normal file
View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer MolViewSpec Example</title>
<style>
body {
background: #111318;
}
#app {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#controls {
position: absolute;
display: flex;
align-items: center;
font-family: sans-serif;
gap: 8px;
left: 10px;
top: 10px;
z-index: 10;
background-color: #111318;
padding: 10px;
color: white;
}
</style>
<link rel="stylesheet" type="text/css" href="theme/dark.css" />
</head>
<body>
<div id="app"></div>
<div id="controls">
<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 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() {
viewer.structureInteractivity({ action: 'select' });
}
molstar.Viewer.create('app', {
layoutIsExpanded: true,
layoutShowControls: false,
layoutShowRemoteState: false,
layoutShowSequence: true,
layoutShowLog: false,
layoutShowLeftPanel: true,
viewportShowExpand: true,
viewportShowSelectionMode: false,
viewportShowControls: false,
viewportShowAnimation: false,
viewportFocusBehavior: 'secondary-zoom',
viewportBackgroundColor: '#111318',
pdbProvider: 'rcsb',
emdbProvider: 'rcsb',
}).then(viewer => {
// Make the viewer accessible globally for the demo buttons
window.viewer = viewer;
// Build MVS state
const builder = molstar.lib.extensions.mvs.createBuilder();
const structure = builder
.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif' })
.parse({ format: 'bcif' })
.modelStructure({});
structure
.component({ selector: 'polymer' })
.representation({ type: 'cartoon' })
.color({ color: 'green' });
structure
.component({ selector: 'ligand' })
.representation({ type: 'ball_and_stick' })
.color({ color: '#cc3399' });
// Extra data can be passed to the MVS snapshot via custom state
// and later accessed it using getCurrentMVSSnapshot() (see hover handler below)
// Each node can have custom data as well, but generally could be harder to access
// This example is a little contrived to demonstrate the concept
builder.extendRootCustomState({
extraResidueAnnotations: {
'REA': 'Ligand'
}
})
builder.canvas({
background_color: "#111318",
})
structure.primitives()
.sphere({
center: { label_comp_id: 'REA' },
radius: 3,
custom: { action: 'Action 1' },
})
.label({
text: '1',
position: { label_comp_id: 'REA' },
label_size: 2.5,
label_color: 'blue',
});
structure.primitives()
.sphere({
center: { label_seq_id: 2 },
radius: 3,
custom: { action: 'Action 2' },
})
.label({
text: '2',
position: { label_seq_id: 2 },
label_size: 2.5,
label_color: 'blue',
});
const mvsData = builder.getState();
viewer.loadMvsData(mvsData, 'mvsj');
// Show current residue interaction
viewer.subscribe(viewer.plugin.behaviors.interaction.hover, e => {
const infoElement = document.getElementById('selection-info');
if (!infoElement) return;
if (molstar.lib.structure.StructureElement.Loci.is(e.current.loci)) {
molstar.lib.structure.StructureElement.Loci.forEachLocation(e.current.loci, location => {
const props = molstar.lib.structure.StructureProperties;
let label = `Hovered Residue: ${props.chain.label_asym_id(location)} ${props.residue.label_seq_id(location)}`;
const compId = props.residue.label_comp_id(location);
const snapshot = molstar.lib.extensions.mvs.util.getCurrentMVSSnapshot(viewer.plugin);
if (snapshot && snapshot.root.custom && snapshot.root.custom.extraResidueAnnotations) {
const extra = snapshot.root.custom.extraResidueAnnotations[compId];
if (extra) label += ` (${extra})`;
}
infoElement.innerText = label;
});
} else {
infoElement.innerText = '';
}
});
// Show clicked primitive action
viewer.subscribe(viewer.plugin.behaviors.interaction.click, e => {
const nodes = molstar.lib.extensions.mvs.util.tryGetPrimitivesFromLoci(e.current.loci);
if (nodes?.length) {
alert('Clicked on: ' + (nodes[0].custom?.action || 'unknown'));
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { G3dProvider } from '../../extensions/g3d/format';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import '../../mol-util/polyfill';
import { ObjectKeys } from '../../mol-util/type-helpers';
import { ExtensionMap } from './extensions';
const CustomFormats: [string, DataFormatProvider][] = [
['g3d', G3dProvider] as const
];
export const DefaultViewerOptions = {
customFormats: CustomFormats as [string, DataFormatProvider][],
extensions: ObjectKeys(ExtensionMap),
disabledExtensions: [] as string[],
layoutIsExpanded: true,
layoutShowControls: true,
layoutShowRemoteState: true,
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
layoutShowSequence: true,
layoutShowLog: true,
layoutShowLeftPanel: true,
collapseLeftPanel: false,
collapseRightPanel: false,
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
pixelScale: PluginConfig.General.PixelScale.defaultValue,
pickScale: PluginConfig.General.PickScale.defaultValue,
transparency: PluginConfig.General.Transparency.defaultValue,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
illumination: false,
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowToggleFullscreen: PluginConfig.Viewport.ShowToggleFullscreen.defaultValue,
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
// 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,
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
config: [] as [PluginConfigItem, any][],
};
export type ViewerOptions = typeof DefaultViewerOptions;

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
import { SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../mol-state';
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-viewer-auto',
display: {
name: 'Automatic (w/ Annotation)', group: 'Annotation',
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
},
isApplicable(a) {
return (
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
);
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const structure = structureCell?.obj?.data;
if (!structureCell || !structure) return {};
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
} else {
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
}
}
});

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import '../../../mol-plugin-ui/skin/blue.scss';

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import '../../../mol-plugin-ui/skin/dark.scss';

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import '../../../mol-plugin-ui/skin/light.scss';

View File

@@ -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,

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
#!/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>
*/
@@ -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
*/

View 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());

View File

@@ -39,7 +39,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.

View File

@@ -21,8 +21,10 @@ import { StripedResidues } from './coloring';
import { CustomToastMessage } from './controls';
import { CustomColorThemeProvider } from './custom-theme';
import './index.html';
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData } from './superposition';
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 }
@@ -94,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) } }
}
}
});
@@ -190,6 +192,45 @@ class BasicWrapper {
PluginCommands.Toast.Hide(this.plugin, { key: 'toast-2' });
}
};
/**
* Run TM-align on two structures
* @param pdbId1 - PDB ID of first structure (reference)
* @param chain1 - Chain ID of first structure
* @param pdbId2 - PDB ID of second structure (mobile)
* @param chain2 - Chain ID of second structure
* @param color1 - Optional color for first structure (hex, default blue)
* @param color2 - Optional color for second structure (hex, default red)
*/
tmAlign(pdbId1: string, chain1: string, pdbId2: string, chain2: string, color1?: number, color2?: number) {
return tmAlignStructures(this.plugin, pdbId1, chain1, pdbId2, chain2, color1, color2);
}
/**
* Load two structures without alignment
* @param pdbId1 - PDB ID of first structure
* @param chain1 - Chain ID of first structure
* @param pdbId2 - PDB ID of second structure
* @param chain2 - Chain ID of second structure
* @param color1 - Optional color for first structure (hex, default blue)
* @param color2 - Optional color for second structure (hex, default red)
*/
loadStructures(pdbId1: string, chain1: string, pdbId2: string, chain2: string, color1?: number, color2?: number) {
return loadStructuresNoAlignment(this.plugin, pdbId1, chain1, pdbId2, chain2, color1, color2);
}
/**
* Align two structures using sequence alignment
* @param pdbId1 - PDB ID of first structure (reference)
* @param chain1 - Chain ID of first structure
* @param pdbId2 - PDB ID of second structure (mobile)
* @param chain2 - Chain ID of second structure
* @param color1 - Optional color for first structure (hex, default blue)
* @param color2 - Optional color for second structure (hex, default red)
*/
sequenceAlign(pdbId1: string, chain1: string, pdbId2: string, chain2: string, color1?: number, color2?: number) {
return sequenceAlignStructures(this.plugin, pdbId1, chain1, pdbId2, chain2, color1, color2);
}
}
(window as any).BasicMolStarWrapper = new BasicWrapper();

View File

@@ -5,8 +5,9 @@
*/
import { Mat4 } from '../../mol-math/linear-algebra';
import { QueryContext, StructureSelection } from '../../mol-model/structure';
import { superpose } from '../../mol-model/structure/structure/util/superposition';
import { QueryContext, StructureSelection, StructureElement } from '../../mol-model/structure';
import { superpose, alignAndSuperpose } from '../../mol-model/structure/structure/util/superposition';
import { tmAlign } from '../../mol-model/structure/structure/util/tm-align';
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
import { PluginContext } from '../../mol-plugin/context';
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
@@ -116,4 +117,217 @@ function transform(plugin: PluginContext, s: StateObjectRef<PSO.Molecule.Structu
const b = plugin.state.data.build().to(s)
.insert(StateTransforms.Model.TransformStructureConformation, { transform: { name: 'matrix', params: { data: matrix, transpose: false } } });
return plugin.runTask(plugin.state.data.updateTree(b));
}
export interface TMAlignResult {
tmScoreA: number;
tmScoreB: number;
rmsd: number;
alignedLength: number;
}
/**
* TM-align superposition: aligns two structures using TM-align algorithm
* @param plugin - Mol* plugin context
* @param pdbId1 - PDB ID of first structure (reference)
* @param chain1 - Chain ID of first structure
* @param pdbId2 - PDB ID of second structure (mobile)
* @param chain2 - Chain ID of second structure
* @param color1 - Optional color for first structure (hex, default blue)
* @param color2 - Optional color for second structure (hex, default red)
*/
export async function tmAlignStructures(
plugin: PluginContext,
pdbId1: string,
chain1: string,
pdbId2: string,
chain2: string,
color1: number = 0x3498db,
color2: number = 0xe74c3c
): Promise<TMAlignResult | undefined> {
await plugin.clear();
const url1 = `https://files.rcsb.org/download/${pdbId1}.pdb`;
const url2 = `https://files.rcsb.org/download/${pdbId2}.pdb`;
const label1 = `${pdbId1} Chain ${chain1}`;
const label2 = `${pdbId2} Chain ${chain2}`;
// Load structures
const struct1 = await loadStructure(plugin, url1, 'pdb');
const struct2 = await loadStructure(plugin, url2, 'pdb');
// Build query for C-alpha atoms from specified chains
const caQuery1 = compile<StructureSelection>(MS.struct.generator.atomGroups({
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain1]),
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
}));
const caQuery2 = compile<StructureSelection>(MS.struct.generator.atomGroups({
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain2]),
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
}));
const structure1Data = struct1.structure.cell?.obj?.data;
const structure2Data = struct2.structure.cell?.obj?.data;
if (!structure1Data || !structure2Data) {
console.error('Failed to load structures');
return undefined;
}
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery1(new QueryContext(structure1Data)));
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery2(new QueryContext(structure2Data)));
const loci1 = StructureElement.Loci.is(sel1) ? sel1 : StructureElement.Loci.none(structure1Data);
const loci2 = StructureElement.Loci.is(sel2) ? sel2 : StructureElement.Loci.none(structure2Data);
if (StructureElement.Loci.size(loci1) === 0 || StructureElement.Loci.size(loci2) === 0) {
console.error('Empty selection - cannot run TM-align');
// Still show the structures without alignment
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
return undefined;
}
// Run TM-align
const result = tmAlign(loci1, loci2);
console.log('TM-score (structure 1):', result.tmScoreA.toFixed(5));
console.log('TM-score (structure 2):', result.tmScoreB.toFixed(5));
console.log('RMSD:', result.rmsd.toFixed(2), 'A');
console.log('Aligned residues:', result.alignedLength);
// Apply the transformation to superimpose structure 2 onto structure 1
await transform(plugin, struct2.structure, result.bTransform);
// Add cartoon representations
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
return {
tmScoreA: result.tmScoreA,
tmScoreB: result.tmScoreB,
rmsd: result.rmsd,
alignedLength: result.alignedLength
};
}
async function addChainRepresentation(
plugin: PluginContext,
structure: StateObjectRef<PSO.Molecule.Structure>,
chain: string,
label: string,
color: number
) {
const component = await plugin.builders.structure.tryCreateComponentFromExpression(
structure,
chainSelection(chain),
label
);
if (component) {
await plugin.builders.structure.representation.addRepresentation(component, {
type: 'cartoon',
color: 'uniform',
colorParams: { value: color }
});
}
}
/**
* Load and display two structures without any alignment
* @param plugin - Mol* plugin context
* @param pdbId1 - PDB ID of first structure
* @param chain1 - Chain ID of first structure
* @param pdbId2 - PDB ID of second structure
* @param chain2 - Chain ID of second structure
* @param color1 - Optional color for first structure (hex, default blue)
* @param color2 - Optional color for second structure (hex, default red)
*/
export async function loadStructuresNoAlignment(
plugin: PluginContext,
pdbId1: string,
chain1: string,
pdbId2: string,
chain2: string,
color1: number = 0x3498db,
color2: number = 0xe74c3c
): Promise<void> {
await plugin.clear();
const url1 = `https://files.rcsb.org/download/${pdbId1}.pdb`;
const url2 = `https://files.rcsb.org/download/${pdbId2}.pdb`;
const label1 = `${pdbId1} Chain ${chain1}`;
const label2 = `${pdbId2} Chain ${chain2}`;
const struct1 = await loadStructure(plugin, url1, 'pdb');
const struct2 = await loadStructure(plugin, url2, 'pdb');
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
console.log('Loaded structures - NO ALIGNMENT');
}
/**
* Sequence-based superposition: aligns two structures using sequence alignment + RMSD minimization
* @param plugin - Mol* plugin context
* @param pdbId1 - PDB ID of first structure (reference)
* @param chain1 - Chain ID of first structure
* @param pdbId2 - PDB ID of second structure (mobile)
* @param chain2 - Chain ID of second structure
* @param color1 - Optional color for first structure (hex, default blue)
* @param color2 - Optional color for second structure (hex, default red)
*/
export async function sequenceAlignStructures(
plugin: PluginContext,
pdbId1: string,
chain1: string,
pdbId2: string,
chain2: string,
color1: number = 0x3498db,
color2: number = 0xe74c3c
): Promise<{ rmsd: number }> {
await plugin.clear();
const url1 = `https://files.rcsb.org/download/${pdbId1}.pdb`;
const url2 = `https://files.rcsb.org/download/${pdbId2}.pdb`;
const label1 = `${pdbId1} Chain ${chain1}`;
const label2 = `${pdbId2} Chain ${chain2}`;
const struct1 = await loadStructure(plugin, url1, 'pdb');
const struct2 = await loadStructure(plugin, url2, 'pdb');
// Build queries for C-alpha atoms from specified chains
const caQuery1 = compile<StructureSelection>(MS.struct.generator.atomGroups({
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain1]),
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
}));
const caQuery2 = compile<StructureSelection>(MS.struct.generator.atomGroups({
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain2]),
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
}));
const structure1Data = struct1.structure.cell?.obj?.data;
const structure2Data = struct2.structure.cell?.obj?.data;
if (!structure1Data || !structure2Data) {
console.error('Failed to load structures');
return { rmsd: 0 };
}
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery1(new QueryContext(structure1Data)));
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery2(new QueryContext(structure2Data)));
// Run sequence alignment + superposition
const transforms = alignAndSuperpose([sel1, sel2]);
// Apply the transformation to superimpose structure 2 onto structure 1
await transform(plugin, struct2.structure, transforms[0].bTransform);
// Add cartoon representations
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
console.log('RMSD:', transforms[0].rmsd.toFixed(2), 'A');
return { rmsd: transforms[0].rmsd };
}

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>TM-align Superposition</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
#app {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
<script type="text/javascript" src="./index.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// Initialize and automatically run TM-align superposition
BasicMolStarWrapper.init('app').then(() => {
BasicMolStarWrapper.setBackground(0xffffff);
BasicMolStarWrapper.tests.tmAlignSuperposition();
});
</script>
</body>
</html>

View File

@@ -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) } }
}
}
});

View File

@@ -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

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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');
};
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 : [] });
}));

View File

@@ -67,6 +67,12 @@ export function getPrimitiveStructureRefs(primitives: MolstarSubtree<'primitives
export class MVSPrimitivesData extends SO.Create<PrimitiveBuilderContext>({ name: 'Primitive Data', typeClass: 'Object' }) { }
export class MVSPrimitiveShapes extends SO.Create<{ mesh?: Shape<Mesh>, labels?: Shape<Text> }>({ name: 'Primitive Shapes', typeClass: 'Object' }) { }
export interface MVSPrimitiveShapeSourceData {
kind: 'mvs-primitives',
node: MVSNode<'primitives'>,
groupToNode: Map<number, MVSNode<'primitive'>>,
}
export type MVSDownloadPrimitiveData = typeof MVSDownloadPrimitiveData
export const MVSDownloadPrimitiveData = MVSTransform({
name: 'mvs-download-primitive-data',
@@ -605,7 +611,7 @@ function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shap
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
} satisfies MVSPrimitiveShapeSourceData,
MeshBuilder.getMesh(meshBuilder),
(g) => colors.get(g) as Color ?? color,
(g) => 1,
@@ -638,7 +644,7 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
} satisfies MVSPrimitiveShapeSourceData,
linesBuilder.getLines(),
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,
@@ -673,7 +679,7 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev: Text | und
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
} satisfies MVSPrimitiveShapeSourceData,
labelsBuilder.getText(),
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,

View File

@@ -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;
}

View File

@@ -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([

View File

@@ -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 };
}

View File

@@ -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 };
},
};

View File

@@ -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 {
@@ -520,7 +524,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 +559,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)!;

View File

@@ -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>
@@ -46,6 +46,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. */
@@ -95,7 +97,10 @@ async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSDat
options
);
await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length);
entries.push(entry);
entries.push({
...entry,
_transientData: { sourceMvsSnapshot: snapshot }
});
if (ctx.shouldUpdate) {
await ctx.update({ message: 'Loading MVS...', current: i, max: multiData.snapshots.length });
@@ -172,15 +177,21 @@ 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);
@@ -294,7 +305,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':

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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). */

View File

@@ -4,8 +4,14 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { OrderedSet } from '../../mol-data/int';
import { Loci } from '../../mol-model/loci';
import { ShapeGroup } from '../../mol-model/shape';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector, StateTree } from '../../mol-state';
import type { MVSPrimitiveShapeSourceData } from './components/primitives';
import type { Snapshot } from './mvs-data';
import type { MVSNode } from './tree/mvs/mvs-tree';
/**
@@ -33,4 +39,26 @@ export function createMVSRefMap(plugin: PluginContext) {
});
return mapping;
}
export function tryGetPrimitivesFromLoci(loci: Loci | undefined): MVSNode<'primitive'>[] | undefined {
if (!ShapeGroup.isLoci(loci)) return undefined;
const srcData = loci.shape.sourceData as MVSPrimitiveShapeSourceData;
if (srcData?.kind !== 'mvs-primitives') return undefined;
const nodes: MVSNode<'primitive'>[] = [];
for (const group of loci.groups) {
OrderedSet.forEach(group.ids, id => {
const node = srcData.groupToNode.get(id);
if (node) nodes.push(node);
});
}
return nodes.length > 0 ? nodes : undefined;
}
// Retrieves the MVS snapshot associated with the current snapshot of the plugin
// This will only work if the current state was created from an MVS snapshot
export function getCurrentMVSSnapshot(plugin: PluginContext): Snapshot | undefined {
return plugin.managers.snapshot.current?._transientData?.sourceMvsSnapshot;
}

View File

@@ -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 = {

View File

@@ -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>
@@ -96,6 +96,7 @@ export const Canvas3DParams = {
cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
transparentBackground: PD.Boolean(false),
checkeredTransparentBackground: PD.Boolean(false),
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
@@ -405,6 +406,22 @@ const cancelAnimationFrame = typeof window !== 'undefined'
? window.cancelAnimationFrame
: (handle: number) => clearImmediate(handle as unknown as NodeJS.Immediate);
function syncCanvasBackground(canvas: HTMLCanvasElement, canvasProps: Canvas3DProps) {
if (canvasProps.transparentBackground && canvasProps.checkeredTransparentBackground) {
Object.assign(canvas.style, {
'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)',
'background-size': '60px 60px',
'background-position': '0 0, 30px 30px'
});
} else {
Object.assign(canvas.style, {
'background-image': '',
'background-size': '',
'background-position': ''
});
}
}
namespace Canvas3D {
export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
@@ -435,6 +452,7 @@ namespace Canvas3D {
let forceNextRender = false;
let currentTime = 0;
syncCanvasBackground(canvas!, p);
updateViewport();
const scene = Scene.create(webgl, passes.draw.transparency, {
dColorMarker: p.renderer.colorMarker,
@@ -546,7 +564,6 @@ namespace Canvas3D {
}
}),
xrManager.sessionChanged.subscribe(() => {
fenceSync = null;
resizeRequested = true;
if (xrManager.session) {
saveNonXRProps();
@@ -628,8 +645,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;
@@ -646,14 +661,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;
@@ -717,10 +724,6 @@ namespace Canvas3D {
}
}
if (didRender && !xrFrame) {
fenceSync = webgl.getFenceSync();
}
return didRender;
}
@@ -1061,6 +1064,7 @@ namespace Canvas3D {
cameraResetDurationMs: p.cameraResetDurationMs,
sceneRadiusFactor: p.sceneRadiusFactor,
transparentBackground: p.transparentBackground,
checkeredTransparentBackground: p.checkeredTransparentBackground,
dpoitIterations: p.dpoitIterations,
pickPadding: p.pickPadding,
userInteractionReleaseMs: p.userInteractionReleaseMs,
@@ -1083,7 +1087,6 @@ namespace Canvas3D {
const contextLostSub = contextLost?.subscribe(() => {
isContextLost = true;
fenceSync = null;
pickHelper.dirty = true;
});
@@ -1211,10 +1214,6 @@ namespace Canvas3D {
reprRenderObjects.clear();
scene.clear();
helper.debug.clear();
if (fenceSync !== null) {
webgl.deleteSync(fenceSync);
fenceSync = null;
}
requestDraw();
reprCount.next(reprRenderObjects.size);
},
@@ -1311,6 +1310,7 @@ namespace Canvas3D {
}
if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
if (props.checkeredTransparentBackground !== undefined) p.checkeredTransparentBackground = props.checkeredTransparentBackground;
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
if (props.pickPadding !== undefined) {
p.pickPadding = props.pickPadding;
@@ -1357,6 +1357,12 @@ namespace Canvas3D {
p.camera.stereo.name = 'off';
}
if ('transparentBackground' in props
|| 'checkeredTransparentBackground' in props
|| (props.renderer && 'backgroundColor' in props.renderer)) {
syncCanvasBackground(canvas!, p);
}
shaderManager.updateRequired(p);
if (!doNotRequestDraw) {
requestDraw();
@@ -1420,11 +1426,6 @@ namespace Canvas3D {
rayHelper.dispose();
xrManager.dispose();
if (fenceSync !== null) {
webgl.deleteSync(fenceSync);
fenceSync = null;
}
reprCount.complete();
interactionEvent.complete();
didDraw.complete();

View File

@@ -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) {

View File

@@ -154,6 +154,7 @@ export class PickHelper {
if (this.dirty) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.identify');
this.webgl.resources.finalizePrograms(['pick'], true);
this.render(camera);
this.buffers.read();
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.identify');

View File

@@ -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;

View File

@@ -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) {

View File

@@ -390,7 +390,7 @@ export class PickBuffers {
this.fenceTimestamp = now();
this.fenceSync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
// gl.flush();
gl.flush();
this.ready = false;
if (isTimingMode) this.webgl.timer.markEnd('PickBuffers.asyncRead');

View File

@@ -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 }),
@@ -681,26 +682,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;

View File

@@ -542,4 +542,38 @@ 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;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-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.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -28,6 +28,7 @@ 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';
export interface Cylinders {
readonly kind: 'cylinders',
@@ -63,6 +64,7 @@ export interface Cylinders {
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
hasBoundingSphere(): boolean
}
export namespace Cylinders {
@@ -131,6 +133,9 @@ export namespace Cylinders {
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(cylinders);
},
hasBoundingSphere() {
return currentHash === hashCode(cylinders);
}
};
return cylinders;
@@ -174,6 +179,7 @@ export namespace Cylinders {
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
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(),
colorMode: PD.Select('default', PD.arrayToOptions(['default', 'interpolate'] as const), BaseGeometry.ShadingCategory)
};
export type Params = typeof Params
@@ -215,7 +221,7 @@ export namespace Cylinders {
const positionIt = createPositionIterator(cylinders, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const size = createSizes(locationIt, positionIt, theme.size);
const marker = props.instanceGranularity
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
@@ -266,6 +272,8 @@ 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'),
};
}
@@ -287,6 +295,8 @@ 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');
}

View File

@@ -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>
*/
@@ -55,6 +55,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 +115,9 @@ export namespace DirectVolume {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(directVolume);
},
hasBoundingSphere() {
return currentHash === hashCode(directVolume);
},
meta: {}
};
return directVolume;

View File

@@ -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;

View File

@@ -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>
*/
@@ -73,6 +73,7 @@ interface Image {
readonly boundingSphere: Sphere3D
setBoundingSphere(boundingSphere: Sphere3D): void
hasBoundingSphere(): boolean
}
namespace Image {
@@ -133,6 +134,9 @@ namespace Image {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(image);
},
hasBoundingSphere() {
return currentHash === hashCode(image);
}
};
return image;
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
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';
export function getInteriorParam() {
return PD.Group({
color: PD.Color(Color.fromRgb(76, 76, 76)),
colorStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
substance: Material.getParam(),
substanceStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
});
}
export type InteriorProp = ReturnType<typeof getInteriorParam>['defaultValue'];
export function areInteriorPropsEquals(a: InteriorProp, b: InteriorProp): 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 {
Color.toArrayNormalized(props.color, out, 0);
out[3] = props.colorStrength;
return out;
}
export function getInteriorSubstance(props: InteriorProp, out: Vec4): Vec4 {
Material.toArrayNormalized(props.substance, out, 0);
out[3] = props.substanceStrength;
return out;
}

View File

@@ -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);
}
};
}
}

View File

@@ -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>
*/
@@ -35,6 +35,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 +49,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 +76,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 +103,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 +121,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 +150,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);
}
@@ -208,7 +228,7 @@ export namespace Lines {
const positionIt = createPositionIterator(lines, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const size = createSizes(locationIt, positionIt, theme.size);
const marker = props.instanceGranularity
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
@@ -218,7 +238,7 @@ export namespace Lines {
const material = createEmptySubstance();
const clipping = createEmptyClipping();
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);
@@ -249,6 +269,9 @@ export namespace Lines {
dLineSizeAttenuation: ValueCell.create(props.lineSizeAttenuation),
uDoubleSided: ValueCell.create(true),
dFlipSided: ValueCell.create(false),
stripCount: lines.stripCount,
stripOffsets: lines.stripBuffer,
};
}

View File

@@ -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);

View File

@@ -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>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -29,6 +29,7 @@ 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';
export interface Mesh {
readonly kind: 'mesh',
@@ -55,6 +56,7 @@ export interface Mesh {
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
hasBoundingSphere(): boolean
readonly meta: { [k: string]: unknown }
}
@@ -119,6 +121,9 @@ export namespace Mesh {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(mesh);
},
hasBoundingSphere() {
return currentHash === hashCode(mesh);
},
meta: {}
};
return mesh;
@@ -633,6 +638,7 @@ export namespace Mesh {
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
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(),
};
export type Params = typeof Params
@@ -719,6 +725,8 @@ 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),
};
@@ -741,6 +749,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));
}
function updateBoundingSphere(values: MeshValues, mesh: Mesh) {

View File

@@ -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>
*/
@@ -46,6 +46,7 @@ export interface Points {
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
hasBoundingSphere(): boolean
}
export namespace Points {
@@ -99,6 +100,9 @@ export namespace Points {
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(points);
},
hasBoundingSphere() {
return currentHash === hashCode(points);
}
};
return points;
@@ -170,7 +174,7 @@ export namespace Points {
const positionIt = createPositionIterator(points, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const size = createSizes(locationIt, positionIt, theme.size);
const marker = props.instanceGranularity
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');

View File

@@ -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);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 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>
*/
@@ -27,6 +27,7 @@ import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
import { RenderableState } from '../../../mol-gl/renderable';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
export interface Spheres {
readonly kind: 'spheres',
@@ -45,6 +46,7 @@ export interface Spheres {
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
hasBoundingSphere(): boolean
shaderData: Spheres.ShaderData
}
@@ -115,6 +117,9 @@ export namespace Spheres {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(spheres);
},
hasBoundingSphere() {
return currentHash === hashCode(spheres);
},
shaderData: {
positionGroup,
texDim,
@@ -256,6 +261,7 @@ export namespace Spheres {
alphaThickness: PD.Numeric(0, { min: 0, max: 20, step: 1 }, { ...BaseGeometry.ShadingCategory, description: 'If not zero, adjusts alpha for radius.' }),
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(),
lodLevels: PD.ObjectList({
minDistance: PD.Numeric(0),
maxDistance: PD.Numeric(0),
@@ -304,7 +310,7 @@ export namespace Spheres {
const positionIt = createPositionIterator(spheres, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const size = createSizes(locationIt, positionIt, theme.size);
const marker = props.instanceGranularity
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
@@ -356,6 +362,8 @@ export namespace Spheres {
uAlphaThickness: ValueCell.create(props.alphaThickness),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
lodLevels: spheres.shaderData.lodLevels,
centerBuffer: spheres.centerBuffer,
@@ -383,6 +391,8 @@ export namespace Spheres {
ValueCell.updateIfChanged(values.uAlphaThickness, props.alphaThickness);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
const lodLevels = getLodLevels(values.lodLevels.ref.value as LodLevelsValue);
if (!areLodLevelsEqual(props.lodLevels, lodLevels)) {

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -76,7 +77,7 @@ export class FontAtlas {
this.props = p;
// create measurements
const fontSize = 32 * (p.fontQuality + 1);
const fontSize = 64 * (p.fontQuality + 1);
this.buffer = fontSize / 8;
this.radius = fontSize / 3;
this.lineHeight = Math.round(fontSize + 2 * this.buffer + this.radius);

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -118,8 +119,9 @@ export namespace TextBuilder {
const xLeft = (-xShift - margin - 0.1) * scale;
const xRight = (bWidth - xShift + margin + 0.1) * scale;
const yTop = (bHeight - yShift + margin) * scale;
const yBottom = (-yShift - margin) * scale;
const yMid = 0.5 - yShift - outline * 0.5; // glyph vertical midpoint accounting for outline offset
const yTop = (yMid + bHeight / 2 + margin) * scale;
const yBottom = (yMid - bHeight / 2 - margin) * scale;
// background
if (background) {

View File

@@ -1,8 +1,9 @@
/**
* 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 Adam Midlik <midlik@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -68,6 +69,7 @@ export interface Text {
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
hasBoundingSphere(): boolean
}
export namespace Text {
@@ -134,6 +136,9 @@ export namespace Text {
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(text);
},
hasBoundingSphere() {
return currentHash === hashCode(text);
}
};
return text;
@@ -212,7 +217,7 @@ export namespace Text {
const positionIt = createPositionIterator(text, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const size = createSizes(locationIt, positionIt, theme.size);
const marker = props.instanceGranularity
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
@@ -323,16 +328,17 @@ export namespace Text {
}
function getPadding(mappings: Float32Array, depths: Float32Array, charCount: number, scale: number) {
let maxOffset = 0;
let maxOffsetX = 0;
let maxOffsetY = 0;
let maxDepth = 0;
for (let i = 0, il = charCount * 4; i < il; ++i) {
const i2 = 2 * i;
const ox = Math.abs(mappings[i2]);
if (ox > maxOffset) maxOffset = ox;
if (ox > maxOffsetX) maxOffsetX = ox;
const oy = Math.abs(mappings[i2 + 1]);
if (oy > maxOffset) maxOffset = oy;
if (oy > maxOffsetY) maxOffsetY = oy;
const d = Math.abs(depths[i]);
if (d > maxDepth) maxDepth = d;
}
return Math.max(maxDepth, scale * maxOffset);
return Math.max(maxDepth, scale * Math.sqrt(maxOffsetX * maxOffsetX + maxOffsetY * maxOffsetY));
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -14,7 +14,7 @@ import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, Values, DefineSpec
import { quad_vert } from '../../../mol-gl/shader/quad.vert';
import { normalize_frag } from '../../../mol-gl/shader/compute/color-smoothing/normalize.frag';
import { QuadSchema, QuadValues } from '../../../mol-gl/compute/util';
import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
import { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
import { accumulate_frag } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.frag';
import { accumulate_vert } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.vert';
@@ -246,18 +246,30 @@ interface ColorSmoothingInput extends AccumulateInput {
invariantBoundingSphere: Sphere3D
}
export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl: WebGLContext, texture?: Texture) {
export type ColorSmoothingOptions = {
resolution: number,
stride: number
};
export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, options: ColorSmoothingOptions, webgl: WebGLContext, texture?: Texture) {
const { drawBuffers } = webgl.extensions;
if (!drawBuffers) throw new Error('need WebGL draw buffers');
if (isTimingMode) webgl.timer.mark('calcTextureMeshColorSmoothing');
const { gl, resources, state, extensions: { colorBufferHalfFloat, textureHalfFloat } } = webgl;
const { resolution, stride } = options;
const isInstanceType = input.colorType.endsWith('Instance');
const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
const pad = 1 + resolution;
const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
if (!isInstanceType) {
input.instanceCount = 1;
input.instanceBuffer = new Float32Array([0]);
input.transformBuffer = new Float32Array(Mat4.id);
}
const scaleFactor = 1 / resolution;
const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor);
const gridDim = Box3D.size(Vec3(), scaledBox);
@@ -389,7 +401,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
const type = isInstanceType ? 'volumeInstance' : 'volume';
if (isTimingMode) webgl.timer.markEnd('calcTextureMeshColorSmoothing');
// printTextureImage(readTexture(webgl, texture), { scale: 0.75 });
// printTextureImage(readTexture(webgl, texture), { scale: 0.75, id: `${texture.id}` });
return { texture, gridDim, gridTexDim: Vec2.create(width, height), gridTransform, type };
}
@@ -404,10 +416,10 @@ function isSupportedColorType(x: string): x is 'group' | 'groupInstance' {
return x === 'group' || x === 'groupInstance';
}
export function applyTextureMeshColorSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshColorSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedColorType(values.dColorType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingRgbName]) {
webgl.namedTextures[ColorSmoothingRgbName] = webgl.resources.texture('image-uint8', 'rgb', 'ubyte', 'nearest');
@@ -427,7 +439,7 @@ export function applyTextureMeshColorSmoothing(values: TextureMeshValues, resolu
colorType: values.dColorType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
ValueCell.update(values.tColorGrid, smoothingData.texture);
@@ -440,10 +452,10 @@ function isSupportedOverpaintType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedOverpaintType(values.dOverpaintType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingRgbaName]) {
webgl.namedTextures[ColorSmoothingRgbaName] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
@@ -463,7 +475,7 @@ export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, re
colorType: values.dOverpaintType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dOverpaintType, smoothingData.type);
ValueCell.update(values.tOverpaintGrid, smoothingData.texture);
@@ -476,10 +488,10 @@ function isSupportedTransparencyType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedTransparencyType(values.dTransparencyType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingAlphaName]) {
webgl.namedTextures[ColorSmoothingAlphaName] = webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'nearest');
@@ -499,7 +511,7 @@ export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues,
colorType: values.dTransparencyType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dTransparencyType, smoothingData.type);
ValueCell.update(values.tTransparencyGrid, smoothingData.texture);
@@ -512,10 +524,10 @@ function isSupportedEmissiveType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedEmissiveType(values.dEmissiveType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingAlphaName]) {
webgl.namedTextures[ColorSmoothingAlphaName] = webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'nearest');
@@ -535,7 +547,7 @@ export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, res
colorType: values.dEmissiveType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dEmissiveType, smoothingData.type);
ValueCell.update(values.tEmissiveGrid, smoothingData.texture);
@@ -548,10 +560,10 @@ function isSupportedSubstanceType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedSubstanceType(values.dSubstanceType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingRgbaName]) {
webgl.namedTextures[ColorSmoothingRgbaName] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
@@ -571,7 +583,7 @@ export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, re
colorType: values.dSubstanceType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
ValueCell.update(values.tSubstanceGrid, smoothingData.texture);

View File

@@ -28,6 +28,7 @@ import { createEmptySubstance } from '../substance-data';
import { RenderableState } from '../../../mol-gl/renderable';
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
export interface TextureMesh {
readonly kind: 'texture-mesh',
@@ -128,6 +129,7 @@ export namespace TextureMesh {
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
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(),
};
export type Params = typeof Params
@@ -244,6 +246,8 @@ export namespace TextureMesh {
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
meta: ValueCell.create(textureMesh.meta),
};
@@ -266,6 +270,8 @@ export namespace TextureMesh {
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
}
function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {

View File

@@ -32,6 +32,8 @@ export const CylindersSchema = {
dSolidInterior: DefineSpec('boolean'),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
dDualColor: DefineSpec('boolean'),
};
export type CylindersSchema = typeof CylindersSchema

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { Renderable, RenderableState, createRenderable } from '../renderable';
import { WebGLContext } from '../webgl/context';
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, ValueSpec } from './schema';
import { ValueCell } from '../../mol-util';
import { LinesShaderCode } from '../shader-code';
@@ -22,6 +22,8 @@ export const LinesSchema = {
dLineSizeAttenuation: DefineSpec('boolean'),
uDoubleSided: UniformSpec('b', 'material'),
dFlipSided: DefineSpec('boolean'),
stripCount: ValueSpec('number'),
stripOffsets: ValueSpec('uint32'),
};
export type LinesSchema = typeof LinesSchema
export type LinesValues = Values<LinesSchema>

View File

@@ -27,6 +27,8 @@ export const MeshSchema = {
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
meta: ValueSpec('unknown')
} as const;
export type MeshSchema = typeof MeshSchema

View File

@@ -162,10 +162,6 @@ export const GlobalUniformSchema = {
uPickingAlphaThreshold: UniformSpec('f'),
uInteriorDarkening: UniformSpec('f'),
uInteriorColorFlag: UniformSpec('b'),
uInteriorColor: UniformSpec('v3'),
uHighlightColor: UniformSpec('v3'),
uSelectColor: UniformSpec('v3'),
uDimColor: UniformSpec('v3'),

View File

@@ -30,6 +30,8 @@ export const SpheresSchema = {
uAlphaThickness: UniformSpec('f'),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
lodLevels: ValueSpec('unknown'),
centerBuffer: ValueSpec('float32'),

View File

@@ -27,6 +27,8 @@ export const TextureMeshSchema = {
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
meta: ValueSpec('unknown')
};
export type TextureMeshSchema = typeof TextureMeshSchema

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -82,6 +82,13 @@ export function printTextureImage(textureImage: TextureImage<any>, options: Part
} else {
data.set(array);
}
} else if (itemSize === 3) {
for (let i = 0, il = width * height; i < il; ++i) {
data[i * 4] = array[i * 3];
data[i * 4 + 1] = array[i * 3 + 1];
data[i * 4 + 2] = array[i * 3 + 2];
data[i * 4 + 3] = 255;
}
} else {
console.warn(`itemSize '${itemSize}' not supported`);
}

View File

@@ -95,10 +95,6 @@ export const RendererParams = {
pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
interiorDarkening: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
interiorColorFlag: PD.Boolean(true, { label: 'Use Interior Color' }),
interiorColor: PD.Color(Color.fromNormalizedRgb(0.3, 0.3, 0.3)),
colorMarker: PD.Boolean(true, { description: 'Enable color marker' }),
highlightColor: PD.Color(Color.fromNormalizedRgb(1.0, 0.4, 0.6)),
selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
@@ -269,10 +265,6 @@ namespace Renderer {
uPickingAlphaThreshold: ValueCell.create(p.pickingAlphaThreshold),
uInteriorDarkening: ValueCell.create(p.interiorDarkening),
uInteriorColorFlag: ValueCell.create(p.interiorColorFlag),
uInteriorColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.interiorColor)),
uHighlightColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.highlightColor)),
uSelectColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.selectColor)),
uDimColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.dimColor)),
@@ -849,19 +841,6 @@ namespace Renderer {
ValueCell.update(globalUniforms.uPickingAlphaThreshold, p.pickingAlphaThreshold);
}
if (props.interiorDarkening !== undefined && props.interiorDarkening !== p.interiorDarkening) {
p.interiorDarkening = props.interiorDarkening;
ValueCell.update(globalUniforms.uInteriorDarkening, p.interiorDarkening);
}
if (props.interiorColorFlag !== undefined && props.interiorColorFlag !== p.interiorColorFlag) {
p.interiorColorFlag = props.interiorColorFlag;
ValueCell.update(globalUniforms.uInteriorColorFlag, p.interiorColorFlag);
}
if (props.interiorColor !== undefined && props.interiorColor !== p.interiorColor) {
p.interiorColor = props.interiorColor;
ValueCell.update(globalUniforms.uInteriorColor, Color.toVec3Normalized(globalUniforms.uInteriorColor.ref.value, p.interiorColor));
}
if (props.colorMarker !== undefined && props.colorMarker !== p.colorMarker) {
p.colorMarker = props.colorMarker;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -207,7 +207,7 @@ export const CylindersShaderCode = ShaderCode('cylinders', cylinders_vert, cylin
import { text_vert } from './shader/text.vert';
import { text_frag } from './shader/text.frag';
export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { drawBuffers: 'optional' }, {}, ignoreDefineUnlit);
export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { fragDepth: 'optional', drawBuffers: 'optional' }, {}, ignoreDefineUnlit);
import { lines_vert } from './shader/lines.vert';
import { lines_frag } from './shader/lines.frag';

View File

@@ -1,13 +1,14 @@
export const apply_interior_color = `
if (interior) {
if (uInteriorColorFlag) {
gl_FragColor.rgb = uInteriorColor;
} else {
gl_FragColor.rgb *= 1.0 - uInteriorDarkening;
}
material.rgb = mix(material.rgb, uInteriorColor.rgb, uInteriorColor.a);
float isf = clamp(uInteriorSubstance.a, 0.0, 0.99); // clamp to avoid artifacts
metalness = mix(metalness, uInteriorSubstance.r, isf);
roughness = mix(roughness, uInteriorSubstance.g, isf);
bumpiness = mix(bumpiness, uInteriorSubstance.b, isf);
#ifdef dTransparentBackfaces_opaque
gl_FragColor.a = 1.0;
material.a = 1.0;
#endif
}
`;

View File

@@ -9,9 +9,13 @@ export const assign_size = `
float size = unpackRGBToInt(readFromTexture(tSize, group, uSizeTexDim).rgb);
#elif defined(dSizeType_groupInstance)
float size = unpackRGBToInt(readFromTexture(tSize, aInstance * float(uGroupCount) + group, uSizeTexDim).rgb);
#elif defined(dSizeType_vertex)
float size = unpackRGBToInt(readFromTexture(tSize, vertexId, uSizeTexDim).rgb);
#elif defined(dSizeType_vertexInstance)
float size = unpackRGBToInt(readFromTexture(tSize, int(aInstance) * uVertexCount + vertexId, uSizeTexDim).rgb);
#endif
#if defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance)
#if defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance) || defined(dSizeType_vertex) || defined(dSizeType_vertexInstance)
size /= 100.0; // NOTE factor also set in TypeScript
#endif

View File

@@ -72,9 +72,6 @@ uniform float uPickingAlphaThreshold;
uniform bool uTransparentBackground;
uniform bool uDoubleSided;
uniform float uInteriorDarkening;
uniform bool uInteriorColorFlag;
uniform vec3 uInteriorColor;
bool interior;
uniform float uXrayEdgeFalloff;

View File

@@ -3,7 +3,7 @@ export const size_vert_params = `
uniform float uSize;
#elif defined(dSizeType_attribute)
attribute float aSize;
#elif defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance)
#elif defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance) || defined(dSizeType_vertex) || defined(dSizeType_vertexInstance)
uniform vec2 uSizeTexDim;
uniform sampler2D tSize;
#endif

View File

@@ -23,6 +23,9 @@ uniform vec3 uCameraDir;
uniform vec3 uCameraPosition;
uniform mat4 uInvView;
uniform vec4 uInteriorColor;
uniform vec4 uInteriorSubstance;
#include common
#include common_frag_params
#include color_frag_params
@@ -177,6 +180,13 @@ bool CylinderImpostor(
if (!objectClipped) {
fragmentDepth = 0.0 + (0.0000002 / vSize);
cameraNormal = -rayDir;
// intersection of ray in model space with near plane in camera space
vec3 cameraRayOrigin = (uView * vec4(rayOrigin, 1.0)).xyz;
vec3 cameraRayDir = (uView * vec4(rayDir, 0.0)).xyz;
float nearT = - (uNear + cameraRayOrigin.z) / cameraRayDir.z;
viewPosition = cameraRayOrigin + nearT * cameraRayDir;
modelPosition = (uInvView * vec4(viewPosition, 1.0)).xyz;
}
#endif
return true;
@@ -197,6 +207,13 @@ bool CylinderImpostor(
if (!objectClipped) {
fragmentDepth = 0.0 + (0.0000002 / vSize);
cameraNormal = -rayDir;
// intersection of ray in model space with near plane in camera space
vec3 cameraRayOrigin = (uView * vec4(rayOrigin, 1.0)).xyz;
vec3 cameraRayDir = (uView * vec4(rayDir, 0.0)).xyz;
float nearT = - (uNear + cameraRayOrigin.z) / cameraRayDir.z;
viewPosition = cameraRayOrigin + nearT * cameraRayDir;
modelPosition = (uInvView * vec4(viewPosition, 1.0)).xyz;
}
#endif
return true;
@@ -216,6 +233,13 @@ bool CylinderImpostor(
if (!objectClipped) {
fragmentDepth = 0.0 + (0.0000002 / vSize);
cameraNormal = -rayDir;
// intersection of ray in model space with near plane in camera space
vec3 cameraRayOrigin = (uView * vec4(rayOrigin, 1.0)).xyz;
vec3 cameraRayDir = (uView * vec4(rayDir, 0.0)).xyz;
float nearT = - (uNear + cameraRayOrigin.z) / cameraRayDir.z;
viewPosition = cameraRayOrigin + nearT * cameraRayDir;
modelPosition = (uInvView * vec4(viewPosition, 1.0)).xyz;
}
#endif
return true;
@@ -274,8 +298,8 @@ void main() {
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#include apply_light_color
#include apply_interior_color
#include apply_light_color
#include apply_marker_color
#if defined(dRenderVariant_color)

Some files were not shown because too many files have changed in this diff Show More