mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 14:04:36 +08:00
Compare commits
354 Commits
v5.0.0-dev
...
v5.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cbb4414e0 | ||
|
|
79fcfe50bc | ||
|
|
216d16456b | ||
|
|
822aaa99b0 | ||
|
|
2c683ab77d | ||
|
|
2ef5af6881 | ||
|
|
36f18be042 | ||
|
|
f093a3ab37 | ||
|
|
74cd42117b | ||
|
|
bb4a4e6102 | ||
|
|
24a3167f9b | ||
|
|
214e1c20ca | ||
|
|
33cab6ddad | ||
|
|
f4b2826bc7 | ||
|
|
ebaa9f2e56 | ||
|
|
812b75a034 | ||
|
|
3b02a5f5ec | ||
|
|
657d2eb1c5 | ||
|
|
25d87dd14d | ||
|
|
d2605e6e3d | ||
|
|
b21ebe0f55 | ||
|
|
2693fe8b7e | ||
|
|
45279a6520 | ||
|
|
22f9b1a7a1 | ||
|
|
8325a58e25 | ||
|
|
0acc508a8f | ||
|
|
2af0cd9d6f | ||
|
|
304858fcba | ||
|
|
ade027911c | ||
|
|
a97e647f7a | ||
|
|
008bed0233 | ||
|
|
bb4c04f3b9 | ||
|
|
62997e5972 | ||
|
|
a20e7bb40d | ||
|
|
2acfac4c85 | ||
|
|
a1a9d87a54 | ||
|
|
1ab71cc487 | ||
|
|
a8b19f5f3c | ||
|
|
4661a4a5f0 | ||
|
|
2c40abc808 | ||
|
|
10d7bcf4c0 | ||
|
|
5f8e4e6913 | ||
|
|
94fa9f124a | ||
|
|
3e70251f38 | ||
|
|
66ed6cfa94 | ||
|
|
d82b6e8d0d | ||
|
|
5a5f6867b9 | ||
|
|
5cd5fc09f5 | ||
|
|
17528d5ca2 | ||
|
|
e658a11947 | ||
|
|
4ac6f5c202 | ||
|
|
5726515707 | ||
|
|
f2ee7d1470 | ||
|
|
4140412e06 | ||
|
|
44ed142521 | ||
|
|
1ae0bbc150 | ||
|
|
8213611293 | ||
|
|
2697634a9f | ||
|
|
d7ba9e0c61 | ||
|
|
c99c4342b7 | ||
|
|
f410e27d1a | ||
|
|
e6d54412cf | ||
|
|
6238684819 | ||
|
|
ea07cd89de | ||
|
|
a7330f40d7 | ||
|
|
92c55ffe35 | ||
|
|
c21ba08fc7 | ||
|
|
ba3a716900 | ||
|
|
3133dc1543 | ||
|
|
fe2541f9e8 | ||
|
|
27af73f97f | ||
|
|
e9a442ca6e | ||
|
|
e86e282bb4 | ||
|
|
213506dff0 | ||
|
|
bc7aa7c9aa | ||
|
|
b234bf8890 | ||
|
|
36b4dcf7a8 | ||
|
|
0e843c20cc | ||
|
|
ecaf19c5fb | ||
|
|
f024aeef2c | ||
|
|
9d9985f117 | ||
|
|
a0f7349ef6 | ||
|
|
01407427d2 | ||
|
|
3dee03d9b6 | ||
|
|
737f6593be | ||
|
|
068e10dd40 | ||
|
|
c1ba5248b0 | ||
|
|
4af0f22ac0 | ||
|
|
25a67e1176 | ||
|
|
a8fcd501d6 | ||
|
|
573ee92889 | ||
|
|
2558d6fada | ||
|
|
2cf3f8d62b | ||
|
|
589d89b0d5 | ||
|
|
7cc7b77460 | ||
|
|
e8a9995bef | ||
|
|
74ff283e00 | ||
|
|
1ecb960b82 | ||
|
|
387d59f97b | ||
|
|
d993082f24 | ||
|
|
5eaa73d56d | ||
|
|
b9428fd3cd | ||
|
|
97d180b79d | ||
|
|
25bd915ea5 | ||
|
|
f8fdffdc44 | ||
|
|
d11aa6ea77 | ||
|
|
fc3c7997ea | ||
|
|
b3aecf8de4 | ||
|
|
f3581e62ef | ||
|
|
88e7fe508f | ||
|
|
98049ed02d | ||
|
|
194092ed67 | ||
|
|
e96157c890 | ||
|
|
a028c1ef42 | ||
|
|
ad2b5e687d | ||
|
|
8ba19f0be4 | ||
|
|
bccc68f6df | ||
|
|
026a05d03d | ||
|
|
2b4741c8ee | ||
|
|
7960ee06d4 | ||
|
|
f73f5af131 | ||
|
|
3123110aa4 | ||
|
|
154063638d | ||
|
|
a720b98365 | ||
|
|
d4a2937e0b | ||
|
|
b0ca7ffbb7 | ||
|
|
c42b738abe | ||
|
|
ab0d0fec53 | ||
|
|
8d96131962 | ||
|
|
95bbcd8b24 | ||
|
|
a21f5c2c23 | ||
|
|
94b7b1281c | ||
|
|
16dba586df | ||
|
|
72b761f959 | ||
|
|
943d81cbf9 | ||
|
|
2ecdc0eafa | ||
|
|
dccfd35c7a | ||
|
|
9e81a4f7a6 | ||
|
|
6f6cc73ce9 | ||
|
|
c248ae11bf | ||
|
|
742be03901 | ||
|
|
00009ef198 | ||
|
|
1cb617524d | ||
|
|
e2e348240b | ||
|
|
b54908492c | ||
|
|
33172862bd | ||
|
|
c5f2767efc | ||
|
|
66f5a81a5d | ||
|
|
9e90e11bfc | ||
|
|
ab372a89d6 | ||
|
|
ea612c3acb | ||
|
|
a1308645e5 | ||
|
|
c6506d515f | ||
|
|
794b705184 | ||
|
|
66264abe50 | ||
|
|
7d0f84ff72 | ||
|
|
31495ab02a | ||
|
|
853ad5c916 | ||
|
|
51fc525215 | ||
|
|
92d1c446d4 | ||
|
|
f2a0ff448b | ||
|
|
0ec096a980 | ||
|
|
44a5b83c1c | ||
|
|
46c5184d40 | ||
|
|
7c46306929 | ||
|
|
d7fe32d000 | ||
|
|
d7beb288c3 | ||
|
|
fb5da1b4d0 | ||
|
|
d89e254555 | ||
|
|
99e11317e1 | ||
|
|
3dc6c4452d | ||
|
|
3a627a878b | ||
|
|
6f9fed180d | ||
|
|
5ecd176f20 | ||
|
|
dff3837df6 | ||
|
|
e42eb31b73 | ||
|
|
721c117309 | ||
|
|
216715b2d5 | ||
|
|
412d4d5bcd | ||
|
|
2734d5754a | ||
|
|
c10f9d8c78 | ||
|
|
7140135cbe | ||
|
|
b5969945b4 | ||
|
|
7f5b3bc16c | ||
|
|
5cf7b6624b | ||
|
|
56225b337d | ||
|
|
79b6ad6f48 | ||
|
|
d0df53dd02 | ||
|
|
3b97bfd9b6 | ||
|
|
9b12623131 | ||
|
|
425370d63e | ||
|
|
1666c89222 | ||
|
|
a7dd4fc555 | ||
|
|
9f1760fbf2 | ||
|
|
d7fb040b77 | ||
|
|
2d7c1bcea2 | ||
|
|
a08c434f35 | ||
|
|
45d402bb9f | ||
|
|
4556544043 | ||
|
|
921d700761 | ||
|
|
9605783f41 | ||
|
|
f23329dc68 | ||
|
|
5f4ac6b2c0 | ||
|
|
f0c2961e95 | ||
|
|
2bdaa565b4 | ||
|
|
ab2bcde794 | ||
|
|
0b9674e14c | ||
|
|
07cbeb524e | ||
|
|
8ff75ea2ab | ||
|
|
6f5db94b2f | ||
|
|
2637957141 | ||
|
|
c1bb6f3987 | ||
|
|
d8df904951 | ||
|
|
a7ca7c922d | ||
|
|
f257992a5a | ||
|
|
62f9f6077d | ||
|
|
e4edb67f62 | ||
|
|
185ccf5ca6 | ||
|
|
bdd1805620 | ||
|
|
29f2722851 | ||
|
|
b38f8b08da | ||
|
|
6d02889f84 | ||
|
|
b864634f1d | ||
|
|
248662b95c | ||
|
|
0eb28bd89e | ||
|
|
e466bf9ba9 | ||
|
|
a14c4faefd | ||
|
|
b87a7f069e | ||
|
|
674a56e2f3 | ||
|
|
521d8cb4f8 | ||
|
|
bd1d85e927 | ||
|
|
4d62b928f8 | ||
|
|
014c9607d9 | ||
|
|
98ef24fc9e | ||
|
|
c04580377b | ||
|
|
a492b38368 | ||
|
|
518f21531e | ||
|
|
36fd40ee09 | ||
|
|
6b8c604762 | ||
|
|
c10382d1fb | ||
|
|
0e968ae59c | ||
|
|
1286a9e560 | ||
|
|
bf73712781 | ||
|
|
53922db113 | ||
|
|
799037d657 | ||
|
|
5cb7a3cc8e | ||
|
|
c14cbb258d | ||
|
|
8a860497f1 | ||
|
|
77d4d0007c | ||
|
|
005824eb24 | ||
|
|
259e04a6ce | ||
|
|
966bc14c67 | ||
|
|
f752b7e155 | ||
|
|
255b8b9ac3 | ||
|
|
15c4fb3c01 | ||
|
|
9fba0c08b2 | ||
|
|
f08dd0255d | ||
|
|
42d969bbeb | ||
|
|
fdc33e44dc | ||
|
|
b0aa889a0a | ||
|
|
4d7bd53231 | ||
|
|
c11cf665c9 | ||
|
|
a4b09d3a0c | ||
|
|
6e488b0f80 | ||
|
|
2cef723483 | ||
|
|
6164281a50 | ||
|
|
c74a014ab7 | ||
|
|
4bbf1dc8aa | ||
|
|
6e53621e01 | ||
|
|
2db7171e2a | ||
|
|
edfc094952 | ||
|
|
b3e1e2900b | ||
|
|
ba2bc206cc | ||
|
|
1e498d535a | ||
|
|
6ed969cd1b | ||
|
|
27bb4f4bca | ||
|
|
6ce2139272 | ||
|
|
856eff5127 | ||
|
|
13cf6613a6 | ||
|
|
52b141c4fa | ||
|
|
701844ca7c | ||
|
|
bcc572bd18 | ||
|
|
c5bb13e295 | ||
|
|
34c8257848 | ||
|
|
fcbf39c935 | ||
|
|
46c8150b2b | ||
|
|
af1a864daa | ||
|
|
3babd9399a | ||
|
|
e57564486f | ||
|
|
464a91ac29 | ||
|
|
4b58ce94ee | ||
|
|
16b0374eac | ||
|
|
67e63dccb4 | ||
|
|
2cc600cc52 | ||
|
|
27fa50a5de | ||
|
|
1e323f18f7 | ||
|
|
2685b2b77d | ||
|
|
d71b47a515 | ||
|
|
88cc720dd2 | ||
|
|
201433cc91 | ||
|
|
8582303491 | ||
|
|
655c3edadd | ||
|
|
a4323a4bd8 | ||
|
|
1b5a7d9546 | ||
|
|
f165cc4629 | ||
|
|
cb499ce42e | ||
|
|
db247d6fbd | ||
|
|
23701bf8e8 | ||
|
|
2e1f2e7eec | ||
|
|
fdb3ff54f1 | ||
|
|
d5fd56718d | ||
|
|
0698ac6dd5 | ||
|
|
825b59ab1e | ||
|
|
3086d1a5c8 | ||
|
|
138796862b | ||
|
|
1b236f1ae5 | ||
|
|
b6c2e25395 | ||
|
|
b7816986aa | ||
|
|
437c70a75a | ||
|
|
de85e0fbae | ||
|
|
8f7fda4919 | ||
|
|
470ccd333f | ||
|
|
2b6d067b0e | ||
|
|
0b928888a5 | ||
|
|
28edfd44cb | ||
|
|
3391c6de07 | ||
|
|
12b7951700 | ||
|
|
c527b59782 | ||
|
|
3bbbac66c7 | ||
|
|
c0980bf18a | ||
|
|
45eab19493 | ||
|
|
1e2a5a5bfd | ||
|
|
45edfa8014 | ||
|
|
899203c855 | ||
|
|
ef823b066b | ||
|
|
33dc2015df | ||
|
|
fcf5ea420b | ||
|
|
8d97327f8d | ||
|
|
cbc0e857fc | ||
|
|
01ce306405 | ||
|
|
a39a49e884 | ||
|
|
887a39dde9 | ||
|
|
abc7ebba3e | ||
|
|
73d593907e | ||
|
|
84a45fabdc | ||
|
|
0dc05e1138 | ||
|
|
dd11cacae4 | ||
|
|
ea17902aa6 | ||
|
|
b503259758 | ||
|
|
1e98741e16 | ||
|
|
50a820b0ae | ||
|
|
2abbb843f8 | ||
|
|
32179f31c2 | ||
|
|
0cb2c3621b |
2
.github/workflows/node.yml
vendored
2
.github/workflows/node.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: sudo apt-get install xvfb
|
||||
- name: Lint
|
||||
|
||||
207
CHANGELOG.md
207
CHANGELOG.md
@@ -4,6 +4,144 @@ All notable changes to this project will be documented in this file, following t
|
||||
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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
|
||||
|
||||
## [v5.4.1] - 2025-11-16
|
||||
- Fix ugly camera clipping in snapshot transitions
|
||||
- Add viewport button to toggle illumination mode
|
||||
- Fix bounding sphere computation for 3D text
|
||||
- Structure bounding sphere includes atom VDW radii / coarse sphere radii
|
||||
- Relax camera limits to allow focusing any selection with >1 atom
|
||||
- MolViewSpec
|
||||
- Fix `appendSnapshots` when loading MVSX
|
||||
- Fix all-selector color not applying on substructure
|
||||
- Fix primitives in root not being transformed with reference structure
|
||||
- Color themes do not prefer smoothing (improves performance in animations)
|
||||
- Allow canvas background interpolation
|
||||
- Fix `direct-volume` not drawn in illumination mode
|
||||
- Fix default trackball animated spin speed
|
||||
- Use `PluginCommands` to set canvas3d props in camera behavior
|
||||
- Volume improvements
|
||||
- Add `Volume.periodicity`
|
||||
- Wrap isosurfaces for periodic volumes
|
||||
- Fix dimensions for slices
|
||||
- Add support for Input Method Editor (IME) to text params input
|
||||
- Update `guessCifVariant` to detect density files not generated by the VolumeServer
|
||||
|
||||
## [v5.3.0] - 2025-11-05
|
||||
- Update loading message in MVS Stories Viewer
|
||||
- Add `Canvas3D.setAttribs`
|
||||
- Fix `normalizeWheel` "spin" calculation fallback
|
||||
- MolViewSpec
|
||||
- Add support for "topology" formats (TOP, PRMTOP, PSF)
|
||||
- Add support for additional "coordiates" formats (NCTRAJ, DCD, TRR)
|
||||
- Fix coarse structure selection
|
||||
- Fix missing default param values in `primitives_from_uri`
|
||||
|
||||
## [v5.2.0] - 2025-10-31
|
||||
- Handle transparency updates on ImagePass
|
||||
- Fix CIF parser edge case when the last token is escaped
|
||||
- MolViewSpec
|
||||
- Fix tooltips persisting across snapshots
|
||||
- Fix CIF annotations with no selector columns being ignored
|
||||
- Fix trackpad lock when camera up parallel to direction
|
||||
- Add clipping support for primitives
|
||||
- Support near camera distance
|
||||
|
||||
## [v5.1.2] - 2025-10-25
|
||||
- Fix createColorScaleByType when offsets are available
|
||||
- Get bond orders from non-standard CONECT records in PDB files
|
||||
- Remove outdated `gl_FrontFacing` workaround for buggy drivers
|
||||
- Fix clip objects for direct-volume rendering
|
||||
- Support "magic window" style AR (via WebXR)
|
||||
- Fix `PluginState.getStateTransitionFrameIndex`
|
||||
- Update `GlycamSaccharideNames` and `Monosaccharides` in `carbohydrates/constants.ts`
|
||||
- Support custom ref resolvers in `State`
|
||||
- Add full-screen mode support to layout manager
|
||||
- Add `show-toggle-fullscreen` URL param option to Viewer app
|
||||
- MolViewSpec
|
||||
- Support accessing Mol* State nodes by MVS-provided ref
|
||||
- Add support for DX map format
|
||||
- Better support for coarse structures in MVS:
|
||||
- Support for MVS annotations on coarse structures (color_from_*, tooltip_from_*)
|
||||
- Support for MVS labels on coarse structures (label, label_from_*)
|
||||
- (Other things already worked on coarse structures before: tooltip, color,component, primitives, component_from_*, primitives_from_*)
|
||||
- Tidy up MVS builder:
|
||||
- Add `sphere` and `angle` methods
|
||||
- [Breaking] Rename builder method primitives_from_uri -> primitivesFromUri
|
||||
|
||||
## [v5.0.0] - 2025-09-28
|
||||
- [Breaking] Renamed some color schemes ('inferno' -> 'inferno-no-black', 'magma' -> 'magma-no-black', 'turbo' -> 'turbo-no-black', 'rainbow' -> 'simple-rainbow')
|
||||
- [Breaking] `Box3D.nearestIntersectionWithRay` -> `Ray3D.intersectBox3D`
|
||||
- [Breaking] `Plane3D.distanceToSpher3D` -> `distanceToSphere3D` (fix spelling)
|
||||
@@ -12,6 +150,8 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- [Breaking] Add `Volume.instances` support and a `VolumeInstances` transform to dynamically assign it
|
||||
- This change is breaking because all volume objects require the `instances` field now.
|
||||
- [Breaking] `Canvas3D.identify` now expects `Vec2` or `Ray3D`
|
||||
- [Breaking] `TrackballControlsParams.animate.spin.speed` now means "Number of rotations per second" instead of "radians per second"
|
||||
- [Breaking] `PluginStateSnapshotManager.play` now accepts an options object instead of a single boolean value
|
||||
- Update production build to use `esbuild`
|
||||
- Emit explicit paths in `import`s in `lib/`
|
||||
- Fix outlines on opaque elements using illumination mode
|
||||
@@ -19,24 +159,40 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- MolViewSpec extension:
|
||||
- Generic color schemes (`palette` parameter for color_from_* nodes)
|
||||
- Annotation field remapping (`field_remapping` parameter for color_from_* nodes)
|
||||
- Representation node: support custom property `molstar_reprepresentation_params`,
|
||||
- Canvas node: support custom properties `molstar_enable_outline`, `molstar_enable_shadow`, `molstar_enable_ssao`
|
||||
- `representation` node: support custom property `molstar_representation_params`
|
||||
- Add `backbone` and `line` representation types
|
||||
- `primitives` node: support custom property `molstar_mesh/label/line_params`
|
||||
- `canvas` node: support custom property `molstar_postprocessing` with the ability to customize outline, depth of field, bloom, shadow, occlusion (SSAO), fog, and background
|
||||
- `clip` node support for structure and volume representations
|
||||
- `grid_slice` representation support for volumes
|
||||
- Support tethers and background for primitive labels
|
||||
- Support `snapshot_key` parameter on primitives that enables transition between states via clicking on 3D objects
|
||||
- Inline selectors and MVS annotations support `instance_id`
|
||||
- Support `matrix` on transform params
|
||||
- Support `surface_type` (`molecular` / `gaussian`) on for `surface` representation nodes
|
||||
- Add `instance` node type
|
||||
- Add `transform.rotation_center` property that enables rotating an object around its centroid or a specific point
|
||||
- Support transforming and instancing of structures, components, and volumes
|
||||
- Use params hash for node version for more performant tree diffs
|
||||
- Add `Snapshot.animation` support that enables animating almost every property in a given tree
|
||||
- Add `createMVSX` helper function
|
||||
- Support Mol* trackball animation via `animation.custom.molstar_trackball`
|
||||
- MVSX - use Murmur hash instead of FNV in archive URI
|
||||
- Support additional file formats (pdbqt, gro, xyz, mol, sdf, mol2, xtc, lammpstrj)
|
||||
- Support loading trajectory coordinates from separate nodes
|
||||
- Trigger markdown commands from primitives using `molstar_markdown_commands` custom extensions
|
||||
- Support `molstar_on_load_markdown_commands` custom state on the `root` node
|
||||
- Print tree validation errors to plugin log
|
||||
- Added new color schemes, synchronized with D3.js ('inferno', 'magma', 'turbo', 'rainbow', 'sinebow', 'warm', 'cool', 'cubehelix-default', 'category-10', 'observable-10', 'tableau-10')
|
||||
- Snapshot Markdown improvements
|
||||
- Add `MarkdownExtensionManager` (`PluginContext.managers.markdownExtensions`)
|
||||
- Support custom markdown commands to control the plugin via the `[link](!command)` pattern
|
||||
- Support rendering custom elements via the `` pattern
|
||||
- Support tables
|
||||
- Support loading images from MVSX files
|
||||
- Support loading images and audio from MVSX files
|
||||
- Indicate external links with ⤴
|
||||
- Audio support
|
||||
- Add `PluginState.Snapshot.onLoadMarkdownCommands`
|
||||
- Avoid calculating rings for coarse-grained structures
|
||||
- Fix isosurface compute shader normals when transformation matrix is applied to volume
|
||||
- Symmetry operator naming for spacegroup symmetry - parenthesize multi-character indices (1_111-1 -> 1_(11)1(-1))
|
||||
@@ -52,7 +208,10 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Add `StructureInstances` transform
|
||||
- `mvs-stories` app
|
||||
- Add `story-id` URL arg support
|
||||
- Add `story-session-url` URL arg support
|
||||
- Add "Download MVS State" link
|
||||
- Add "Open in Mol*" link
|
||||
- Add "Edit in MolViewStories" link for story states
|
||||
- Add ray-based picking
|
||||
- Render narrow view of scene scene from ray origin & direction to a few pixel sized viewport
|
||||
- Cast ray on every input as opposed to the standard "whole screen" picking
|
||||
@@ -62,6 +221,48 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Add async, non-blocking picking (only WebGL2)
|
||||
- Refactor `Canvas3dInteractionHelper` internals to use async picking for move events
|
||||
- Add `enable` param for post-processing effects. If false, no effects are applied.
|
||||
- Dot volume representation improvements
|
||||
- Add positional perturbation to avoid camera artifacts
|
||||
- Fix handling of negative isoValues by considering only volume cells with values lower than isoValue (#1559)
|
||||
- Fix volume-value size theme
|
||||
- Change the parsing of residue names in PDB files from 3-letter to 4-letter.
|
||||
- Support versioning transform using a hash function in `mol-state`
|
||||
- Support for "state snapshot transitions"
|
||||
- Add `PluginState.Snapshot.transition` that enables associating a state snapshot with a list states that can be animated
|
||||
- Add `AnimateStateSnapshotTransition` animation
|
||||
- Update the snapshots UI to support this feature
|
||||
- Use "proper time" in the animation loop to prevent animation skips during blocking operations (e.g., shader complication)
|
||||
- Add `Hsl` and (normalized) `Rgb` color spaces
|
||||
- Add `Color.interpolateHsl`
|
||||
- Add `rotationCenter` property to `TransformParam`
|
||||
- Add Monolayer transparency (exploiting dpoit).
|
||||
- Add plugin config item ShowReset (shows/hides "Reset Zoom" button)
|
||||
- Fix transform params not being normalized when used together with param hash version
|
||||
- Replace `immer` with `mutative`
|
||||
- Fix renderer transparency check
|
||||
- Add outlines improvements
|
||||
- VolumeServer & "VolumeCIF": default to P 1 spacegroup
|
||||
- Fix `ColorScale` for continuous case without offsets (broke in v4.13.0)
|
||||
- Experimental: support for custom color themes in Sequence Panel
|
||||
- Switch files.rcsb.org validation report URL to new endpoint /validation/view
|
||||
- Improve picking of objects with too many groups, pick whole instance/object
|
||||
- Add WebXR support
|
||||
- Requires immersive AR/VR headset
|
||||
- Supplements non-XR: enter/exit XR anytime and see (mostly) the same scene
|
||||
- Add `Canvas3D.xr` for managing XR sessions
|
||||
- Add `PointerHelper` for rendering XR input devices
|
||||
- Add XR button to Viewer and Mesoscale Explorer
|
||||
- Add XR button to render-structure in tests/browser
|
||||
- Fix illumination denoising with transparency on transparent background
|
||||
- Change the `to_mmCIF` function parameter from `structure` to `structures` to support either a single structure or an array of structures
|
||||
- ModelServer and VolumeServer: add configurable robots.txt
|
||||
- Adaptive parallel shader compilation
|
||||
- Split shader compilation into linking and finalizing
|
||||
- Start linking as early as possible and wait with finalizing to avoid blocking main thread
|
||||
- Use of `KHR_parallel_shader_compile` extension when available to check status
|
||||
- Add `ShaderManager` to compile shaders based on `Canvas3D` params and `Scene` content
|
||||
- Draw `Scene` only when shaders are ready
|
||||
- Fix incorrect animation loop handling in the screenshot code
|
||||
|
||||
## [v4.18.0] - 2025-06-08
|
||||
- MolViewSpec extension:
|
||||
|
||||
1
breaking-v6-changes.md
Normal file
1
breaking-v6-changes.md
Normal file
@@ -0,0 +1 @@
|
||||
- Remove `checkeredCanvasBackground` from `PluginContext` and `PluginContainer`
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -247,7 +274,7 @@ async function init() {
|
||||
const canvas = <HTMLCanvasElement> document.getElementById('molstar-canvas');
|
||||
const parent = <HTMLDivElement> document.getElementById('molstar-parent');
|
||||
|
||||
if (!(await plugin.initViewer(canvas, parent))) {
|
||||
if (!(await plugin.initViewerAsync(canvas, parent))) {
|
||||
console.error('Failed to init Mol*');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,25 @@ The main use case of this is enriching [MolViewSpec](`https://molstar.org/mol-vi
|
||||
|
||||
Extends Markdown Hyperlink syntax to support expressions of the form `[title](!c1=v1&c2=v2&...)` into an executable command. The command can be executed either on click, mouse enter, or mouse leave.
|
||||
|
||||
Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `encodeURIComponent`, in Python `urllib.parse.quote_plus/urlencode`).
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
- `center-camera` - Centers the camera
|
||||
- `apply-snapshot=key` - Loads snapshots with the provided key
|
||||
- `next-snapshot[=-1|1]` - Loads next/previous snapshot, the direction is optional and default to `1`
|
||||
- `play-snapshots` - Starts playback of state snapshots
|
||||
- `play-transition` - Plays an animation associated with the given snapshot
|
||||
- `stop-animation` - Stops currently playing animation
|
||||
- `focus-refs=ref1,ref2,...` - On click, focuses nodes with the provided refs
|
||||
- `highlight-refs=ref1,ref2,...` - On mouse over, highlights the provided refs
|
||||
- `query=...&lang=...&action=highlight,focus&focus-radius=...`
|
||||
- `query` is an expression (e.g., `resn HEM` when using PyMol syntax)
|
||||
- (optional) `lang` is one of `mol-script` (default), `pymol`, `vmd`, `jmol`
|
||||
- (optional) `action` is an array of `highlight` (default), `focus` (multiple actions can be specified)
|
||||
- (optional) `focus-radius` is extra distance applied when focusing the selection (default is `3`)
|
||||
- Example: `[HEM](!query=resn%20HEM%26lang=pymol&action=highlight,focus)` highlights or focuses the HEM residue (the query must be URL encoded because it contains spaces and possibly other special characters)
|
||||
- `play-audio=src`, `toggle-audio[=src]`, `stop-audio`, `pause-audio`, `dispose-audio` - Audio playback support
|
||||
|
||||
## Custom Content
|
||||
|
||||
@@ -28,11 +41,11 @@ Extends Markdown Image syntax to support expressions of the form `
|
||||
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
|
||||
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
|
||||
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
|
||||
- `color-palette-discrete` - Renders discrete color list instead of interpolating
|
||||
- `color-palette-name=name` - Renders a gradient with the provided named color palette (see `mol-util/color/lists.ts` for supported color schemes)
|
||||
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
|
||||
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
|
||||
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
|
||||
- `color-palette-discrete` - Renders discrete color list instead of interpolating
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
@@ -1,59 +1,44 @@
|
||||
# Selections
|
||||
|
||||
|
||||
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the following method you can select structural data.
|
||||
## Basic Concepts
|
||||
|
||||
### Selecting directly from the `hierarchy` manager
|
||||
### Location
|
||||
|
||||
One can select a subcomponent's data directly from the plugin manager.
|
||||
The selection model in Mol\* is based on a generic concept called *location*. A location is a pointer to a selectable element within a scene. For example:
|
||||
|
||||
```typescript
|
||||
import { Structure } from '../mol-model/structure';
|
||||
- A structure element location (an atom or a coarse element) is an object composed of `{ structure: Structure, unit: Unit, element: UnitIndex }` (you can think of a `Unit` as a generalized chain)
|
||||
- A bond location is very similar to structure element, requiring pointers to two units and elements
|
||||
- A "shape" (generally a mesh) location consists of pointer to the parent shape and a group of triangles
|
||||
|
||||
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
|
||||
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
|
||||
### Loci
|
||||
|
||||
plugin.managers.camera.focusLoci(ligandLoci);
|
||||
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
|
||||
```
|
||||
Structures and other renderable elements generally consist of many locations and simply using a list of locations would be
|
||||
prohibitively expensive (e.g., large selections in structures with hundreds of thousands of atoms).
|
||||
|
||||
## Selection callbacks
|
||||
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
|
||||
This is why Mol\* introduces
|
||||
the concept of `Loci` — a compressed representation of multiple locations. Instead of having a list of structure element locations (`{ structure: Structure, unit: Unit, element: UnitIndex }[]`), the representation becomes (simplified) `{ structure: Structure, unit: Unit, elements: OrderedSet<UnitIndex> }`. The ordered set can be further compressed for continuous ranges, keeping only the index of the 1st and last element.
|
||||
|
||||
Here's an example of passing in a React "set" function to update selected residue positions.
|
||||
```typescript
|
||||
import {
|
||||
Structure,
|
||||
StructureProperties,
|
||||
} from "molstar/lib/mol-model/structure"
|
||||
// setSelected is assumed to be a "set" function returned by useState
|
||||
// (selected: any[]) => void
|
||||
plugin.behaviors.interaction.click.subscribe(
|
||||
(event: InteractivityManager.ClickEvent) => {
|
||||
const selections = Array.from(
|
||||
plugin.managers.structure.selection.entries.values()
|
||||
);
|
||||
// This bit can be customized to record any piece information you want
|
||||
const localSelected: any[] = [];
|
||||
for (const { structure } of selections) {
|
||||
if (!structure) continue;
|
||||
Structure.eachAtomicHierarchyElement(structure, {
|
||||
residue: (loc) => {
|
||||
const position = StructureProperties.residue.label_seq_id(loc);
|
||||
localSelected.push({ position });
|
||||
},
|
||||
});
|
||||
}
|
||||
setSelected(localSelected);
|
||||
}
|
||||
)
|
||||
```
|
||||
### Bundle
|
||||
|
||||
### `Molscript` language
|
||||
Locations and loci point to the raw JavaScript data structures representing the underlying molecules, making them not serializable in JSON. A *bundle* is a serializable version of the loci.
|
||||
|
||||
Molscript is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
|
||||
### Structure Queries
|
||||
|
||||
### Querying a structure for a specific chain and residue range (select residues with 12<res_id<200 of chain with auth_asym_id==A) :
|
||||
Defining selections directly using the loci would be very cumbersome. For this reason, Mol\* includes the [MolQl query language](https://molql.org) to help define selections.
|
||||
|
||||
|
||||
## Selection Methods
|
||||
|
||||
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the methods you can use to create selections.
|
||||
|
||||
### MolQL (`mol-script`) language
|
||||
|
||||
[MolQL](https://molql.org) (`mol-script`) is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
|
||||
|
||||
**Example:** Querying a structure for a specific chain and residue range
|
||||
|
||||
Select residues with `12<res_id<200 of chain with auth_asym_id=A`
|
||||
|
||||
```typescript
|
||||
import { compileIdListSelection } from 'molstar/lib/mol-script/util/id-list'
|
||||
@@ -62,12 +47,12 @@ const query = compileIdListSelection('A 12-200', 'auth');
|
||||
window.molstar?.managers.structure.selection.fromCompiledQuery('add',query);
|
||||
```
|
||||
|
||||
## Selection Queries
|
||||
### Selection Queries
|
||||
|
||||
Another way to create a selection is via a `SelectionQuery` object. This is a more programmatic way to create a selection. The following example shows how to select a chain and a residue range using a `SelectionQuery` object.
|
||||
This relies on the concept of `Expression` which is basically a intermediate representation between a Molscript statement and a selection query.
|
||||
|
||||
### Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object:
|
||||
**Example:** Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object
|
||||
|
||||
```typescript
|
||||
import { MolScriptBuilder as MS, MolScriptBuilder } from 'molstar/lib/mol-script/language/builder';
|
||||
@@ -107,7 +92,7 @@ var sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
||||
let loci = StructureSelection.toLociWithSourceUnits(sel);
|
||||
```
|
||||
|
||||
## Query Functions
|
||||
### Query Functions
|
||||
|
||||
Instead of building expressions, query functions can be created directly, e.g.:
|
||||
|
||||
@@ -125,7 +110,7 @@ const selection = query(new QueryContext(structure));
|
||||
// ...
|
||||
```
|
||||
|
||||
## Selection Schema
|
||||
### Selection Schema
|
||||
|
||||
For simple selections, the `StructureElement.Schema` can be used to reference elements within a protein structure using mmCIF `atom_site` field names, e.g.:
|
||||
|
||||
@@ -143,6 +128,63 @@ const loci = StructureElement.Loci.fromSchema(structure, residues);
|
||||
|
||||
Usually, a code editor such as VS Code will auto-suggest all the available field names.
|
||||
|
||||
### Using the `hierarchy` manager
|
||||
|
||||
It is possible to select a subcomponent's data directly from the plugin manager.
|
||||
|
||||
```typescript
|
||||
import { Structure } from '../mol-model/structure';
|
||||
|
||||
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
|
||||
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
|
||||
|
||||
plugin.managers.camera.focusLoci(ligandLoci);
|
||||
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
|
||||
```
|
||||
|
||||
## Selection Events
|
||||
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
|
||||
|
||||
Here's an example of passing in a React "set" function to update selected residue positions.
|
||||
```typescript
|
||||
import {
|
||||
Structure,
|
||||
StructureProperties,
|
||||
} from "molstar/lib/mol-model/structure"
|
||||
// setSelected is assumed to be a "set" function returned by useState
|
||||
// (selected: any[]) => void
|
||||
plugin.behaviors.interaction.click.subscribe(
|
||||
(event: InteractivityManager.ClickEvent) => {
|
||||
const selections = Array.from(
|
||||
plugin.managers.structure.selection.entries.values()
|
||||
);
|
||||
// This bit can be customized to record any piece information you want
|
||||
const localSelected: any[] = [];
|
||||
for (const { structure } of selections) {
|
||||
if (!structure) continue;
|
||||
Structure.eachAtomicHierarchyElement(structure, {
|
||||
residue: (loc) => {
|
||||
const position = StructureProperties.residue.label_seq_id(loc);
|
||||
localSelected.push({ position });
|
||||
},
|
||||
});
|
||||
}
|
||||
setSelected(localSelected);
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
|
||||
### `Viewer` app
|
||||
|
||||
The `Viewer` app provides the `structureInteractivity` function which allows easy selection/highlighting of the loaded structure. For example:
|
||||
|
||||
```ts
|
||||
viewer.structureInteractivity({
|
||||
elements: { beg_auth_seq_id: 10, end_auth_seq_id: 50 },
|
||||
action: 'select',
|
||||
});
|
||||
```
|
||||
152
docs/docs/plugin/superposition.md
Normal file
152
docs/docs/plugin/superposition.md
Normal 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).
|
||||
@@ -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'
|
||||
|
||||
@@ -39,6 +39,7 @@ export default defineConfig([{
|
||||
"comma-spacing": "off",
|
||||
"space-infix-ops": "off",
|
||||
"comma-dangle": "off",
|
||||
quotes: ["warn", "single", { "allowTemplateLiterals": true, "avoidEscape": true }],
|
||||
eqeqeq: ["error", "smart"],
|
||||
"import/order": "off",
|
||||
"no-eval": "warn",
|
||||
|
||||
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
Binary file not shown.
BIN
examples/trajectory/protein.dcd
Normal file
BIN
examples/trajectory/protein.dcd
Normal file
Binary file not shown.
BIN
examples/trajectory/protein.nc
Normal file
BIN
examples/trajectory/protein.nc
Normal file
Binary file not shown.
264
examples/trajectory/protein.parm7
Normal file
264
examples/trajectory/protein.parm7
Normal file
@@ -0,0 +1,264 @@
|
||||
%VERSION VERSION_STAMP = V0001.000 DATE = 11/04/25 11:55:47
|
||||
%FLAG TITLE
|
||||
%FORMAT(20a4)
|
||||
alanine-dipeptide.solvated.pdb
|
||||
%FLAG POINTERS
|
||||
%FORMAT(10I8)
|
||||
22 7 12 9 25 11 39 19 0 0
|
||||
99 3 9 11 19 7 11 20 0 0
|
||||
0 0 0 0 0 0 0 1 10 0
|
||||
0 1
|
||||
%FLAG ATOM_NAME
|
||||
%FORMAT(20a4)
|
||||
H1 CH3 H2 H3 C O N H CA HA CB HB1 HB2 HB3 C O N H C H1
|
||||
H2 H3
|
||||
%FLAG ATOMIC_NUMBER
|
||||
%FORMAT(10I8)
|
||||
1 6 1 1 6 8 7 1 6 1
|
||||
6 1 1 1 6 8 7 1 6 1
|
||||
1 1
|
||||
%FLAG RESIDUE_LABEL
|
||||
%FORMAT(20a4)
|
||||
ACE ALA NME
|
||||
%FLAG RESIDUE_POINTER
|
||||
%FORMAT(10I8)
|
||||
1 7 17
|
||||
%FLAG RESIDUE_NUMBER
|
||||
%FORMAT(20I4)
|
||||
1 2 3
|
||||
%FLAG RESIDUE_ICODE
|
||||
%FORMAT(20a4)
|
||||
|
||||
%FLAG RESIDUE_CHAINID
|
||||
%FORMAT(20a4)
|
||||
B B B
|
||||
%FLAG SOLVENT_POINTERS
|
||||
%FORMAT(3I8)
|
||||
0 1 0
|
||||
%FLAG ATOMS_PER_MOLECULE
|
||||
%FORMAT(10I8)
|
||||
22
|
||||
%FLAG MASS
|
||||
%FORMAT(5E16.8)
|
||||
3.02400000E+00 5.96200000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
|
||||
1.60000000E+01 1.19940000E+01 3.02400000E+00 9.99400000E+00 3.02400000E+00
|
||||
5.96200000E+00 3.02400000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
|
||||
1.60000000E+01 1.19940000E+01 3.02400000E+00 5.96200000E+00 3.02400000E+00
|
||||
3.02400000E+00 3.02400000E+00
|
||||
%FLAG CHARGE
|
||||
%FORMAT(5E16.8)
|
||||
2.04636429E+00 -6.67300626E+00 2.04636429E+00 2.04636429E+00 1.08823576E+01
|
||||
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 6.14091510E-01 1.49969529E+00
|
||||
-3.32556975E+00 1.09880469E+00 1.09880469E+00 1.09880469E+00 1.08841798E+01
|
||||
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 -2.71512270E+00 1.77849648E+00
|
||||
1.77849648E+00 1.77849648E+00
|
||||
%FLAG AMBER_ATOM_TYPE
|
||||
%FORMAT(20a4)
|
||||
a0 a1 a0 a0 a2 a3 a4 a5 a1 a6 a1 a0 a0 a0 a2 a3 a4 a5 a1 a6
|
||||
a6 a6
|
||||
%FLAG ATOM_TYPE_INDEX
|
||||
%FORMAT(10I8)
|
||||
1 2 1 1 3 4 5 6 2 7
|
||||
2 1 1 1 3 4 5 6 2 7
|
||||
7 7
|
||||
%FLAG NONBONDED_PARM_INDEX
|
||||
%FORMAT(10I8)
|
||||
1 2 4 7 11 16 22 2 3 5
|
||||
8 12 17 23 4 5 6 9 13 18
|
||||
24 7 8 9 10 14 19 25 11 12
|
||||
13 14 15 20 26 16 17 18 19 20
|
||||
21 27 22 23 24 25 26 27 28
|
||||
%FLAG LENNARD_JONES_ACOEF
|
||||
%FORMAT(5E16.8)
|
||||
7.51607703E+03 9.71708117E+04 1.04308023E+06 8.61541883E+04 9.24822269E+05
|
||||
8.19971662E+05 5.44261042E+04 6.47841732E+05 5.74393458E+05 3.79876399E+05
|
||||
8.96776989E+04 9.95480466E+05 8.82619071E+05 6.06829343E+05 9.44293233E+05
|
||||
1.07193645E+02 2.56678134E+03 2.27577560E+03 1.02595236E+03 2.12601181E+03
|
||||
1.39982777E-01 4.98586847E+03 6.78771368E+04 6.01816484E+04 3.69471530E+04
|
||||
6.20665998E+04 5.94667299E+01 3.25969625E+03
|
||||
%FLAG LENNARD_JONES_BCOEF
|
||||
%FORMAT(5E16.8)
|
||||
2.17257828E+01 1.26919150E+02 6.75612247E+02 1.12529845E+02 5.99015525E+02
|
||||
5.31102864E+02 1.11805549E+02 6.26720080E+02 5.55666449E+02 5.64885984E+02
|
||||
1.36131731E+02 7.36907417E+02 6.53361429E+02 6.77220874E+02 8.01323529E+02
|
||||
2.59456373E+00 2.06278363E+01 1.82891803E+01 1.53505284E+01 2.09604198E+01
|
||||
9.37598976E-02 1.76949863E+01 1.06076943E+02 9.40505981E+01 9.21192137E+01
|
||||
1.13252062E+02 1.93248820E+00 1.43076527E+01
|
||||
%FLAG NUMBER_EXCLUDED_ATOMS
|
||||
%FORMAT(10I8)
|
||||
6 7 4 3 7 3 10 4 10 7
|
||||
6 3 2 1 7 3 5 4 3 2
|
||||
1 1
|
||||
%FLAG EXCLUDED_ATOMS_LIST
|
||||
%FORMAT(10I8)
|
||||
2 3 4 5 6 7 3 4 5 6
|
||||
7 8 9 4 5 6 7 5 6 7
|
||||
6 7 8 9 10 11 15 7 8 9
|
||||
8 9 10 11 12 13 14 15 16 17
|
||||
9 10 11 15 10 11 12 13 14 15
|
||||
16 17 18 19 11 12 13 14 15 16
|
||||
17 12 13 14 15 16 17 13 14 15
|
||||
14 15 15 16 17 18 19 20 21 22
|
||||
17 18 19 18 19 20 21 22 19 20
|
||||
21 22 20 21 22 21 22 22 0
|
||||
%FLAG BOND_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
3.40000000E+02 4.34000000E+02 3.17000000E+02 5.70000000E+02 4.90000000E+02
|
||||
3.37000000E+02 3.10000000E+02
|
||||
%FLAG BOND_EQUIL_VALUE
|
||||
%FORMAT(5E16.8)
|
||||
1.09000000E+00 1.01000000E+00 1.52200000E+00 1.22900000E+00 1.33500000E+00
|
||||
1.44900000E+00 1.52600000E+00
|
||||
%FLAG BONDS_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 1 3 6 1 3 9 1 18
|
||||
21 2 24 27 1 30 33 1 30 36
|
||||
1 30 39 1 48 51 2 54 57 1
|
||||
54 60 1 54 63 1
|
||||
%FLAG BONDS_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 3 12 15 4 12 18 5 18
|
||||
24 6 24 42 3 24 30 7 42 48
|
||||
5 42 45 4 48 54 6
|
||||
%FLAG ANGLE_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
3.50000000E+01 5.00000000E+01 5.00000000E+01 5.00000000E+01 8.00000000E+01
|
||||
7.00000000E+01 5.00000000E+01 8.00000000E+01 8.00000000E+01 6.30000000E+01
|
||||
6.30000000E+01
|
||||
%FLAG ANGLE_EQUIL_VALUE
|
||||
%FORMAT(5E16.8)
|
||||
1.91113553E+00 1.91113553E+00 2.09439510E+00 2.06018665E+00 2.10137642E+00
|
||||
2.03505391E+00 2.12755636E+00 2.14500965E+00 1.91462619E+00 1.92160751E+00
|
||||
1.93906080E+00
|
||||
%FLAG ANGLES_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 6 1 0 3 9 1 0 3
|
||||
12 2 6 3 9 1 6 3 12 2
|
||||
9 3 12 2 12 18 21 3 18 24
|
||||
27 2 21 18 24 4 24 30 33 2
|
||||
24 30 36 2 24 30 39 2 27 24
|
||||
30 2 27 24 42 2 33 30 36 1
|
||||
33 30 39 1 36 30 39 1 42 48
|
||||
51 3 48 54 57 2 48 54 60 2
|
||||
48 54 63 2 51 48 54 4 57 54
|
||||
60 1 57 54 63 1 60 54 63 1
|
||||
%FLAG ANGLES_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 15 5 3 12 18 6 12 18
|
||||
24 7 15 12 18 8 18 24 30 9
|
||||
18 24 42 10 24 42 45 5 24 42
|
||||
48 6 30 24 42 11 42 48 54 7
|
||||
45 42 48 8
|
||||
%FLAG DIHEDRAL_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
8.00000000E-01 8.00000000E-02 2.50000000E+00 2.50000000E+00 2.00000000E+00
|
||||
1.55555556E-01 1.10000000E+00 0.00000000E+00 0.00000000E+00 8.00000000E-01
|
||||
1.80000000E+00 4.20000000E-01 2.70000000E-01 5.50000000E-01 1.58000000E+00
|
||||
4.50000000E-01 4.00000000E-01 2.00000000E-01 2.00000000E-01 1.05000000E+01
|
||||
%FLAG DIHEDRAL_PERIODICITY
|
||||
%FORMAT(5E16.8)
|
||||
1.00000000E+00 3.00000000E+00 2.00000000E+00 2.00000000E+00 1.00000000E+00
|
||||
3.00000000E+00 2.00000000E+00 1.00000000E+00 1.00000000E+00 3.00000000E+00
|
||||
2.00000000E+00 3.00000000E+00 2.00000000E+00 3.00000000E+00 2.00000000E+00
|
||||
1.00000000E+00 3.00000000E+00 2.00000000E+00 1.00000000E+00 2.00000000E+00
|
||||
%FLAG DIHEDRAL_PHASE
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 3.14159265E+00 3.14159265E+00 3.14159265E+00 0.00000000E+00
|
||||
0.00000000E+00 3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00 3.14159265E+00
|
||||
3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00
|
||||
%FLAG SCEE_SCALE_FACTOR
|
||||
%FORMAT(5E16.8)
|
||||
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 0.00000000E+00
|
||||
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 1.20000000E+00
|
||||
0.00000000E+00 1.20000000E+00 0.00000000E+00 1.20000000E+00 0.00000000E+00
|
||||
0.00000000E+00 1.20000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
%FLAG SCNB_SCALE_FACTOR
|
||||
%FORMAT(5E16.8)
|
||||
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 0.00000000E+00
|
||||
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 2.00000000E+00
|
||||
0.00000000E+00 2.00000000E+00 0.00000000E+00 2.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 2.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
%FLAG DIHEDRALS_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 12 15 1 0 3 -12 15 2
|
||||
3 12 18 21 3 6 3 12 15 1
|
||||
6 3 -12 15 2 9 3 12 15 1
|
||||
9 3 -12 15 2 15 12 18 21 4
|
||||
15 12 -18 21 5 18 24 30 33 6
|
||||
18 24 30 36 6 18 24 30 39 6
|
||||
24 42 48 51 3 27 24 30 33 6
|
||||
27 24 30 36 6 27 24 30 39 6
|
||||
27 24 42 45 1 27 24 -42 45 2
|
||||
42 24 30 33 6 42 24 30 36 6
|
||||
42 24 30 39 6 45 42 48 51 4
|
||||
45 42 -48 51 5 21 18 -24 -12 7
|
||||
51 48 -54 -42 7 51 48 54 60 8
|
||||
21 18 24 30 8 42 48 54 57 8
|
||||
6 3 12 18 9 42 48 54 63 8
|
||||
51 48 54 57 8 21 18 24 42 8
|
||||
0 3 12 18 9 42 48 54 60 8
|
||||
27 24 42 48 8 21 18 24 27 8
|
||||
51 48 54 63 8 9 3 12 18 9
|
||||
12 18 24 27 8
|
||||
%FLAG DIHEDRALS_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 18 24 3 12 18 24 30 10
|
||||
12 18 -24 30 11 12 18 -24 30 5
|
||||
12 18 24 42 12 12 18 -24 42 13
|
||||
15 12 18 24 3 18 24 42 48 14
|
||||
18 24 -42 48 15 18 24 -42 48 16
|
||||
24 42 48 54 3 30 24 42 48 17
|
||||
30 24 -42 48 18 30 24 -42 48 19
|
||||
45 42 48 54 3 15 12 -18 -3 20
|
||||
45 42 -48 -24 20 18 24 42 45 8
|
||||
30 24 42 45 8
|
||||
%FLAG SOLTY
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBOND_ACOEF
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBOND_BCOEF
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBCUT
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG TREE_CHAIN_CLASSIFICATION
|
||||
%FORMAT(20a4)
|
||||
BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA
|
||||
BLA BLA
|
||||
%FLAG JOIN_ARRAY
|
||||
%FORMAT(10I8)
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0
|
||||
%FLAG IROTAT
|
||||
%FORMAT(10I8)
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0
|
||||
%FLAG BOX_DIMENSIONS
|
||||
%FORMAT(5E16.8)
|
||||
9.00000000E+01 3.00000000E+01 3.00000000E+01 3.00000000E+01
|
||||
%FLAG RADIUS_SET
|
||||
%FORMAT(1a80)
|
||||
0
|
||||
%FLAG RADII
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00
|
||||
%FLAG SCREEN
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00
|
||||
%FLAG IPOL
|
||||
%FORMAT(1I8)
|
||||
0
|
||||
26
examples/trajectory/protein.pdb
Normal file
26
examples/trajectory/protein.pdb
Normal file
@@ -0,0 +1,26 @@
|
||||
CRYST1 30.000 30.000 30.000 90.00 90.00 90.00 P 1 1
|
||||
ATOM 1 H1 ACE A 1 2.000 1.000 -0.000 0.00 0.00 H
|
||||
ATOM 2 CH3 ACE A 1 2.000 2.090 0.000 0.00 0.00 C
|
||||
ATOM 3 H2 ACE A 1 1.486 2.454 0.890 0.00 0.00 H
|
||||
ATOM 4 H3 ACE A 1 1.486 2.454 -0.890 0.00 0.00 H
|
||||
ATOM 5 C ACE A 1 3.427 2.641 -0.000 0.00 0.00 C
|
||||
ATOM 6 O ACE A 1 4.391 1.877 -0.000 0.00 0.00 O
|
||||
ATOM 7 N ALA A 2 3.555 3.970 -0.000 0.00 0.00 N
|
||||
ATOM 8 H ALA A 2 2.733 4.556 -0.000 0.00 0.00 H
|
||||
ATOM 9 CA ALA A 2 4.853 4.614 -0.000 0.00 0.00 C
|
||||
ATOM 10 HA ALA A 2 5.408 4.316 0.890 0.00 0.00 H
|
||||
ATOM 11 CB ALA A 2 5.661 4.221 -1.232 0.00 0.00 C
|
||||
ATOM 12 HB1 ALA A 2 5.123 4.521 -2.131 0.00 0.00 H
|
||||
ATOM 13 HB2 ALA A 2 6.630 4.719 -1.206 0.00 0.00 H
|
||||
ATOM 14 HB3 ALA A 2 5.809 3.141 -1.241 0.00 0.00 H
|
||||
ATOM 15 C ALA A 2 4.713 6.129 0.000 0.00 0.00 C
|
||||
ATOM 16 O ALA A 2 3.601 6.653 0.000 0.00 0.00 O
|
||||
ATOM 17 N NME A 3 5.846 6.835 0.000 0.00 0.00 N
|
||||
ATOM 18 H NME A 3 6.737 6.359 -0.000 0.00 0.00 H
|
||||
ATOM 19 C NME A 3 5.846 8.284 0.000 0.00 0.00 C
|
||||
ATOM 20 H1 NME A 3 4.819 8.648 0.000 0.00 0.00 H
|
||||
ATOM 21 H2 NME A 3 6.360 8.648 0.890 0.00 0.00 H
|
||||
ATOM 22 H3 NME A 3 6.360 8.648 -0.890 0.00 0.00 H
|
||||
TER 23 NME A 3
|
||||
CONECT 5 7
|
||||
CONECT 15 17
|
||||
14
examples/trajectory/protein.rst7
Normal file
14
examples/trajectory/protein.rst7
Normal file
@@ -0,0 +1,14 @@
|
||||
alanine-dipeptide.solvated.pdb
|
||||
22
|
||||
0.7494821 1.2436848 0.8743532 1.0856344 2.2423820 0.5955986
|
||||
0.4304414 2.9747953 1.0671825 1.0497815 2.3544810 -0.4880289
|
||||
2.5015950 2.4471725 1.0820421 3.1003812 1.5343071 1.6479120
|
||||
3.0220696 3.6519467 0.8741013 2.4411554 4.3533213 0.4373955
|
||||
4.3920715 4.0500473 1.2160543 4.7674596 3.4172266 2.0202454
|
||||
5.2805058 3.8202998 -0.0180103 4.9565949 4.4537317 -0.8438106
|
||||
6.3180425 4.0583459 0.2164072 5.2327259 2.7740601 -0.3200050
|
||||
4.4431625 5.5106563 1.7135265 3.4307644 6.2198007 1.6891606
|
||||
5.6170320 5.9613562 2.1744082 6.3997462 5.3231585 2.1616313
|
||||
5.8784762 7.3296314 2.6320299 5.1056278 8.0184146 2.2908769
|
||||
5.9253575 7.3544224 3.7207393 6.8360338 7.6745804 2.2419090
|
||||
30.0000000 30.0000000 30.0000000 90.0000000 90.0000000 90.0000000
|
||||
23286
package-lock.json
generated
23286
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.2",
|
||||
"version": "5.6.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": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
@@ -74,7 +74,7 @@
|
||||
"js"
|
||||
],
|
||||
"transform": {
|
||||
"\\.ts$": "ts-jest"
|
||||
"\\.ts$": "esbuild-jest-transform"
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
@@ -121,58 +121,62 @@
|
||||
"Andy Turner <agdturner@gmail.com>",
|
||||
"Lukáš Polák <admin@lukaspolak.cz>",
|
||||
"Chetan Mishra <chetan.s115@gmail.com>",
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>"
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>",
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>",
|
||||
"Diego del Alamo <diego.delalamo@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react": "^18.3.27",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"@types/webxr": "^0.5.24",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||
"@typescript-eslint/parser": "^8.53.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"cpx2": "^8.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"esbuild": "^0.25.5",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "^9.29.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"esbuild": "^0.27.2",
|
||||
"esbuild-jest-transform": "^2.0.1",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest": "^30.2.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.89.1",
|
||||
"simple-git": "^3.28.0",
|
||||
"ts-jest": "^29.3.4",
|
||||
"sass": "^1.97.2",
|
||||
"simple-git": "^3.30.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^18.19.111",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/swagger-ui-dist": "3.30.5",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^20.19.30",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/swagger-ui-dist": "3.30.6",
|
||||
"argparse": "^2.0.1",
|
||||
"compression": "^1.8.0",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"express": "^5.2.1",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^5.1.2",
|
||||
"immutable": "^5.1.4",
|
||||
"io-ts": "^2.2.22",
|
||||
"mutative": "^1.3.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.24.0",
|
||||
"swagger-ui-dist": "^5.31.0",
|
||||
"tslib": "^2.8.1",
|
||||
"util.promisify": "^1.1.3"
|
||||
},
|
||||
|
||||
@@ -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' },
|
||||
@@ -131,7 +131,6 @@ function getPaths(app) {
|
||||
|
||||
async function createBundle(app) {
|
||||
const { name, kind } = app;
|
||||
|
||||
const { prefix, entry, outfile } = getPaths(app);
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
@@ -161,6 +160,7 @@ async function createBundle(app) {
|
||||
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),
|
||||
__MOLSTAR_PLUGIN_VERSION__: JSON.stringify(VERSION),
|
||||
__MOLSTAR_BUILD_TIMESTAMP__: `${TIMESTAMP}`,
|
||||
@@ -172,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', ''));
|
||||
@@ -229,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();
|
||||
|
||||
@@ -260,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));
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
|
||||
const git = require('simple-git');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
const argparse = require('argparse');
|
||||
|
||||
const VERSION = require(path.resolve(__dirname, '../package.json')).version;
|
||||
const MVS_STORIES_VERSION = require(path.resolve(__dirname, '../src/apps/mvs-stories/version.ts')).VERSION;
|
||||
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git";
|
||||
const remoteUrl = 'https://github.com/molstar/molstar.github.io.git';
|
||||
const dataDir = path.resolve(__dirname, '../data/');
|
||||
const buildDir = path.resolve(__dirname, '../build/');
|
||||
const deployDir = path.resolve(__dirname, '../deploy/');
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -147,6 +147,7 @@ export class MesoscaleExplorer {
|
||||
behaviors: [
|
||||
PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
|
||||
PluginSpec.Behavior(PluginBehaviors.Camera.CameraControls),
|
||||
PluginSpec.Behavior(PluginBehaviors.State.SnapshotControls),
|
||||
|
||||
PluginSpec.Behavior(MesoFocusLoci),
|
||||
PluginSpec.Behavior(MesoSelectLoci),
|
||||
@@ -252,6 +253,10 @@ export class MesoscaleExplorer {
|
||||
},
|
||||
cameraFog: { name: 'off', params: {} },
|
||||
hiZ: { enabled: true },
|
||||
xr: {
|
||||
disablePostprocessing: false,
|
||||
sceneRadiusInMeters: 0.75,
|
||||
},
|
||||
});
|
||||
|
||||
plugin.representation.structure.registry.clear();
|
||||
@@ -261,7 +266,6 @@ export class MesoscaleExplorer {
|
||||
image: true,
|
||||
componentManager: false,
|
||||
structureSelection: true,
|
||||
behavior: true,
|
||||
});
|
||||
|
||||
plugin.managers.lociLabels.clearProviders();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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>
|
||||
*/
|
||||
@@ -21,6 +21,7 @@ const Key = Binding.TriggerKey;
|
||||
const DefaultMesoFocusLociBindings = {
|
||||
clickCenter: Binding([
|
||||
Trigger(B.Flag.Primary, M.create()),
|
||||
Trigger(B.Flag.Trigger),
|
||||
], 'Camera center', 'Click element using ${triggers}'),
|
||||
clickCenterFocus: Binding([
|
||||
Trigger(B.Flag.Secondary, M.create()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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>
|
||||
*/
|
||||
@@ -24,7 +24,8 @@ const Trigger = Binding.Trigger;
|
||||
|
||||
const DefaultMesoSelectLociBindings = {
|
||||
click: Binding([
|
||||
Trigger(B.Flag.Primary, M.create())
|
||||
Trigger(B.Flag.Primary, M.create()),
|
||||
Trigger(B.Flag.Trigger),
|
||||
], 'Click', 'Click element using ${triggers}'),
|
||||
clickToggleSelect: Binding([
|
||||
Trigger(B.Flag.Primary, M.create({ shift: true })),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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>
|
||||
*/
|
||||
@@ -18,12 +18,14 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { RendererParams } from '../../../mol-gl/renderer';
|
||||
import { TrackballControlsParams } from '../../../mol-canvas3d/controls/trackball';
|
||||
import { XRManagerParams } from '../../../mol-canvas3d/helper/xr-manager';
|
||||
|
||||
const Spacer = () => <div style={{ height: '2em' }} />;
|
||||
|
||||
const ViewportParams = {
|
||||
renderer: PD.Group(RendererParams),
|
||||
trackball: PD.Group(TrackballControlsParams),
|
||||
xr: PD.Group(XRManagerParams, { label: 'XR' }),
|
||||
};
|
||||
|
||||
class ViewportSettingsUI extends CollapsableControls<{}, {}> {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -9,14 +9,14 @@ import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import type { MVSStoriesViewerModel } from './elements/viewer';
|
||||
|
||||
export type MVSStoriesCommand =
|
||||
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData | string | Uint8Array }
|
||||
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData | string | Uint8Array<ArrayBuffer> }
|
||||
|
||||
|
||||
export class MVSStoriesContext {
|
||||
commands = new BehaviorSubject<MVSStoriesCommand | undefined>(undefined);
|
||||
state = {
|
||||
viewers: new BehaviorSubject<{ name?: string, model: MVSStoriesViewerModel }[]>([]),
|
||||
currentStoryData: new BehaviorSubject<string | Uint8Array | undefined>(undefined),
|
||||
currentStoryData: new BehaviorSubject<string | Uint8Array<ArrayBuffer> | undefined>(undefined),
|
||||
isLoading: new BehaviorSubject(false),
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
|
||||
import { PluginReactContext } from '../../../mol-plugin-ui/base';
|
||||
import { CSSProperties } from 'react';
|
||||
import { CSSProperties, useEffect, useState } from 'react';
|
||||
import { Markdown } from '../../../mol-plugin-ui/controls/markdown';
|
||||
|
||||
export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
@@ -70,6 +70,28 @@ export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
}
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <div>
|
||||
<div style={{ marginBottom: 16 }}><i>Loading times may vary depending on the story size, your internet connection, and device performance</i></div>
|
||||
<div>Fetching data<Dots /></div>
|
||||
<div>Generating animations<Dots /></div>
|
||||
<div>Preparing visuals<Dots /></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Dots() {
|
||||
const [dots, setDots] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDots(d => (d + 1) % 4);
|
||||
}, Math.random() * 500 + 300);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return <span>{'.'.repeat(dots)}</span>;
|
||||
}
|
||||
|
||||
export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnapshotMarkdownModel }) {
|
||||
const state = useBehavior(model.state);
|
||||
const isLoading = useBehavior(model.context.state.isLoading);
|
||||
@@ -79,7 +101,8 @@ export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnaps
|
||||
|
||||
if (isLoading) {
|
||||
return <div style={style} className={className}>
|
||||
<i>Loading...</i>
|
||||
<h3>The story will be ready momentarily</h3>
|
||||
<Loading />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export class MVSStoriesViewerModel extends PluginComponent {
|
||||
loadedData = await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
|
||||
}
|
||||
if (StringLike.is(loadedData) || loadedData instanceof Uint8Array) {
|
||||
this.context.state.currentStoryData.next(loadedData as string | Uint8Array);
|
||||
this.context.state.currentStoryData.next(loadedData as string | Uint8Array<ArrayBuffer>);
|
||||
} else if (loadedData) {
|
||||
this.context.state.currentStoryData.next(JSON.stringify(loadedData));
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#links .sep {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
#viewer {
|
||||
position: absolute;
|
||||
@@ -90,13 +94,16 @@
|
||||
</div>
|
||||
|
||||
<div id="links">
|
||||
<a href="#" id="mvs-data">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
<span id="open-in-stories" style="display: none;"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a> <span class="sep">•</span></span>
|
||||
<span id="open-in-molstar" style="display: none;"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a> <span class="sep">•</span></span>
|
||||
<a href="#" id="mvs-data" title="MolViewSpec State for this story. Can be opened in the Mol* app.">Download MVS</a> <span class="sep">•</span> <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var storyId = urlParams.get('story-id');
|
||||
var storyUrl = urlParams.get('story-url');
|
||||
var storySessionUrl = urlParams.get('story-session-url');
|
||||
var format = urlParams.get('data-format');
|
||||
|
||||
// For testing purposes:
|
||||
@@ -104,12 +111,32 @@
|
||||
// storyUrl = 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/kinase-story.mvsj';
|
||||
// }
|
||||
|
||||
var molstarDataLink = storyUrl;
|
||||
var editInStoriesUrl = undefined;
|
||||
|
||||
if (storyId) {
|
||||
mvsStories.loadFromID(storyId, { format: format || 'mvsj', contextName: 'story1' });
|
||||
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?published-session-id=' + storyId;
|
||||
molstarDataLink = 'https://stories.molstar.org/api/story/' + storyId + '/data';
|
||||
} else if (storyUrl) {
|
||||
mvsStories.loadFromURL(storyUrl, { format: format || 'mvsj', contextName: 'story1' });
|
||||
}
|
||||
|
||||
if (!editInStoriesUrl && storySessionUrl) {
|
||||
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?session-url=' + encodeURIComponent(storySessionUrl);
|
||||
}
|
||||
|
||||
if (molstarDataLink) {
|
||||
var molstarLink = 'https://molstar.org/viewer?mvs-url=' + encodeURIComponent(molstarDataLink) + '&mvs-format=' + encodeURIComponent(format || 'mvsj');
|
||||
document.getElementById('open-in-molstar-link').setAttribute('href', molstarLink);
|
||||
document.getElementById('open-in-molstar').style.display = 'inline';
|
||||
}
|
||||
|
||||
if (editInStoriesUrl) {
|
||||
document.getElementById('open-in-stories-link').setAttribute('href', editInStoriesUrl);
|
||||
document.getElementById('open-in-stories').style.display = 'inline';
|
||||
}
|
||||
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
mvsStories.downloadCurrentStory({ contextName: 'story1' });
|
||||
|
||||
@@ -28,7 +28,7 @@ export function loadFromURL(url: string, options?: { format: 'mvsx' | 'mvsj', co
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function loadFromData(data: MVSData | string | Uint8Array, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
|
||||
export function loadFromData(data: MVSData | string | Uint8Array<ArrayBuffer>, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
|
||||
setTimeout(() => {
|
||||
getContext(options?.contextName).dispatch({
|
||||
kind: 'load-mvs',
|
||||
|
||||
@@ -182,6 +182,15 @@
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1d4ed7;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
|
||||
@@ -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,95 +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,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.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> = {}) {
|
||||
@@ -145,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 || []],
|
||||
@@ -187,8 +137,11 @@ export class Viewer {
|
||||
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
|
||||
[PluginConfig.General.PowerPreference, o.powerPreference],
|
||||
[PluginConfig.General.ResolutionMode, o.resolutionMode],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowReset, o.viewportShowReset],
|
||||
[PluginConfig.Viewport.ShowScreenshotControls, o.viewportShowScreenshotControls],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowToggleFullscreen, o.viewportShowToggleFullscreen],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
|
||||
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
|
||||
@@ -222,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 });
|
||||
@@ -517,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));
|
||||
@@ -525,7 +491,7 @@ export class Viewer {
|
||||
} else if (format === 'mvsx') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
|
||||
await this.plugin.runTask(Task.create('Load MVSX file', async ctx => {
|
||||
const parsed = await loadMVSX(this.plugin, ctx, data);
|
||||
const parsed = await loadMVSX(this.plugin, ctx, data, { doNotClearAssets: options?.appendSnapshots });
|
||||
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl, ...options });
|
||||
}));
|
||||
} else {
|
||||
@@ -536,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, 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);
|
||||
}
|
||||
|
||||
@@ -561,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();
|
||||
}
|
||||
}
|
||||
@@ -580,52 +595,12 @@ export interface VolumeIsovalueInfo {
|
||||
|
||||
export interface LoadTrajectoryParams {
|
||||
model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
|
||||
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
|
||||
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format?: BuiltInTrajectoryFormat /* mmcif */ }
|
||||
| { kind: 'topology-url', url: string, format: BuiltInTopologyFormat, isBinary?: boolean }
|
||||
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuiltInTopologyFormat },
|
||||
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInTopologyFormat },
|
||||
modelLabel?: string,
|
||||
coordinates: { kind: 'coordinates-url', url: string, format: BuiltInCoordinatesFormat, isBinary?: boolean }
|
||||
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuiltInCoordinatesFormat },
|
||||
| { 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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
69
src/apps/viewer/extensions.ts
Normal file
69
src/apps/viewer/extensions.ts
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -28,10 +28,10 @@
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 100px;
|
||||
top: 100px;
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
@@ -66,6 +66,7 @@
|
||||
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
|
||||
var illumination = getParam('illumination', '[^&]+').trim() === '1';
|
||||
var resolutionMode = getParam('resolution-mode', '[^&]+').trim().toLowerCase();
|
||||
var viewportShowToggleFullscreen = getParam('show-toggle-fullscreen', '[^&]+').trim() === '1';
|
||||
|
||||
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
|
||||
|
||||
@@ -73,6 +74,7 @@
|
||||
disabledExtensions: [], // anything from Object.keys(molstar.ExtensionMap)
|
||||
layoutShowControls: !hideControls,
|
||||
viewportShowExpand: false,
|
||||
viewportShowToggleFullscreen: viewportShowToggleFullscreen,
|
||||
collapseLeftPanel: collapseLeftPanel,
|
||||
pdbProvider: pdbProvider || 'pdbe',
|
||||
emdbProvider: emdbProvider || 'pdbe',
|
||||
@@ -87,7 +89,7 @@
|
||||
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
|
||||
powerPreference: powerPreference || 'high-performance',
|
||||
illumination: illumination,
|
||||
resolutionMode: resolutionMode || 'auto'
|
||||
resolutionMode: resolutionMode || 'auto',
|
||||
}).then(viewer => {
|
||||
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
|
||||
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
|
||||
|
||||
@@ -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
58
src/apps/viewer/lib.ts
Normal 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
179
src/apps/viewer/mvs.html
Normal 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>
|
||||
72
src/apps/viewer/options.ts
Normal file
72
src/apps/viewer/options.ts
Normal 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;
|
||||
42
src/apps/viewer/presets.ts
Normal file
42
src/apps/viewer/presets.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
7
src/apps/viewer/theme/blue.ts
Normal file
7
src/apps/viewer/theme/blue.ts
Normal 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';
|
||||
7
src/apps/viewer/theme/dark.ts
Normal file
7
src/apps/viewer/theme/dark.ts
Normal 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';
|
||||
7
src/apps/viewer/theme/light.ts
Normal file
7
src/apps/viewer/theme/light.ts
Normal 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';
|
||||
@@ -29,7 +29,7 @@ async function readFile(ctx: RuntimeContext, filename: string): Promise<ReaderRe
|
||||
const isGz = /\.gz$/i.test(filename);
|
||||
if (filename.match(/\.bcif/)) {
|
||||
let input = await readFileAsync(filename);
|
||||
if (isGz) input = await unzipAsync(input);
|
||||
if (isGz) input = await unzipAsync(input) as NonSharedBuffer;
|
||||
return await CIF.parseBinary(new Uint8Array(input)).runInContext(ctx);
|
||||
} else {
|
||||
const data = isGz ? await unzipAsync(await readFileAsync(filename)) : await readFileAsync(filename);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
|
||||
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-validation';
|
||||
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018 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 * as fs from 'fs';
|
||||
@@ -38,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) })).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.
|
||||
|
||||
@@ -21,7 +21,8 @@ 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';
|
||||
|
||||
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
|
||||
@@ -190,6 +191,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();
|
||||
@@ -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 };
|
||||
}
|
||||
39
src/examples/basic-wrapper/tm-align.html
Normal file
39
src/examples/basic-wrapper/tm-align.html
Normal 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>
|
||||
@@ -30,8 +30,8 @@ import { ExampleMol } from './example-data';
|
||||
import './index.html';
|
||||
import { jsonCifToMolfile } from './molfile';
|
||||
import { RGroupName } from './r-groups';
|
||||
import { SingleTaskQueue } from './utils';
|
||||
import { molfileToJSONCif } from '../../extensions/json-cif/utils';
|
||||
import { SingleTaskQueue } from '../../mol-util/single-task-queue';
|
||||
|
||||
async function init(target: HTMLElement | string, molfile: string = ExampleMol) {
|
||||
const root = typeof target === 'string' ? document.getElementById(target)! : target;
|
||||
|
||||
407
src/examples/mvs-stories/stories/animation.ts
Normal file
407
src/examples/mvs-stories/stories/animation.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder, Structure as MVSStructure, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../../../extensions/mvs/tree/mvs/param-types';
|
||||
import { Mat4 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
const Colors = {
|
||||
'1cbs': '#4577B2' as ColorT,
|
||||
|
||||
'ligand-away': '#F3794C' as ColorT,
|
||||
'ligand-docked': '#B9E3A0' as ColorT,
|
||||
};
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'Animation Demo',
|
||||
key: 'intro',
|
||||
description: `### Molecular Animation
|
||||
A story showcasing MolViewSpec animation capabilities.
|
||||
|
||||
[\[**🔄 Replay Intro**\]](!play-transition)
|
||||
[\[**⏵ Play Snapshots**\]](!play-snapshots)
|
||||
[\[**⏹ Stop Animation**\]](!stop-animation)
|
||||
|
||||
[\[**➡️ Next Snapshot**\]](!next-snapshot)
|
||||
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
const prims = _1cbs.primitives({
|
||||
ref: 'prims',
|
||||
label_opacity: 0,
|
||||
});
|
||||
prims.label({ text: 'Animation Demo', position: { label_asym_id: 'A' }, label_size: 10 });
|
||||
|
||||
const anim = builder.animation({
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'rock',
|
||||
params: { speed: 0.5 },
|
||||
}
|
||||
}
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
start: 1,
|
||||
end: 0.66,
|
||||
});
|
||||
|
||||
|
||||
// Uncomment this to make 2nd frame render much faster
|
||||
// It will cause shader compilation to happen during the 1st snapshot
|
||||
|
||||
// const surface = poly.representation({
|
||||
// type: 'surface',
|
||||
// surface_type: 'gaussian',
|
||||
// }).opacity({ opacity: 0 });
|
||||
|
||||
// _1cbs.component({ selector: 'ligand' })
|
||||
// .representation({ type: 'ball_and_stick' })
|
||||
// .opacity({ opacity: 0 });
|
||||
|
||||
// surface.clip({
|
||||
// ref: 'clip',
|
||||
// type: 'plane',
|
||||
// point: [22.0, 15, 0],
|
||||
// normal: [0, 0, 1],
|
||||
// });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-11.49, -37.05, 15.78],
|
||||
target: [15.85, 17.26, 24.32],
|
||||
up: [-0.88, 0.4, 0.26],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Ligand Docking',
|
||||
description: `Animate ligand moving to the binding site`,
|
||||
linger_duration_ms: 2500,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
const [poly, repr] = polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
repr.colorFromSource({
|
||||
ref: 'residue_colors',
|
||||
schema: 'residue',
|
||||
category_name: 'atom_site',
|
||||
field_name: 'label_comp_id',
|
||||
palette: {
|
||||
kind: 'categorical',
|
||||
missing_color: 'white',
|
||||
colors: {
|
||||
ALA: 'red',
|
||||
ILE: 'white',
|
||||
LYS: 'white',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const surface = poly.representation({
|
||||
type: 'surface',
|
||||
surface_type: 'gaussian',
|
||||
}).opacity({ opacity: 0.33 });
|
||||
|
||||
_1cbs.component({ selector: 'ligand' })
|
||||
.transform({
|
||||
ref: 'xform',
|
||||
translation: [5, 20, -20],
|
||||
rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1],
|
||||
rotation_center: 'centroid',
|
||||
})
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({ ref: 'ligand-color', color: 'red' });
|
||||
|
||||
surface.clip({
|
||||
ref: 'clip',
|
||||
type: 'plane',
|
||||
point: [22.0, 15, 0],
|
||||
normal: [0, 0, 1],
|
||||
});
|
||||
|
||||
const anim = builder.animation();
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
easing: 'sin-in',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 600,
|
||||
duration_ms: 800,
|
||||
property: ['point', 2],
|
||||
end: 0,
|
||||
easing: 'sin-out',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'vec3',
|
||||
target_ref: 'xform',
|
||||
duration_ms: 2000,
|
||||
property: 'translation',
|
||||
end: [0, 0, 0],
|
||||
noise_magnitude: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'rotation_matrix',
|
||||
target_ref: 'xform',
|
||||
duration_ms: 2000,
|
||||
property: 'rotation',
|
||||
noise_magnitude: 0.2,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'ligand-color',
|
||||
duration_ms: 2000,
|
||||
property: 'color',
|
||||
end: Colors['ligand-docked'],
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'residue_colors',
|
||||
duration_ms: 2000,
|
||||
property: ['palette', 'colors'],
|
||||
start: {
|
||||
ALA: 'yellow',
|
||||
},
|
||||
end: {
|
||||
ILE: 'blue',
|
||||
LYS: 'purple',
|
||||
},
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-30.63, 77.29, 2.28],
|
||||
target: [19.16, 26.15, 22.82],
|
||||
up: [0.69, 0.71, 0.09],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Highlight & Opacity',
|
||||
description: `Animate emissive, opacity and transform properties`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 0,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
const [poly,] = polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
poly.representation({
|
||||
type: 'surface',
|
||||
surface_type: 'gaussian'
|
||||
}).opacity({ ref: 'opacity', opacity: 1 }).color({ ref: 'surface-color', color: 'white' });
|
||||
|
||||
_1cbs.component({ selector: 'ligand' })
|
||||
.transform({ ref: 'xform', translation: [0, 0, 0] })
|
||||
.representation({
|
||||
ref: 'repr',
|
||||
type: 'ball_and_stick',
|
||||
custom: {
|
||||
molstar_representation_params: {
|
||||
emissive: 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
.color({ color: Colors['ligand-docked'] });
|
||||
|
||||
const primitives = builder.primitives({
|
||||
ref: 'primitives',
|
||||
instances: [
|
||||
Mat4.identity()
|
||||
],
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
primitives.ellipsoid({
|
||||
center: [0, 0, 0],
|
||||
radius: [2, 3, 2.5],
|
||||
color: 'red'
|
||||
});
|
||||
|
||||
const anim = builder.animation();
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'repr',
|
||||
duration_ms: 1000,
|
||||
property: ['custom', 'molstar_representation_params', 'emissive'],
|
||||
end: 0.2,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'opacity',
|
||||
duration_ms: 1000,
|
||||
frequency: 2,
|
||||
alternate_direction: true,
|
||||
property: 'opacity',
|
||||
end: 0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'transform_matrix',
|
||||
target_ref: 'primitives',
|
||||
property: ['instances', 0],
|
||||
translation_start: [20.24, 29.64, 14.85],
|
||||
translation_end: [21.84, 21.71, 27.04],
|
||||
translation_frequency: 4,
|
||||
pivot: [0, 0, 0],
|
||||
rotation_noise_magnitude: 0.2,
|
||||
scale_end: [0.01, 0.01, 0.01],
|
||||
duration_ms: 1000,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'primitives',
|
||||
duration_ms: 1000,
|
||||
property: 'opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'surface-color',
|
||||
duration_ms: 2000,
|
||||
property: 'color',
|
||||
palette: {
|
||||
kind: 'continuous',
|
||||
colors: ['white', Colors['1cbs'], 'white'],
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [6.92, 47.17, 10.68],
|
||||
target: [21.79, 22.2, 23.43],
|
||||
up: [0.8, 0.57, 0.2],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}
|
||||
];
|
||||
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
return builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
}
|
||||
|
||||
function polymer(structure: MVSStructure, options?: { color?: ColorT }) {
|
||||
const component = structure.component({ selector: { label_asym_id: 'A' } });
|
||||
const reprensentation = component.representation({ type: 'cartoon' });
|
||||
if (options?.color) {
|
||||
reprensentation.color({ color: options.color });
|
||||
}
|
||||
return [component, reprensentation] as const;
|
||||
}
|
||||
|
||||
function pdbUrl(id: string) {
|
||||
return `https://www.ebi.ac.uk/pdbe/entry-files/download/${id.toLowerCase()}.bcif`;
|
||||
}
|
||||
|
||||
export function buildStory(): MVSData_States {
|
||||
const snapshots = Steps.map((s, i) => {
|
||||
const builder = s.state();
|
||||
if (s.camera) builder.camera(s.camera);
|
||||
|
||||
builder.canvas({
|
||||
custom: {
|
||||
molstar_postprocessing: {
|
||||
enable_outline: true,
|
||||
enable_ssao: true,
|
||||
background: {
|
||||
name: 'horizontalGradient',
|
||||
params: {
|
||||
topColor: 0x777777,
|
||||
bottomColor: 0xffffff,
|
||||
}
|
||||
},
|
||||
// Example with background image:
|
||||
// background: {
|
||||
// name: 'image',
|
||||
// params: {
|
||||
// // URL can also be filename in MVSX archive
|
||||
// source: { name: 'url', params: 'URL' }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const description = i > 0 ? `${s.description}\n\n[Go to start](#intro)` : s.description;
|
||||
|
||||
return builder.getSnapshot({
|
||||
title: s.header,
|
||||
key: s.key,
|
||||
description,
|
||||
description_format: 'markdown',
|
||||
linger_duration_ms: s.linger_duration_ms ?? 500,
|
||||
transition_duration_ms: s.transition_duration_ms ?? 1000,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'Animation Showcase',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
186
src/examples/mvs-stories/stories/audio.ts
Normal file
186
src/examples/mvs-stories/stories/audio.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -6,8 +6,14 @@
|
||||
|
||||
import { buildStory as kinase } from './kinase';
|
||||
import { buildStory as tbp } from './tbp';
|
||||
import { buildStory as animation } from './animation';
|
||||
import { buildStory as audio } from './audio';
|
||||
import { buildStory as motm1 } from './motm1';
|
||||
|
||||
export const Stories = [
|
||||
{ id: 'kinase', name: 'BCR-ABL: A Kinase Out of Control', buildStory: kinase },
|
||||
{ id: 'tata', name: 'TATA-Binding Protein and its Role in Transcription Initiation ', buildStory: tbp },
|
||||
{ id: 'motm1', name: 'RCSB PDB Molecule of the Month #1', buildStory: motm1 },
|
||||
{ id: 'animation-example', name: 'Molecular Animation Example', buildStory: animation },
|
||||
{ id: 'audio-example', name: 'Audio Playback Example', buildStory: audio },
|
||||
] as const;
|
||||
1020
src/examples/mvs-stories/stories/motm1.ts
Normal file
1020
src/examples/mvs-stories/stories/motm1.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
private materialMap = new Map<string, number>();
|
||||
private accessors: Record<string, any>[] = [];
|
||||
private bufferViews: Record<string, any>[] = [];
|
||||
private binaryBuffer: ArrayBuffer[] = [];
|
||||
private binaryBuffer: ArrayBufferLike[] = [];
|
||||
private byteOffset = 0;
|
||||
private centerTransform: Mat4;
|
||||
|
||||
@@ -72,7 +72,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
private addBuffer(buffer: ArrayBuffer, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
|
||||
private addBuffer(buffer: ArrayBufferLike, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
|
||||
this.binaryBuffer.push(buffer);
|
||||
|
||||
const bufferViewOffset = this.bufferViews.length;
|
||||
@@ -304,7 +304,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
materials: this.materials
|
||||
};
|
||||
|
||||
const createChunk = (chunkType: number, data: ArrayBuffer[], byteLength: number, padChar: number): [ArrayBuffer[], number] => {
|
||||
const createChunk = (chunkType: number, data: ArrayBufferLike[], byteLength: number, padChar: number): [ArrayBufferLike[], number] => {
|
||||
let padding = null;
|
||||
if (byteLength % 4 !== 0) {
|
||||
const pad = 4 - (byteLength % 4);
|
||||
|
||||
@@ -305,7 +305,7 @@ function PlotInteractivity({ drawing, interactity }: { drawing: MAPairwiseMetric
|
||||
let labelNode: ReactNode | undefined;
|
||||
if (label) {
|
||||
const labelStyle: CSSProperties | undefined = label ? { fontSize: '45px', fill: 'black', fontWeight: 'bold', pointerEvents: 'none', userSelect: 'none' } : undefined;
|
||||
let x: number, y: number, anchor: string;
|
||||
let x: number, y: number, anchor: 'start' | 'end';
|
||||
if (crosshairOffset![0] < PlotSize / 2) {
|
||||
x = PlotOffset + crosshairOffset![0] + 20;
|
||||
anchor = 'start';
|
||||
|
||||
@@ -39,7 +39,7 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
|
||||
const format = options?.format ?? 'cif';
|
||||
const { structures } = plugin.managers.structure.hierarchy.current;
|
||||
|
||||
const files: [name: string, data: string | Uint8Array][] = [];
|
||||
const files: [name: string, data: string | Uint8Array<ArrayBuffer>][] = [];
|
||||
const entryMap = new Map<string, number>();
|
||||
|
||||
for (const _s of structures) {
|
||||
@@ -80,7 +80,7 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
|
||||
if (files.length === 1) {
|
||||
download(new Blob([files[0][1]]), files[0][0]);
|
||||
} else if (files.length > 1) {
|
||||
const zipData: Record<string, Uint8Array> = {};
|
||||
const zipData: Record<string, Uint8Array<ArrayBuffer>> = {};
|
||||
for (const [fn, data] of files) {
|
||||
if (data instanceof Uint8Array) {
|
||||
zipData[fn] = data;
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface Mp4EncoderParams<A extends PluginStateAnimation = PluginStateAn
|
||||
quantizationParameter?: number
|
||||
}
|
||||
|
||||
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>) {
|
||||
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>): Promise<Uint8Array<ArrayBuffer>> {
|
||||
await ctx.update({ message: 'Initializing...', isIndeterminate: true });
|
||||
|
||||
validateViewport(params);
|
||||
@@ -88,7 +88,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
|
||||
stoppedAnimation = true;
|
||||
encoder.finalize();
|
||||
finalized = true;
|
||||
return encoder.FS.readFile(encoder.outputFilename);
|
||||
return encoder.FS.readFile(encoder.outputFilename) as Uint8Array<ArrayBuffer>;
|
||||
} finally {
|
||||
if (finalized) encoder.delete();
|
||||
if (params.customBackground !== void 0) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Mp4AnimationParams, Mp4Controls } from './controls';
|
||||
|
||||
interface State {
|
||||
busy?: boolean,
|
||||
data?: { movie: Uint8Array, filename: string };
|
||||
data?: { movie: Uint8Array<ArrayBuffer>, filename: string };
|
||||
}
|
||||
|
||||
export class Mp4EncoderUI extends CollapsableControls<{}, State> {
|
||||
|
||||
@@ -1 +1 @@
|
||||
Find the MVS extension documentation [here](../../../docs/extensions/mvs/README.md).
|
||||
Please refer to the standalone documentation [here](https://molstar.org/mol-view-spec-docs/).
|
||||
|
||||
@@ -12,7 +12,7 @@ import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StructureRepresentationProvider } from '../../mol-repr/structure/representation';
|
||||
import { StateAction, StateObjectCell, StateTree } from '../../mol-state';
|
||||
import { StateAction, StateObject, StateObjectCell, StateTree } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { fileToDataUri } from '../../mol-util/file';
|
||||
@@ -111,6 +111,19 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
this.ctx.state.data.actions.add(action);
|
||||
}
|
||||
|
||||
this.ctx.state.data.registerRefResolver('mvs', (state, ref) => {
|
||||
const tagSearch = StateTree.doPreOrder(state.tree, state.tree.root, { ref, ret: undefined as StateObject | undefined }, (n, _, s) => {
|
||||
if (!n.tags) return;
|
||||
for (const t of n.tags) {
|
||||
if (t.startsWith('mvs-ref:') && t.substring(8) === ref) {
|
||||
s.ret = state.cells.get(n.ref)?.obj?.data;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return tagSearch.ret;
|
||||
});
|
||||
|
||||
this.ctx.managers.markdownExtensions.registerRefResolver('mvs', (plugin, refs) => {
|
||||
const mvsRefs = new Set(refs.map(ref => `mvs-ref:${ref}`));
|
||||
return StateTree.doPreOrder(
|
||||
@@ -180,6 +193,7 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
for (const action of this.registrables.actions ?? []) {
|
||||
this.ctx.state.data.actions.remove(action);
|
||||
}
|
||||
this.ctx.state.data.removeRefResolver('mvs');
|
||||
this.ctx.managers.markdownExtensions.removeRefResolver('mvs');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
/**
|
||||
* 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 Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { Canvas3DParams, Canvas3DProps } 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';
|
||||
import { DofParams } from '../../mol-canvas3d/passes/dof';
|
||||
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: 5,
|
||||
extraRadius: 0,
|
||||
};
|
||||
const DefaultCanvasBackgroundColor = ColorNames.white;
|
||||
|
||||
|
||||
@@ -56,35 +58,32 @@ export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: Mols
|
||||
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
|
||||
const up = Vec3.create(...params.up);
|
||||
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
|
||||
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius, radiusMax: radius };
|
||||
|
||||
const snapshot: Partial<Camera.Snapshot> = {
|
||||
target,
|
||||
position,
|
||||
up,
|
||||
radius,
|
||||
radiusMax: radius,
|
||||
minNear: params.near ?? undefined,
|
||||
};
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,59 +96,134 @@ 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;
|
||||
}
|
||||
|
||||
/** Set canvas properties based on a canvas node. */
|
||||
export function setCanvas(plugin: PluginContext, node: MolstarNode<'canvas'> | undefined) {
|
||||
plugin.canvas3d?.setProps(old => modifyCanvasProps(old, node));
|
||||
function optionalParams(enable: boolean | undefined, values: any, params: ParamDefinition.Params, fallback: any) {
|
||||
if (typeof enable === 'boolean') {
|
||||
return enable
|
||||
? { name: 'on', params: { ...ParamDefinition.getDefaultValues(params), ...values } }
|
||||
: { name: 'off', params: {} };
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeBackground(variant: any, prev: any): any {
|
||||
if (!variant) return prev;
|
||||
return ParamDefinition.normalizeParams(BackgroundParams, { variant }, 'children');
|
||||
}
|
||||
|
||||
/** Create a deep copy of `oldCanvasProps` with values modified according to a canvas node params. */
|
||||
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, custom?: Record<string, any>): Canvas3DProps {
|
||||
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, animationNode: MVSAnimationNode<'animation'> | undefined): Canvas3DProps {
|
||||
const params = canvasNode?.params;
|
||||
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
|
||||
|
||||
const outline = !!canvasNode?.custom?.molstar_enable_outline;
|
||||
const shadow = !!canvasNode?.custom?.molstar_enable_shadow;
|
||||
const occlusion = !!canvasNode?.custom?.molstar_enable_ssao;
|
||||
const molstar_postprocessing = canvasNode?.custom?.molstar_postprocessing;
|
||||
|
||||
const outline = molstar_postprocessing?.enable_outline;
|
||||
const outlineParams = molstar_postprocessing?.outline_params;
|
||||
|
||||
const shadow = molstar_postprocessing?.enable_shadow;
|
||||
const shadowParams = molstar_postprocessing?.shadow_params;
|
||||
|
||||
const occlusion = molstar_postprocessing?.enable_ssao;
|
||||
const occlusionParams = molstar_postprocessing?.ssao_params;
|
||||
|
||||
const fog = molstar_postprocessing?.enable_fog;
|
||||
const fogParams = molstar_postprocessing?.fog_params;
|
||||
|
||||
const dof = molstar_postprocessing?.enable_depth_of_field;
|
||||
const dofParams = molstar_postprocessing?.depth_of_field_params;
|
||||
|
||||
const bloom = molstar_postprocessing?.enable_bloom;
|
||||
const bloomParams = molstar_postprocessing?.bloom_params;
|
||||
|
||||
const background = molstar_postprocessing?.background;
|
||||
|
||||
const trackballAnimation = animationNode?.custom?.molstar_trackball;
|
||||
const trackballAnimationName = trackballAnimation?.name;
|
||||
const trackballAnimationParams = trackballAnimation?.params ?? {};
|
||||
|
||||
return {
|
||||
...oldCanvasProps,
|
||||
postprocessing: {
|
||||
...oldCanvasProps.postprocessing,
|
||||
outline: outline
|
||||
? { name: 'on', params: ParamDefinition.getDefaultValues(OutlineParams) }
|
||||
: oldCanvasProps.postprocessing.outline,
|
||||
shadow: shadow
|
||||
? { name: 'on', params: ParamDefinition.getDefaultValues(ShadowParams) }
|
||||
: oldCanvasProps.postprocessing.shadow,
|
||||
occlusion: occlusion
|
||||
? { name: 'on', params: ParamDefinition.getDefaultValues(SsaoParams) }
|
||||
: oldCanvasProps.postprocessing.occlusion,
|
||||
outline: optionalParams(outline, outlineParams, OutlineParams, oldCanvasProps.postprocessing.outline),
|
||||
shadow: optionalParams(shadow, shadowParams, ShadowParams, oldCanvasProps.postprocessing.shadow),
|
||||
occlusion: optionalParams(occlusion, occlusionParams, SsaoParams, oldCanvasProps.postprocessing.occlusion),
|
||||
dof: optionalParams(dof, dofParams, DofParams, oldCanvasProps.postprocessing.dof),
|
||||
bloom: optionalParams(bloom, bloomParams, BloomParams, oldCanvasProps.postprocessing.bloom),
|
||||
background: normalizeBackground(background, oldCanvasProps.postprocessing.background),
|
||||
},
|
||||
cameraFog: optionalParams(fog, fogParams, CameraFogParams, oldCanvasProps.cameraFog),
|
||||
renderer: {
|
||||
...oldCanvasProps.renderer,
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
trackball: {
|
||||
...oldCanvasProps?.trackball,
|
||||
...(trackballAnimationName
|
||||
? {
|
||||
animate: {
|
||||
name: trackballAnimationName,
|
||||
params: {
|
||||
...TrackballControlsParams.animate.map(trackballAnimationName)?.defaultValue,
|
||||
...trackballAnimationParams
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resetCanvasProps(plugin: PluginContext) {
|
||||
const old = plugin.canvas3d?.props;
|
||||
plugin.canvas3d?.setProps({
|
||||
...old,
|
||||
postprocessing: {
|
||||
...old,
|
||||
outline: deepClone(DefaultCanvas3DParams.postprocessing.outline),
|
||||
shadow: deepClone(DefaultCanvas3DParams.postprocessing.shadow),
|
||||
occlusion: deepClone(DefaultCanvas3DParams.postprocessing.occlusion),
|
||||
dof: deepClone(DefaultCanvas3DParams.postprocessing.dof),
|
||||
bloom: deepClone(DefaultCanvas3DParams.postprocessing.bloom),
|
||||
background: deepClone(DefaultCanvas3DParams.postprocessing.background),
|
||||
},
|
||||
cameraFog: deepClone(DefaultCanvas3DParams.cameraFog),
|
||||
trackball: {
|
||||
...old?.trackball,
|
||||
animate: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
|
||||
return {
|
||||
factory: MVSAnnotationColorTheme,
|
||||
granularity: 'groupInstance',
|
||||
preferSmoothing: true,
|
||||
preferSmoothing: false,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Assigns colors based on custom MolViewSpec annotation data.',
|
||||
@@ -241,7 +241,7 @@ function makeNumericPaletteScale(props: MVSContinuousPaletteProps | MVSDiscreteP
|
||||
}
|
||||
}
|
||||
|
||||
function makeContinuousPaletteCheckpoints(props: MVSContinuousPaletteProps) {
|
||||
export function makeContinuousPaletteCheckpoints(props: MVSContinuousPaletteProps) {
|
||||
if (props.colors.colors.every(x => Array.isArray(x))) {
|
||||
// Explicit checkpoints
|
||||
const sorted = props.colors.colors.sort((a, b) => a[1] - b[1]);
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -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 p = textPropsForSelection(structure, theme.size.size, rowsInGroup, model);
|
||||
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');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ import { CustomModelProperty } from '../../../mol-model-props/common/custom-mode
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Model } from '../../../mol-model/structure';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure/structure';
|
||||
import { Structure, StructureElement, Unit } from '../../../mol-model/structure/structure';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Jsonable, canonicalJsonString } from '../../../mol-util/json';
|
||||
import { objectOfArraysToArrayOfObjects, pickObjectKeysWithRemapping, promiseAllObj } from '../../../mol-util/object';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { AtomRanges } from '../helpers/atom-ranges';
|
||||
import { ElementRanges } from '../helpers/element-ranges';
|
||||
import { IndicesAndSortings } from '../helpers/indexing';
|
||||
import { MaybeStringParamDefinition } from '../helpers/param-definition';
|
||||
import { MVSAnnotationRow, MVSAnnotationSchema, getCifAnnotationSchema } from '../helpers/schemas';
|
||||
import { getAtomRangesForRow } from '../helpers/selections';
|
||||
import { getAtomRangesForRow, getGaussianRangesForRow, getSphereRangesForRow } from '../helpers/selections';
|
||||
import { Maybe, isDefined, safePromise } from '../helpers/utils';
|
||||
|
||||
|
||||
@@ -141,10 +141,25 @@ export function getMVSAnnotationForStructure(structure: Structure, annotationId:
|
||||
|
||||
type FieldRemapping = Record<string, string | null>;
|
||||
|
||||
/** Mapping `ElementIndex` -> annotation row index for all elements in a `Model`.
|
||||
/** Mapping `ElementIndex` -> annotation row index for all elements of one kind (atoms, spheres, gaussians) in a `Model`.
|
||||
* `-1` means no row applies to the element.
|
||||
* `null` means no row applies to any element. */
|
||||
type IndexedModel = number[] | null;
|
||||
type IndexedElements = number[] | null;
|
||||
|
||||
/** Mapping `ElementIndex` -> annotation row index for atoms, spheres, and gaussians in a `Model`. */
|
||||
type IndexedModel = {
|
||||
atoms: IndexedElements,
|
||||
spheres: IndexedElements,
|
||||
gaussians: IndexedElements,
|
||||
};
|
||||
|
||||
function getIndexedElementsForUnitKind(indexedModel: IndexedModel, unitKind: Unit.Kind): IndexedElements {
|
||||
if (unitKind === Unit.Kind.Atomic) return indexedModel.atoms;
|
||||
if (unitKind === Unit.Kind.Spheres) return indexedModel.spheres;
|
||||
if (unitKind === Unit.Kind.Gaussians) return indexedModel.gaussians;
|
||||
console.warn(`Unknown Unit.Kind value: ${unitKind}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Main class for processing MVS annotation */
|
||||
export class MVSAnnotation {
|
||||
@@ -202,7 +217,8 @@ export class MVSAnnotation {
|
||||
/** Return value of field `fieldName` assigned to location `loc`, if any */
|
||||
getValueForLocation(loc: StructureElement.Location, fieldName: string): string | undefined {
|
||||
const indexedModel = this.getIndexedModel(loc.unit.model, loc.unit.conformation.operator.instanceId);
|
||||
const iRow = (indexedModel !== null) ? indexedModel[loc.element] : -1;
|
||||
const indexedElements = getIndexedElementsForUnitKind(indexedModel, loc.unit.kind);
|
||||
const iRow = indexedElements ? indexedElements[loc.element] : -1;
|
||||
return this.getValueForRow(iRow, fieldName);
|
||||
}
|
||||
/** Return value of field `fieldName` assigned to `i`-th annotation row, if any */
|
||||
@@ -235,16 +251,22 @@ export class MVSAnnotation {
|
||||
private getRowForEachAtom(model: Model, instanceId: string): IndexedModel {
|
||||
const indices = IndicesAndSortings.get(model);
|
||||
const nAtoms = model.atomicHierarchy.atoms._rowCount;
|
||||
let result: IndexedModel = null;
|
||||
const nSpheres = model.coarseHierarchy.spheres.count;
|
||||
const nGaussians = model.coarseHierarchy.gaussians.count;
|
||||
let indexedAtoms: IndexedElements = null;
|
||||
let indexedSpheres: IndexedElements = null;
|
||||
let indexedGaussians: IndexedElements = null;
|
||||
const rows = this.getRows();
|
||||
for (let i = 0, nRows = rows.length; i < nRows; i++) {
|
||||
const row = rows[i];
|
||||
for (let iRow = 0, nRows = rows.length; iRow < nRows; iRow++) {
|
||||
const row = rows[iRow];
|
||||
const atomRanges = getAtomRangesForRow(row, model, instanceId, indices);
|
||||
if (AtomRanges.count(atomRanges) === 0) continue;
|
||||
result ??= Array(nAtoms).fill(-1);
|
||||
AtomRanges.foreach(atomRanges, (from, to) => result!.fill(i, from, to));
|
||||
indexedAtoms = fillValueOnRanges(indexedAtoms, nAtoms, atomRanges, iRow);
|
||||
const sphereRanges = getSphereRangesForRow(row, model, instanceId, indices);
|
||||
indexedSpheres = fillValueOnRanges(indexedSpheres, nSpheres, sphereRanges, iRow);
|
||||
const gaussianRanges = getGaussianRangesForRow(row, model, instanceId, indices);
|
||||
indexedGaussians = fillValueOnRanges(indexedGaussians, nGaussians, gaussianRanges, iRow);
|
||||
}
|
||||
return result;
|
||||
return { atoms: indexedAtoms, spheres: indexedSpheres, gaussians: indexedGaussians };
|
||||
}
|
||||
|
||||
/** Parse and return all annotation rows in this annotation, or return cached result if available */
|
||||
@@ -355,6 +377,7 @@ function getRowsFromCif(data: CifCategory, schema: MVSAnnotationSchema, fieldRem
|
||||
const columnArray = getArrayFromCifCategory(data, srcKey, cifSchema[key]); // Avoiding `column.toArray` as it replaces . and ? fields by 0 or ''
|
||||
if (columnArray) columns[key] = columnArray;
|
||||
}
|
||||
if (Object.keys(columns).length === 0) return new Array(data.rowCount).fill({});
|
||||
return objectOfArraysToArrayOfObjects(columns);
|
||||
}
|
||||
|
||||
@@ -437,3 +460,11 @@ function annotationSourceFromSpec(s: MVSAnnotationSpec): MVSAnnotationSource {
|
||||
return { kind: 'source-cif' };
|
||||
}
|
||||
}
|
||||
|
||||
/** In `array`, set value `fillValue` to all positions described by `fillRanges`. In case `array` is `null`, initialize it with length `n` prefilled with -1. */
|
||||
function fillValueOnRanges(array: IndexedElements, n: number, fillRanges: ElementRanges | undefined, fillValue: number): IndexedElements {
|
||||
if (!fillRanges || ElementRanges.count(fillRanges) === 0) return array;
|
||||
const out = array ?? Array(n).fill(-1);
|
||||
ElementRanges.foreach(fillRanges, (from, to) => out.fill(fillValue, from, to));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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 Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -75,7 +75,7 @@ function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme,
|
||||
break;
|
||||
case 'selection':
|
||||
const substructure = substructureFromSelector(structure, item.position.params.selector);
|
||||
const p = textPropsForSelection(substructure, theme.size.size, [{}]);
|
||||
const p = textPropsForSelection(substructure, [{}]);
|
||||
const group = serialIndexOfSubstructure(structure, substructure) ?? 0;
|
||||
if (p) builder.add(item.text, p.center[0], p.center[1], p.center[2], p.depth, p.scale, group);
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { murmurHash3_128_fromBytes } from '../../../mol-data/util';
|
||||
import { StringLike } from '../../../mol-io/common/string-like';
|
||||
import { DataFormatProvider } from '../../../mol-plugin-state/formats/provider';
|
||||
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
|
||||
@@ -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 : [] });
|
||||
}));
|
||||
|
||||
|
||||
@@ -112,16 +113,23 @@ export const MVSXFormatProvider: DataFormatProvider<{}, StateObjectRef<Mvs>, any
|
||||
* add all contained files to `plugin`'s asset manager,
|
||||
* and parse the main file in the archive as MVSJ.
|
||||
* Return parsed MVS data and `sourceUrl` for resolution of relative URIs. */
|
||||
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array, mainFilePath: string = 'index.mvsj'): Promise<{ mvsData: MVSData, sourceUrl: string }> {
|
||||
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array<ArrayBuffer>, mainFilePathOrOptions?: string | { mainFilePath?: string, doNotClearAssets?: boolean }): Promise<{ mvsData: MVSData, sourceUrl: string }> {
|
||||
// TODO: on next major version, streamline mainFilePathOrOptions
|
||||
if (typeof mainFilePathOrOptions === 'string') {
|
||||
mainFilePathOrOptions = { mainFilePath: mainFilePathOrOptions };
|
||||
}
|
||||
const mainFilePath = mainFilePathOrOptions?.mainFilePath ?? 'index.mvsj';
|
||||
const doNotClearAssets = mainFilePathOrOptions?.doNotClearAssets ?? false;
|
||||
|
||||
// Ensure at most one generation of MVSX file assets exists in the asset manager.
|
||||
// Hopefully, this is a reasonable compromise to ensure MVSX files work in multi-snapshot
|
||||
// states.
|
||||
clearMVSXFileAssets(plugin);
|
||||
if (!doNotClearAssets) clearMVSXFileAssets(plugin);
|
||||
|
||||
const archiveId = `ni,fnv1a;${hashFnv32a(data)}`;
|
||||
let files: { [path: string]: Uint8Array };
|
||||
const archiveId = `ni,MurmurHash3_128;${murmurHash3_128_fromBytes(data, 42)}`;
|
||||
let files: { [path: string]: Uint8Array<ArrayBuffer> };
|
||||
try {
|
||||
files = await unzip(runtimeCtx, data) as typeof files;
|
||||
files = await unzip(runtimeCtx, data.buffer) as typeof files;
|
||||
} catch (err) {
|
||||
plugin.log.error('Invalid MVSX file');
|
||||
throw err;
|
||||
@@ -138,7 +146,7 @@ export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext
|
||||
return { mvsData, sourceUrl };
|
||||
}
|
||||
|
||||
export async function loadMVSData(plugin: PluginContext, data: MVSData | StringLike | Uint8Array, format: 'mvsj' | 'mvsx', options?: MVSLoadOptions) {
|
||||
export async function loadMVSData(plugin: PluginContext, data: MVSData | StringLike | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: MVSLoadOptions) {
|
||||
if (typeof data === 'string' && data.startsWith('base64')) {
|
||||
data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array
|
||||
}
|
||||
@@ -160,7 +168,7 @@ export async function loadMVSData(plugin: PluginContext, data: MVSData | StringL
|
||||
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
|
||||
}
|
||||
await plugin.runTask(Task.create('Load MVSX file', async ctx => {
|
||||
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array);
|
||||
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array<ArrayBuffer>, { doNotClearAssets: options?.appendSnapshots });
|
||||
await loadMVS(plugin, parsed.mvsData, { sanityChecks: true, ...options, sourceUrl: parsed.sourceUrl });
|
||||
}));
|
||||
} else {
|
||||
@@ -182,7 +190,7 @@ function tryGetDownloadUrl(pso: SO.Data.String, plugin: PluginContext): string |
|
||||
}
|
||||
|
||||
/** Return a URI referencing a file within an archive, using ARCP scheme (https://arxiv.org/pdf/1809.06935.pdf).
|
||||
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,fnv1a;938511930')
|
||||
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,MurmurHash3_128;e6494f6be71f34c556f3de73d306780c')
|
||||
* `path` corresponds to the path to a file within the archive */
|
||||
function arcpUri(archiveId: string, path: string): string {
|
||||
return new URL(path, `arcp://${archiveId}/`).href;
|
||||
@@ -190,7 +198,7 @@ function arcpUri(archiveId: string, path: string): string {
|
||||
|
||||
/** Add a URL asset to asset manager.
|
||||
* Skip if an asset with the same URL already exists. */
|
||||
function ensureUrlAsset(manager: AssetManager, url: string, data: Uint8Array, options?: { isFile?: boolean }) {
|
||||
function ensureUrlAsset(manager: AssetManager, url: string, data: Uint8Array<ArrayBuffer>, options?: { isFile?: boolean }) {
|
||||
const asset = Asset.getUrlAsset(manager, url);
|
||||
if (!manager.has(asset)) {
|
||||
const filename = url.split('/').pop() ?? '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 Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -72,7 +72,7 @@ export const DefaultMultilayerColorThemeProps: MultilayerColorThemeProps = { lay
|
||||
* If a nested theme provider has `ensureCustomProperties` methods, these will not be called automatically
|
||||
* (the caller must ensure that any required custom properties be attached). */
|
||||
function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry): ColorTheme<MultilayerColorThemeParams> {
|
||||
const { colorLayers, granularity } = makeLayers(ctx, props, colorThemeRegistry);
|
||||
const { colorLayers, granularity, preferSmoothing } = makeLayers(ctx, props, colorThemeRegistry);
|
||||
|
||||
function structureElementColor(loc: StructureElement.Location, isSecondary: boolean): Color {
|
||||
for (const layer of colorLayers) {
|
||||
@@ -101,7 +101,7 @@ function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorT
|
||||
return {
|
||||
factory: (ctx_, props_) => makeMultilayerColorTheme(ctx_, props_, colorThemeRegistry),
|
||||
granularity,
|
||||
preferSmoothing: true,
|
||||
preferSmoothing,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Combines colors from multiple color themes.',
|
||||
@@ -136,6 +136,7 @@ interface ColorLayer {
|
||||
function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry) {
|
||||
const colorLayers: ColorLayer[] = [];
|
||||
let granularityFlags = 0;
|
||||
let preferSmoothing = false;
|
||||
for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from the end to get top layer first, bottom layer last
|
||||
const layer = props.layers[i];
|
||||
const themeProvider = colorThemeRegistry.get(layer.theme.name);
|
||||
@@ -175,8 +176,9 @@ function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, col
|
||||
default:
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`);
|
||||
}
|
||||
if (theme.preferSmoothing) preferSmoothing = true;
|
||||
}
|
||||
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags) };
|
||||
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags), preferSmoothing };
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,14 +33,17 @@ import { Task } from '../../../mol-task';
|
||||
import { round } from '../../../mol-util';
|
||||
import { range } from '../../../mol-util/array';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { MarkerActions } from '../../../mol-util/marker-action';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { capitalize } from '../../../mol-util/string';
|
||||
import { rowsToExpression, rowToExpression } from '../helpers/selections';
|
||||
import { collectMVSReferences, decodeColor, isDefined } from '../helpers/utils';
|
||||
import { MolstarNode, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSNode } from '../tree/mvs/mvs-tree';
|
||||
import { addParamDefaults } from '../tree/generic/params-schema';
|
||||
import { treeValidationIssues } from '../tree/generic/tree-validation';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSNode, MVSTreeSchema } from '../tree/mvs/mvs-tree';
|
||||
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
@@ -64,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',
|
||||
@@ -80,15 +89,35 @@ export const MVSDownloadPrimitiveData = MVSTransform({
|
||||
const url = Asset.getUrlAsset(plugin.managers.asset, params.uri);
|
||||
const asset = await plugin.managers.asset.resolve(url, 'string').runInContext(ctx);
|
||||
const node = JSON.parse(StringLike.toString(asset.data)) as MolstarSubtree<'primitives'>;
|
||||
const validationIssues = treeValidationIssues(MVSTreeSchema, node, { anyRoot: true });
|
||||
if (validationIssues) {
|
||||
throw new Error(`Invalid primitive data from ${params.uri}:\n${validationIssues.join('\n')}`);
|
||||
}
|
||||
if (node.kind !== 'primitives') {
|
||||
throw new Error(`Expected primitives node from ${params.uri}, got ${node.kind}`);
|
||||
}
|
||||
const nodeWithDefaults: MolstarSubtree<'primitives'> = {
|
||||
...node,
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitives.params, node.params || {}),
|
||||
children: node.children?.map((child: any) => {
|
||||
if (child.kind === 'primitive') {
|
||||
return {
|
||||
...child,
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitive.params, child.params || {})
|
||||
};
|
||||
}
|
||||
return child;
|
||||
})
|
||||
};
|
||||
(cache as any).asset = asset;
|
||||
return new MVSPrimitivesData({
|
||||
node,
|
||||
node: nodeWithDefaults,
|
||||
defaultStructure: SO.Molecule.Structure.is(a) ? a.data : undefined,
|
||||
structureRefs: {},
|
||||
primitives: getPrimitives(node),
|
||||
options: { ...node.params },
|
||||
primitives: getPrimitives(nodeWithDefaults),
|
||||
options: { ...nodeWithDefaults.params },
|
||||
positionCache: new Map(),
|
||||
instances: getInstances(node.params),
|
||||
instances: getInstances(nodeWithDefaults.params),
|
||||
}, { label: 'Primitive Data' });
|
||||
});
|
||||
},
|
||||
@@ -97,6 +126,16 @@ export const MVSDownloadPrimitiveData = MVSTransform({
|
||||
},
|
||||
});
|
||||
|
||||
/* Cannot use MolstarSubtree<'primitives'>> because information about type of children would be lost and cause TypeScript errors in dependent code */
|
||||
interface PrimitivesSubtree {
|
||||
kind: 'primitives',
|
||||
params: MolstarNodeParams<'primitives'>,
|
||||
children?: {
|
||||
kind: 'primitive',
|
||||
params: MolstarNodeParams<'primitive'>,
|
||||
}[],
|
||||
}
|
||||
|
||||
export type MVSInlinePrimitiveData = typeof MVSInlinePrimitiveData
|
||||
export const MVSInlinePrimitiveData = MVSTransform({
|
||||
name: 'mvs-inline-primitive-data',
|
||||
@@ -104,7 +143,10 @@ export const MVSInlinePrimitiveData = MVSTransform({
|
||||
from: [SO.Root, SO.Molecule.Structure],
|
||||
to: MVSPrimitivesData,
|
||||
params: {
|
||||
node: PD.Value<MolstarSubtree<'primitives'>>(undefined as any, { isHidden: true }),
|
||||
node: PD.Value<PrimitivesSubtree>({
|
||||
kind: 'primitives',
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitives.params, {}),
|
||||
}, { isHidden: true }),
|
||||
},
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
@@ -127,7 +169,8 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
from: MVSPrimitivesData,
|
||||
to: SO.Shape.Provider,
|
||||
params: {
|
||||
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh')
|
||||
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh'),
|
||||
clip: PD.Value<Clip.Props | undefined>(undefined, { isHidden: true })
|
||||
}
|
||||
})({
|
||||
apply({ a, params, dependencies }) {
|
||||
@@ -135,17 +178,20 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
const context: PrimitiveBuilderContext = { ...a.data, structureRefs };
|
||||
|
||||
const snapshotKey = { snapshotKey: { ...SnapshotKey, defaultValue: a.data.options?.snapshot_key ?? '' } };
|
||||
const markdownCommands = { markdownCommands: { ...MarkdownCommands, defaultValue: a.data.node?.custom?.molstar_markdown_commands } };
|
||||
|
||||
const label = capitalize(params.kind);
|
||||
if (params.kind === 'mesh') {
|
||||
if (!hasPrimitiveKind(a.data, 'mesh')) return StateObject.Null;
|
||||
|
||||
const customMeshParams = a.data.node.custom?.molstar_mesh_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: {
|
||||
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customMeshParams }),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveMesh(data, prev?.geometry),
|
||||
geometryUtils: Mesh.Utils,
|
||||
@@ -155,6 +201,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
|
||||
const options = a.data.options;
|
||||
const bgColor = options?.label_background_color;
|
||||
const customLabelParams = a.data.node.custom?.molstar_label_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
@@ -166,8 +213,11 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
tetherLength: options?.label_tether_length ?? 1,
|
||||
background: isDefined(bgColor),
|
||||
backgroundColor: isDefined(bgColor) ? decodeColor(bgColor) : undefined,
|
||||
clip: params.clip,
|
||||
...customLabelParams,
|
||||
}),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, props, prev: any) => buildPrimitiveLabels(data, prev?.geometry, props),
|
||||
geometryUtils: Text.Utils,
|
||||
@@ -175,12 +225,14 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
} else if (params.kind === 'lines') {
|
||||
if (!hasPrimitiveKind(a.data, 'line')) return StateObject.Null;
|
||||
|
||||
const customLineParams = a.data.node.custom?.molstar_line_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: {
|
||||
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customLineParams }),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveLines(data, prev?.geometry),
|
||||
geometryUtils: Lines.Utils,
|
||||
@@ -209,7 +261,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
|
||||
const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils);
|
||||
await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
|
||||
|
||||
const pickable = !!(params as any).snapshotKey?.trim();
|
||||
const pickable = !!(params as any).snapshotKey?.trim() || !!(params as any).markdownCommands;
|
||||
if (pickable) {
|
||||
repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
|
||||
}
|
||||
@@ -223,7 +275,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
|
||||
await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx);
|
||||
b.data.sourceData = a.data;
|
||||
|
||||
const pickable = !!(newParams as any).snapshotKey?.trim();
|
||||
const pickable = !!(newParams as any).snapshotKey?.trim() || !!(newParams as any).markdownCommands;
|
||||
if (pickable) {
|
||||
b.data.repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
|
||||
}
|
||||
@@ -234,6 +286,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
|
||||
});
|
||||
|
||||
const SnapshotKey = PD.Text('', { isEssential: true, disableInteractiveUpdates: true, description: 'Activate the snapshot with the provided key when clicking on the label' });
|
||||
const MarkdownCommands = PD.Value<any>(undefined, { isHidden: true });
|
||||
|
||||
/* **************************************************** */
|
||||
|
||||
@@ -558,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,
|
||||
@@ -591,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,
|
||||
@@ -626,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,
|
||||
|
||||
36
src/extensions/mvs/components/trajectory.ts
Normal file
36
src/extensions/mvs/components/trajectory.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { getTrajectory } from '../../../mol-plugin-state/transforms/model';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { ParamDefinition } from '../../../mol-util/param-definition';
|
||||
import { getMVSReferenceObject } from '../helpers/utils';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
export const MVSTrajectoryWithCoordinates = MVSTransform({
|
||||
name: 'trajectory-with-coordinates',
|
||||
display: { name: 'Trajectory with Coordinates', description: 'Create a trajectory from existing model and the provided coordinates.' },
|
||||
from: [PluginStateObject.Molecule.Model, PluginStateObject.Molecule.Topology],
|
||||
to: PluginStateObject.Molecule.Trajectory,
|
||||
params: {
|
||||
coordinatesRef: ParamDefinition.Text('', { isHidden: true }),
|
||||
}
|
||||
})({
|
||||
apply({ a, params, dependencies }) {
|
||||
return Task.create('Create trajectory from model/topology and coordinates', async ctx => {
|
||||
const coordinates = getMVSReferenceObject([PluginStateObject.Molecule.Coordinates], dependencies, params.coordinatesRef);
|
||||
|
||||
if (!coordinates) {
|
||||
throw new Error('Coordinates not found.');
|
||||
}
|
||||
|
||||
const trajectory = await getTrajectory(ctx, a, coordinates.data);
|
||||
const props = { label: 'Trajectory', description: `${trajectory.frameCount} model${trajectory.frameCount === 1 ? '' : 's'}` };
|
||||
return new PluginStateObject.Molecule.Trajectory(trajectory, props);
|
||||
});
|
||||
}
|
||||
});
|
||||
26
src/extensions/mvs/export.ts
Normal file
26
src/extensions/mvs/export.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Zip } from '../../mol-util/zip/zip';
|
||||
import { MVSData } from './mvs-data';
|
||||
|
||||
/**
|
||||
* Creates an MVSX zip file with from the provided data and assets
|
||||
*/
|
||||
export async function createMVSX(data: MVSData, assets: { name: string, content: string | Uint8Array<ArrayBuffer> }[]) {
|
||||
const encoder = new TextEncoder();
|
||||
const files: Record<string, Uint8Array<ArrayBuffer>> = {
|
||||
'index.mvsj': encoder.encode(JSON.stringify(data)),
|
||||
};
|
||||
for (const asset of assets) {
|
||||
files[asset.name] = typeof asset.content === 'string'
|
||||
? encoder.encode(asset.content)
|
||||
: asset.content;
|
||||
}
|
||||
|
||||
const zip = await Zip(files).run();
|
||||
return new Uint8Array(zip);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { AtomRanges } from '../atom-ranges';
|
||||
import { ElementRanges } from '../element-ranges';
|
||||
|
||||
|
||||
describe('union', () => {
|
||||
@@ -12,39 +12,39 @@ describe('union', () => {
|
||||
const a = {
|
||||
from: [0, 20, 40, 60, 80],
|
||||
to: [10, 30, 50, 70, 90],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const b = {
|
||||
from: [11, 37, 51, 205],
|
||||
to: [15, 39, 55, 210],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const c = {
|
||||
from: [-10, 200, 300],
|
||||
to: [-5, 202, 305],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const result = {
|
||||
from: [-10, 0, 11, 20, 37, 40, 51, 60, 80, 200, 205, 300],
|
||||
to: [-5, 10, 15, 30, 39, 50, 55, 70, 90, 202, 210, 305],
|
||||
} as AtomRanges;
|
||||
expect(AtomRanges.union([a, b, c])).toEqual(result);
|
||||
} as ElementRanges;
|
||||
expect(ElementRanges.union([a, b, c])).toEqual(result);
|
||||
});
|
||||
it('union overlapping', async () => {
|
||||
const a = {
|
||||
from: [0, 20, 40, 60, 80],
|
||||
to: [10, 30, 50, 70, 90],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const b = {
|
||||
from: [10, 37, 51, 84, 205],
|
||||
to: [15, 40, 55, 88, 220],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const c = {
|
||||
from: [-10, 67, 200, 300],
|
||||
to: [5, 80, 210, 305],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const result = {
|
||||
from: [-10, 20, 37, 51, 60, 200, 300],
|
||||
to: [15, 30, 50, 55, 90, 220, 305],
|
||||
} as AtomRanges;
|
||||
expect(AtomRanges.union([a, b, c])).toEqual(result);
|
||||
} as ElementRanges;
|
||||
expect(ElementRanges.union([a, b, c])).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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([
|
||||
674
src/extensions/mvs/helpers/animation.ts
Normal file
674
src/extensions/mvs/helpers/animation.ts
Normal file
@@ -0,0 +1,674 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import * as EasingFns from '../../../mol-math/easing';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { EPSILON, Mat3, Mat4, Quat, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { decodeColor } from '../../../mol-util/color/utils';
|
||||
import { produce } from '../../../mol-util/produce';
|
||||
import { makeContinuousPaletteCheckpoints, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from '../components/annotation-color-theme';
|
||||
import { palettePropsFromMVSPalette } from '../load-helpers';
|
||||
import { Snapshot } from '../mvs-data';
|
||||
import { MVSAnimationEasing, MVSAnimationNode, MVSAnimationSchema } from '../tree/animation/animation-tree';
|
||||
import { Tree } from '../tree/generic/tree-schema';
|
||||
import { addDefaults } from '../tree/generic/tree-utils';
|
||||
import { MVSTree } from '../tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
|
||||
export async function generateStateTransition(ctx: RuntimeContext, snapshot: Snapshot, snapshotIndex: number, snapshotCount: number) {
|
||||
if (!snapshot.animation) return undefined;
|
||||
|
||||
const tree = addDefaults(snapshot.animation, MVSAnimationSchema);
|
||||
const transitions = tree.children?.filter(child => child.kind === 'interpolate');
|
||||
if (!transitions?.length) return undefined;
|
||||
|
||||
const duration = Math.max(
|
||||
snapshot.animation.params?.duration_ms ?? 0,
|
||||
...transitions.map(t => (t.params.start_ms ?? 0) + t.params.duration_ms)
|
||||
);
|
||||
|
||||
const frames: [tree: MVSTree, time: number][] = [];
|
||||
const dt = tree.params?.frame_time_ms ?? (1000 / 60);
|
||||
const N = Math.ceil(duration / dt);
|
||||
|
||||
const nodeMap = makeNodeMap(snapshot.root, new Map(), []);
|
||||
const cache = new Map<any, InterpolationCacheEntry>();
|
||||
|
||||
const transitionGroups = groupTranstions(transitions);
|
||||
|
||||
let prevRoot: MVSTree | undefined;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
const t = i * dt;
|
||||
const root = createSnapshot(snapshot.root, transitionGroups, t, cache, nodeMap);
|
||||
|
||||
if (root === prevRoot || (prevRoot && deepEqual(root, prevRoot))) {
|
||||
frames[frames.length - 1][1] += dt;
|
||||
} else {
|
||||
frames.push([root, dt]);
|
||||
}
|
||||
|
||||
prevRoot = root;
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Generating transition for snapshot ${snapshotIndex + 1}/${snapshotCount}`, current: i + 1, max: N });
|
||||
}
|
||||
}
|
||||
|
||||
return { tree, frametimeMs: dt, frames };
|
||||
}
|
||||
|
||||
const EasingFnMap: Record<MVSAnimationEasing, (t: number) => number> = {
|
||||
'linear': t => t,
|
||||
'bounce-in': EasingFns.bounceIn,
|
||||
'bounce-out': EasingFns.bounceOut,
|
||||
'bounce-in-out': EasingFns.bounceInOut,
|
||||
'circle-in': EasingFns.circleIn,
|
||||
'circle-out': EasingFns.circleOut,
|
||||
'circle-in-out': EasingFns.circleInOut,
|
||||
'cubic-in': EasingFns.cubicIn,
|
||||
'cubic-out': EasingFns.cubicOut,
|
||||
'cubic-in-out': EasingFns.cubicInOut,
|
||||
'exp-in': EasingFns.expIn,
|
||||
'exp-out': EasingFns.expOut,
|
||||
'exp-in-out': EasingFns.expInOut,
|
||||
'quad-in': EasingFns.quadIn,
|
||||
'quad-out': EasingFns.quadOut,
|
||||
'quad-in-out': EasingFns.quadInOut,
|
||||
'sin-in': EasingFns.sinIn,
|
||||
'sin-out': EasingFns.sinOut,
|
||||
'sin-in-out': EasingFns.sinInOut,
|
||||
};
|
||||
|
||||
interface InterpolationCacheEntry {
|
||||
paletteFn?: (value: number) => Color,
|
||||
startColor?: Color | Record<number | string, Color>,
|
||||
endColor?: Color | Record<number | string, Color>,
|
||||
rotation?: { axis: Vec3, angle: number, start: Quat, end: Quat },
|
||||
}
|
||||
|
||||
function getTransitionKey(transition: MVSAnimationNode<'interpolate'>) {
|
||||
const prop = transition.params.property;
|
||||
if (Array.isArray(prop)) {
|
||||
return `${transition.params.target_ref}:${prop.join('.')}`;
|
||||
}
|
||||
return `${transition.params.target_ref}:${prop}`;
|
||||
}
|
||||
|
||||
function groupTranstions(transitions: MVSAnimationNode<'interpolate'>[]) {
|
||||
const map = new Map<string, MVSAnimationNode<'interpolate'>[]>();
|
||||
const groups: MVSAnimationNode<'interpolate'>[][] = [];
|
||||
for (const t of transitions) {
|
||||
const key = getTransitionKey(t);
|
||||
if (!map.has(key)) {
|
||||
const group: MVSAnimationNode<'interpolate'>[] = [];
|
||||
map.set(key, group);
|
||||
groups.push(group);
|
||||
}
|
||||
map.get(key)!.push(t);
|
||||
}
|
||||
for (const group of groups) {
|
||||
group.sort((a, b) => {
|
||||
const s = (a.params.start_ms ?? 0) - (b.params.start_ms ?? 0);
|
||||
if (s !== 0) return s;
|
||||
return a.params.duration_ms - b.params.duration_ms;
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function createSnapshot(tree: MVSTree, transitionGroups: MVSAnimationNode<'interpolate'>[][], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
|
||||
let modified = false;
|
||||
const ret = produce(tree, (draft) => {
|
||||
for (const transitionGroup of transitionGroups) {
|
||||
|
||||
const pivot = transitionGroup[0];
|
||||
const nodePath = nodeMap.get(pivot.params.target_ref);
|
||||
if (!nodePath) continue;
|
||||
|
||||
const node = select(draft, nodePath, 0);
|
||||
const target = pivot.params.property[0] === 'custom' ? node?.custom : node?.params;
|
||||
if (!target) continue;
|
||||
|
||||
|
||||
const offset = pivot.params.property[0] === 'custom' ? 1 : 0;
|
||||
|
||||
let transition: MVSAnimationNode<'interpolate'> = pivot;
|
||||
let previous: MVSAnimationNode<'interpolate'> | undefined;
|
||||
|
||||
for (let i = transitionGroup.length - 1; i > 0; i--) {
|
||||
const current = transitionGroup[i];
|
||||
const currentStart = current.params.start_ms ?? 0;
|
||||
if (time >= currentStart) {
|
||||
transition = current;
|
||||
previous = i > 0 ? transitionGroup[i - 1] : undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.has(transition)) {
|
||||
cache.set(transition, {});
|
||||
}
|
||||
|
||||
const cacheEntry: InterpolationCacheEntry = cache.get(transition)!;
|
||||
|
||||
const startTime: number = transition.params.start_ms ?? 0;
|
||||
const durationMs: number = transition.params.duration_ms ?? 0;
|
||||
const t = (time - startTime) / durationMs;
|
||||
|
||||
let next: any;
|
||||
if (transition.params.kind === 'transform_matrix') {
|
||||
next = processTransformMatrix(transition, target, clamp(t, 0, 1), cacheEntry, offset, previous);
|
||||
} else {
|
||||
next = processScalarLike(transition, target, t, cacheEntry, offset, previous);
|
||||
}
|
||||
|
||||
if (next === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modified = true;
|
||||
assign(target, transition.params.property, next, offset);
|
||||
}
|
||||
});
|
||||
return modified ? ret : tree;
|
||||
}
|
||||
|
||||
function applyFrequency(t: number, frequency: number, alternate: boolean) {
|
||||
let v = (t * (frequency || 1));
|
||||
if (v < 1) return v;
|
||||
|
||||
if (!alternate) {
|
||||
v = (v % 1);
|
||||
if (v === 0) return 1;
|
||||
return v;
|
||||
}
|
||||
|
||||
if (Math.abs(v - 1) < EPSILON) return 1;
|
||||
v = v % 2;
|
||||
if (v > 1) return 2 - v;
|
||||
return v;
|
||||
}
|
||||
|
||||
function getPreviousScalarEnd(previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (!previous || previous.params.kind === 'transform_matrix') return undefined;
|
||||
return previous.params.end;
|
||||
}
|
||||
|
||||
function processScalarLike(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cacheEntry: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind === 'transform_matrix') return;
|
||||
if (previous && previous.params.kind === 'transform_matrix') return;
|
||||
|
||||
const startValue = transition.params.start ?? getPreviousScalarEnd(previous) ?? select(target, transition.params.property, offset);
|
||||
if (transition.params.kind === 'color' && !cacheEntry.paletteFn) {
|
||||
cacheEntry.paletteFn = makePaletteFunction(transition);
|
||||
}
|
||||
|
||||
const endValue: any = transition.params.end;
|
||||
|
||||
if (time <= 0) return startValue;
|
||||
else if (time >= 1 - EPSILON && !transition.params.alternate_direction && transition.params.kind !== 'color') return endValue;
|
||||
|
||||
let t = clamp(time, 0, 1);
|
||||
t = applyFrequency(t, transition.params.frequency ?? 1, !!transition.params.alternate_direction);
|
||||
|
||||
const easing = EasingFnMap[transition.params.easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
t = easing(t);
|
||||
|
||||
if (transition.params.kind === 'scalar') {
|
||||
return interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.discrete);
|
||||
} else if (transition.params.kind === 'vec3') {
|
||||
return interpolateVectors(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.spherical);
|
||||
} else if (transition.params.kind === 'rotation_matrix') {
|
||||
return interpolateRotation(startValue, endValue, t, transition.params.noise_magnitude ?? 0, cacheEntry);
|
||||
} else if (transition.params.kind === 'color') {
|
||||
if (cacheEntry.paletteFn) {
|
||||
const color = cacheEntry.paletteFn(t);
|
||||
return Color.toHexStyle(color);
|
||||
}
|
||||
|
||||
const baseColors = typeof startValue === 'object' ? select(target, transition.params.property, offset) : undefined;
|
||||
return interpolateColors(startValue, endValue, t, cacheEntry, baseColors);
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviousMatrixEnd(previous: MVSAnimationNode<'interpolate'> | undefined, prop: 'rotation_start' | 'translation_start' | 'scale_start') {
|
||||
if (!previous || previous.params.kind !== 'transform_matrix') return undefined;
|
||||
return previous.params[prop];
|
||||
}
|
||||
|
||||
const TransformState = {
|
||||
pivotTranslation: Mat4(),
|
||||
pivotTranslationInv: Mat4(),
|
||||
rotation: Mat4(),
|
||||
scale: Mat4(),
|
||||
translation: Mat4(),
|
||||
pivotNeg: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cache: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind !== 'transform_matrix') return;
|
||||
if (previous && previous.params.kind !== 'transform_matrix') return;
|
||||
|
||||
const transform = select(target, transition.params.property, offset) ?? Mat4.identity();
|
||||
|
||||
const startRotation = transition.params.rotation_start ?? getPreviousMatrixEnd(previous, 'rotation_start') ?? Mat3.fromMat4(Mat3(), transform);
|
||||
const startTranslation = transition.params.translation_start ?? getPreviousMatrixEnd(previous, 'translation_start') ?? Mat4.getTranslation(Vec3(), transform);
|
||||
const startScale = transition.params.scale_start ?? getPreviousMatrixEnd(previous, 'scale_start') ?? Mat4.getScaling(Vec3(), transform);
|
||||
|
||||
const endRotation = transition.params.rotation_end;
|
||||
const endTranslation = transition.params.translation_end;
|
||||
const endScale = transition.params.scale_end;
|
||||
|
||||
let rotation, translation, scale;
|
||||
|
||||
if (time <= 0) {
|
||||
rotation = startRotation as Mat3;
|
||||
translation = startTranslation as Vec3;
|
||||
scale = startScale as Vec3;
|
||||
} else {
|
||||
const clampedTime = clamp(time, 0, 1);
|
||||
|
||||
let t = applyFrequency(clampedTime, transition.params.rotation_frequency ?? 1, !!transition.params.rotation_alternate_direction);
|
||||
let easing = EasingFnMap[transition.params.rotation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
rotation = interpolateRotation(startRotation as Mat3, endRotation as Mat3, easing(t), transition.params.rotation_noise_magnitude ?? 0, cache);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.translation_frequency ?? 1, !!transition.params.translation_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.translation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
translation = interpolateVec3(startTranslation as Vec3, endTranslation as Vec3 | undefined, easing(t), transition.params.translation_noise_magnitude ?? 0, false);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.scale_frequency ?? 1, !!transition.params.scale_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.scale_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
scale = interpolateVec3(startScale as Vec3, endScale as Vec3 | undefined, easing(t), transition.params.scale_noise_magnitude ?? 0, false);
|
||||
}
|
||||
|
||||
const pivot = transition.params.pivot ?? Vec3.zero();
|
||||
|
||||
Mat4.fromTranslation(TransformState.translation, translation);
|
||||
Mat4.fromScaling(TransformState.scale, scale);
|
||||
Mat4.setIdentity(TransformState.rotation);
|
||||
Mat4.fromMat3(TransformState.rotation, rotation);
|
||||
Mat4.fromTranslation(TransformState.pivotTranslation, pivot as Vec3);
|
||||
Mat4.fromTranslation(TransformState.pivotTranslationInv, Vec3.negate(TransformState.pivotNeg, pivot as Vec3));
|
||||
|
||||
// translation . pivot . rotation . scale . pivotInv
|
||||
const result = Mat4();
|
||||
Mat4.mul(result, TransformState.scale, TransformState.pivotTranslationInv);
|
||||
Mat4.mul(result, TransformState.rotation, result);
|
||||
Mat4.mul(result, TransformState.translation, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function interpolateScalars(start: number | number[], end: number | number[] | undefined, t: number, noise: number, discrete: boolean) {
|
||||
if (Array.isArray(start)) {
|
||||
const ret = Array.from<number>({ length: start.length }).fill(0.1);
|
||||
if (!end || !Array.isArray(end)) {
|
||||
for (let i = 0; i < start.length; i++) {
|
||||
ret[i] = interpolateScalar(start[i], end, t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let i = 0; i < start.length; i++) {
|
||||
ret[i] = interpolateScalar(start[i], end[i], t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (Array.isArray(end)) {
|
||||
const ret = Array.from<number>({ length: end.length }).fill(0.1);
|
||||
for (let i = 0; i < end.length; i++) {
|
||||
ret[i] = interpolateScalar(start, end[i], t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return interpolateScalar(start, end, t, noise, discrete);
|
||||
}
|
||||
|
||||
function interpolateScalar(start: number, end: number | undefined, t: number, noise: number, discrete: boolean) {
|
||||
let v = typeof end === 'number' ? lerp(start, end, t) : start;
|
||||
if (noise) {
|
||||
v += (Math.random() - 0.5) * noise;
|
||||
}
|
||||
if (discrete) {
|
||||
v = Math.round(v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const InterpolateVectorsState = {
|
||||
start: Vec3(),
|
||||
end: Vec3(),
|
||||
v: Vec3(),
|
||||
};
|
||||
function interpolateVectors(start: number[], end: number[] | undefined, t: number, noise: number, isSpherical: boolean) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
const ret: number[] = Array.from<number>({ length: start.length }).fill(0.1);
|
||||
|
||||
for (let i = 0; i < start.length; i += 3) {
|
||||
const s = Vec3.fromArray(InterpolateVectorsState.start, start, i);
|
||||
|
||||
let v: Vec3;
|
||||
if (end) {
|
||||
const e = Vec3.fromArray(InterpolateVectorsState.end, end, i);
|
||||
v = isSpherical
|
||||
? Vec3.slerp(InterpolateVectorsState.v, s, e, t)
|
||||
: Vec3.lerp(InterpolateVectorsState.v, s, e, t);
|
||||
} else {
|
||||
v = Vec3.clone(s);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(Vec3Noise, noise);
|
||||
Vec3.add(v, v, Vec3Noise);
|
||||
}
|
||||
|
||||
Vec3.toArray(v, ret, i);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const Vec3Noise = Vec3();
|
||||
function interpolateVec3(start: Vec3, end: Vec3 | undefined, t: number, noise: number, isSpherical: boolean) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
let v: Vec3;
|
||||
|
||||
if (end) {
|
||||
v = isSpherical
|
||||
? Vec3.slerp(Vec3(), start, end, t)
|
||||
: Vec3.lerp(Vec3(), start, end, t);
|
||||
} else {
|
||||
v = Vec3.clone(start);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(Vec3Noise, noise);
|
||||
Vec3.add(v, v, Vec3Noise);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const RotationState = {
|
||||
start: Quat(),
|
||||
end: Quat(),
|
||||
v: Quat(),
|
||||
noise: Quat(),
|
||||
axis: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function interpolateRotation(start: Mat3, end: Mat3 | undefined, t: number, noise: number, cache: InterpolationCacheEntry) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
if (end) {
|
||||
if (!cache.rotation) {
|
||||
cache.rotation = {
|
||||
...relativeAxisAngle(start, end),
|
||||
start: Quat.fromMat3(Quat(), start),
|
||||
end: Quat.fromMat3(Quat(), end),
|
||||
};
|
||||
}
|
||||
|
||||
const { axis, angle, start: startQ, end: endQ } = cache.rotation;
|
||||
|
||||
if (angle < 1e-6) {
|
||||
// start ≈ end: make a clean spin about the detected (or default) axis
|
||||
Quat.setAxisAngle(RotationState.v, axis, t * 2 * Math.PI); // Make a full turn
|
||||
} else {
|
||||
// Normal case: stick with your existing slerp between start/end
|
||||
Quat.slerp(RotationState.v, startQ, endQ, t);
|
||||
}
|
||||
} else {
|
||||
Quat.fromMat3(RotationState.v, start);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(RotationState.axis, 1);
|
||||
Quat.setAxisAngle(RotationState.noise, RotationState.axis, 2 * Math.PI * noise * (Math.random() - 0.5));
|
||||
Quat.multiply(RotationState.v, RotationState.noise, RotationState.v);
|
||||
}
|
||||
Mat4.fromQuat(RotationState.temp, RotationState.v);
|
||||
return Mat3.fromMat4(Mat3(), RotationState.temp);
|
||||
}
|
||||
|
||||
function decodeColors(color: ColorT | Record<number | string, ColorT> | undefined, baseColors: Record<number | string, ColorT> | undefined) {
|
||||
if (color === undefined || color === null) return undefined;
|
||||
|
||||
if (typeof color === 'object') {
|
||||
const ret: Record<number | string, Color> = {};
|
||||
if (baseColors) {
|
||||
for (const key of Object.keys(baseColors)) {
|
||||
const decoded = decodeColor(baseColors[key]);
|
||||
if (decoded !== undefined) {
|
||||
ret[key] = decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(color)) {
|
||||
const decoded = decodeColor(color[key]);
|
||||
if (decoded !== undefined) {
|
||||
ret[key] = decoded;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return decodeColor(color);
|
||||
}
|
||||
|
||||
function interpolateColors(start: ColorT | Record<number, ColorT>, end: ColorT | Record<number, ColorT> | undefined, time: number, cacheEntry: InterpolationCacheEntry, baseColors: Record<number, ColorT> | undefined) {
|
||||
const t = clamp(time, 0, 1);
|
||||
|
||||
if (cacheEntry.paletteFn) {
|
||||
const c = cacheEntry.paletteFn(t);
|
||||
return Color.toHexStyle(c);
|
||||
}
|
||||
|
||||
if (cacheEntry.startColor === undefined) {
|
||||
cacheEntry.startColor = decodeColors(start, baseColors);
|
||||
}
|
||||
if (cacheEntry.endColor === undefined) {
|
||||
cacheEntry.endColor = decodeColors(end, undefined);
|
||||
}
|
||||
|
||||
const { startColor, endColor } = cacheEntry;
|
||||
|
||||
if (typeof startColor === 'object') {
|
||||
if (typeof baseColors !== 'object') {
|
||||
throw new Error('Cannot interpolate from scalar color to color mapping');
|
||||
}
|
||||
|
||||
const ret = { ...baseColors as any, ...startColor as any };
|
||||
if (typeof endColor === 'object') {
|
||||
for (const key of Object.keys(endColor)) {
|
||||
ret[key] = Color.toHexStyle(Color.interpolate(startColor[key], endColor[key], t));
|
||||
}
|
||||
} else if (typeof endColor === 'number') {
|
||||
for (const key of Object.keys(startColor)) {
|
||||
ret[key] = Color.toHexStyle(Color.interpolate(startColor[key], endColor, t));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
if (typeof endColor === 'object') {
|
||||
throw new Error('Cannot interpolate from scalar color to color mapping');
|
||||
}
|
||||
|
||||
if (typeof endColor === 'number' && typeof startColor === 'number') {
|
||||
return Color.toHexStyle(Color.interpolate(startColor, endColor, t));
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
function select(params: any, path: string | (string | number)[], offset: number) {
|
||||
if (typeof path === 'string') {
|
||||
return params?.[path];
|
||||
}
|
||||
|
||||
let f = params;
|
||||
for (let i = offset; i < path.length; i++) {
|
||||
if (!f) break;
|
||||
f = f[path[i]];
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
function assign(params: any, path: string | (string | number)[], value: any, offset: number) {
|
||||
if (!params) return;
|
||||
|
||||
if (typeof path === 'string') {
|
||||
params[path] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
let f = params;
|
||||
for (let i = offset; i < path.length; i++) {
|
||||
if (!f) break;
|
||||
if (i === path.length - 1) {
|
||||
f[path[i]] = value;
|
||||
} else {
|
||||
f = f[path[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeNodeMap(tree: Tree, map: Map<string, (string | number)[]>, currentPath: (string | number)[]) {
|
||||
if (tree.ref) {
|
||||
map.set(tree.ref, [...currentPath]);
|
||||
}
|
||||
|
||||
if (!tree.children) return map;
|
||||
|
||||
currentPath.push('children');
|
||||
for (let i = 0; i < tree.children.length; i++) {
|
||||
const child = tree.children[i];
|
||||
currentPath.push(i);
|
||||
makeNodeMap(child, map, currentPath);
|
||||
currentPath.pop();
|
||||
}
|
||||
currentPath.pop();
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function makePaletteFunction(props: MVSAnimationNode<'interpolate'>): ((value: number) => Color) | undefined {
|
||||
if (props.params.kind !== 'color' || !props.params.palette) return undefined;
|
||||
|
||||
const params = palettePropsFromMVSPalette(props.params.palette);
|
||||
if (params.name === 'discrete') return makePaletteFunctionDiscrete(params.params);
|
||||
if (params.name === 'continuous') return makePaletteFunctionContinuous(params.params);
|
||||
throw new Error(`NotImplementedError: makePaletteFunction for ${(props as any).name}`);
|
||||
}
|
||||
|
||||
|
||||
function makePaletteFunctionDiscrete(props: MVSDiscretePaletteProps): (value: number) => Color {
|
||||
const defaultColor = Color(0x0);
|
||||
if (props.colors.length === 0) return () => defaultColor;
|
||||
|
||||
return (value: number) => {
|
||||
const x = clamp(value, 0, 1);
|
||||
for (let i = props.colors.length - 1; i >= 0; i--) {
|
||||
const { color, fromValue, toValue } = props.colors[i];
|
||||
if (fromValue <= x && x <= toValue) return color;
|
||||
}
|
||||
return defaultColor;
|
||||
};
|
||||
}
|
||||
|
||||
function makePaletteFunctionContinuous(props: MVSContinuousPaletteProps): (value: number) => Color {
|
||||
const defaultColor = Color(0x0);
|
||||
const { colors, checkpoints } = makeContinuousPaletteCheckpoints(props);
|
||||
if (colors.length === 0) return () => defaultColor;
|
||||
|
||||
const underflowColor = props.setUnderflowColor ? props.underflowColor : defaultColor;
|
||||
const overflowColor = props.setOverflowColor ? props.overflowColor : defaultColor;
|
||||
|
||||
return (value: number) => {
|
||||
const x = clamp(value, 0, 1);
|
||||
const gteIdx = SortedArray.findPredecessorIndex(checkpoints, x); // Index of the first greater or equal checkpoint
|
||||
if (gteIdx === 0) {
|
||||
if (x === checkpoints[0]) return colors[0];
|
||||
else return underflowColor;
|
||||
}
|
||||
if (gteIdx === checkpoints.length) {
|
||||
return overflowColor;
|
||||
}
|
||||
const q = (x - checkpoints[gteIdx - 1]) / (checkpoints[gteIdx] - checkpoints[gteIdx - 1]);
|
||||
return Color.interpolate(colors[gteIdx - 1], colors[gteIdx], q);
|
||||
};
|
||||
}
|
||||
|
||||
const RelativeAxisAngleState = {
|
||||
Rt: Mat3(),
|
||||
R: Mat3(),
|
||||
};
|
||||
function relativeAxisAngle(start: Mat3, end: Mat3): { axis: Vec3, angle: number } {
|
||||
// R_rel = end * start^T
|
||||
const R0 = start, R1 = end;
|
||||
const Rt = Mat3.transpose(RelativeAxisAngleState.Rt, R0);
|
||||
const R = Mat3.mul(RelativeAxisAngleState.R, R1, Rt);
|
||||
|
||||
const tr = R[0] + R[4] + R[8]; // trace
|
||||
let angle = Math.acos(clamp((tr - 1) * 0.5, -1, 1)); // in [0, π]
|
||||
const axis = Vec3();
|
||||
|
||||
const eps = 1e-6;
|
||||
const sinA = Math.sin(angle);
|
||||
|
||||
if (angle < eps) {
|
||||
// Near identity: axis undefined; return any unit axis (choose something stable)
|
||||
Vec3.set(axis, 0, 0, 1);
|
||||
angle = 0.0;
|
||||
return { axis, angle };
|
||||
}
|
||||
|
||||
if (Math.PI - angle > 1e-4) {
|
||||
// General case
|
||||
axis[0] = (R[5] - R[7]) / (2 * sinA); // (r32 - r23)
|
||||
axis[1] = (R[6] - R[2]) / (2 * sinA); // (r13 - r31)
|
||||
axis[2] = (R[1] - R[3]) / (2 * sinA); // (r21 - r12)
|
||||
Vec3.normalize(axis, axis);
|
||||
return { axis, angle };
|
||||
}
|
||||
|
||||
// angle ~ π: use diagonal-based extraction for stability
|
||||
// Compute squared components then pick the largest to avoid precision loss
|
||||
const xx = Math.max(0, (R[0] + 1) * 0.5);
|
||||
const yy = Math.max(0, (R[4] + 1) * 0.5);
|
||||
const zz = Math.max(0, (R[8] + 1) * 0.5);
|
||||
|
||||
let x = Math.sqrt(xx), y = Math.sqrt(yy), z = Math.sqrt(zz);
|
||||
|
||||
if (x >= y && x >= z) {
|
||||
x = Math.max(x, 1e-8);
|
||||
y = (R[1] + R[3]) / (4 * x);
|
||||
z = (R[2] + R[6]) / (4 * x);
|
||||
Vec3.set(axis, x, y, z);
|
||||
} else if (y >= x && y >= z) {
|
||||
y = Math.max(y, 1e-8);
|
||||
x = (R[1] + R[3]) / (4 * y);
|
||||
z = (R[5] + R[7]) / (4 * y);
|
||||
Vec3.set(axis, x, y, z);
|
||||
} else {
|
||||
z = Math.max(z, 1e-8);
|
||||
x = (R[2] + R[6]) / (4 * z);
|
||||
y = (R[5] + R[7]) / (4 * z);
|
||||
Vec3.set(axis, x, y, z);
|
||||
}
|
||||
|
||||
Vec3.normalize(axis, axis);
|
||||
return { axis, angle: Math.PI };
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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 Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -9,35 +9,35 @@ import { ElementIndex } from '../../../mol-model/structure';
|
||||
import { arrayExtend, range } from '../../../mol-util/array';
|
||||
|
||||
|
||||
/** Represents a collection of disjoint atom ranges in a model.
|
||||
* The number of ranges is `AtomRanges.count(ranges)`,
|
||||
* the i-th range covers atoms `[ranges.from[i], ranges.to[i])`. */
|
||||
export interface AtomRanges {
|
||||
/** Represents a collection of disjoint elements ranges in a model (atoms, spheres, or gaussians).
|
||||
* The number of ranges is `ElementRanges.count(ranges)`,
|
||||
* the i-th range covers elements `[ranges.from[i], ranges.to[i])`. */
|
||||
export interface ElementRanges {
|
||||
from: ElementIndex[],
|
||||
to: ElementIndex[],
|
||||
}
|
||||
|
||||
export const AtomRanges = {
|
||||
/** Return the number of disjoined ranges in a `AtomRanges` object */
|
||||
count(ranges: AtomRanges): number {
|
||||
export const ElementRanges = {
|
||||
/** Return the number of disjoined ranges in a `ElementRanges` object */
|
||||
count(ranges: ElementRanges): number {
|
||||
return ranges.from.length;
|
||||
},
|
||||
|
||||
/** Create new `AtomRanges` without any atoms */
|
||||
empty(): AtomRanges {
|
||||
/** Create new `ElementRanges` without any elements */
|
||||
empty(): ElementRanges {
|
||||
return { from: [], to: [] };
|
||||
},
|
||||
|
||||
/** Create new `AtomRanges` containing a single range of atoms `[from, to)` */
|
||||
single(from: ElementIndex, to: ElementIndex): AtomRanges {
|
||||
/** Create new `ElementRanges` containing a single range of elements `[from, to)` */
|
||||
single(from: ElementIndex, to: ElementIndex): ElementRanges {
|
||||
return { from: [from], to: [to] };
|
||||
},
|
||||
|
||||
/** Add a range of atoms `[from, to)` to existing `AtomRanges` and return the modified original.
|
||||
/** Add a range of elements `[from, to)` to existing `ElementRanges` and return the modified original.
|
||||
* The added range must start after the end of the last existing range
|
||||
* (if it starts just on the next atom, these two ranges will get merged). */
|
||||
add(ranges: AtomRanges, from: ElementIndex, to: ElementIndex): AtomRanges {
|
||||
const n = AtomRanges.count(ranges);
|
||||
* (if it starts just on the next element, these two ranges will get merged). */
|
||||
add(ranges: ElementRanges, from: ElementIndex, to: ElementIndex): ElementRanges {
|
||||
const n = ElementRanges.count(ranges);
|
||||
if (n > 0) {
|
||||
const lastTo = ranges.to[n - 1];
|
||||
if (from < lastTo) throw new Error('Overlapping ranges not allowed');
|
||||
@@ -55,28 +55,30 @@ export const AtomRanges = {
|
||||
},
|
||||
|
||||
/** Apply function `func` to each range in `ranges` */
|
||||
foreach(ranges: AtomRanges, func: (from: ElementIndex, to: ElementIndex) => any) {
|
||||
const n = AtomRanges.count(ranges);
|
||||
foreach(ranges: ElementRanges, func: (from: ElementIndex, to: ElementIndex) => any) {
|
||||
const n = ElementRanges.count(ranges);
|
||||
for (let i = 0; i < n; i++) func(ranges.from[i], ranges.to[i]);
|
||||
},
|
||||
|
||||
/** Apply function `func` to each range in `ranges` and return an array with results */
|
||||
map<T>(ranges: AtomRanges, func: (from: ElementIndex, to: ElementIndex) => T): T[] {
|
||||
const n = AtomRanges.count(ranges);
|
||||
map<T>(ranges: ElementRanges, func: (from: ElementIndex, to: ElementIndex) => T): T[] {
|
||||
const n = ElementRanges.count(ranges);
|
||||
const result: T[] = new Array(n);
|
||||
for (let i = 0; i < n; i++) result[i] = func(ranges.from[i], ranges.to[i]);
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Compute the set union of multiple `AtomRanges` objects (as sets of atoms) */
|
||||
union(ranges: AtomRanges[]): AtomRanges {
|
||||
const concat = AtomRanges.empty();
|
||||
/** Compute the set union of multiple `ElementRanges` objects (as sets of elements) */
|
||||
union(ranges: (ElementRanges | undefined)[]): ElementRanges {
|
||||
const concat = ElementRanges.empty();
|
||||
for (const r of ranges) {
|
||||
arrayExtend(concat.from, r.from);
|
||||
arrayExtend(concat.to, r.to);
|
||||
if (r) {
|
||||
arrayExtend(concat.from, r.from);
|
||||
arrayExtend(concat.to, r.to);
|
||||
}
|
||||
}
|
||||
const indices = range(concat.from.length).sort((i, j) => concat.from[i] - concat.from[j]); // sort by start of range
|
||||
const result = AtomRanges.empty();
|
||||
const result = ElementRanges.empty();
|
||||
let last = -1;
|
||||
for (const i of indices) {
|
||||
const from = concat.from[i];
|
||||
@@ -94,26 +96,26 @@ export const AtomRanges = {
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Return a sorted subset of `atoms` which lie in any of `ranges` (i.e. set intersection of `atoms` and `ranges`).
|
||||
/** Return a sorted subset of `elements` which lie in any of `ranges` (i.e. set intersection of `elements` and `ranges`).
|
||||
* If `out` is provided, use it to store the result (clear any old contents).
|
||||
* If `outFirstAtomIndex` is provided, fill `outFirstAtomIndex.value` with the index of the first selected atom (if any). */
|
||||
selectAtomsInRanges(atoms: SortedArray<ElementIndex>, ranges: AtomRanges, out?: ElementIndex[], outFirstAtomIndex: { value?: number } = {}): ElementIndex[] {
|
||||
* If `outFirstElementIndex` is provided, fill `outFirstElementIndex.value` with the index of the first selected element (if any). */
|
||||
selectElementsInRanges(elements: SortedArray<ElementIndex>, ranges: ElementRanges, out?: ElementIndex[], outFirstElementIndex: { value?: number } = {}): ElementIndex[] {
|
||||
out ??= [];
|
||||
out.length = 0;
|
||||
outFirstAtomIndex.value = undefined;
|
||||
outFirstElementIndex.value = undefined;
|
||||
|
||||
const nAtoms = atoms.length;
|
||||
const nRanges = AtomRanges.count(ranges);
|
||||
if (nAtoms <= nRanges) {
|
||||
// Implementation 1 (more efficient when there are fewer atoms)
|
||||
let iRange = SortedArray.findPredecessorIndex(SortedArray.ofSortedArray(ranges.to), atoms[0] + 1);
|
||||
for (let iAtom = 0; iAtom < nAtoms; iAtom++) {
|
||||
const a = atoms[iAtom];
|
||||
const nElements = elements.length;
|
||||
const nRanges = ElementRanges.count(ranges);
|
||||
if (nElements <= nRanges) {
|
||||
// Implementation 1 (more efficient when there are fewer elements)
|
||||
let iRange = SortedArray.findPredecessorIndex(SortedArray.ofSortedArray(ranges.to), elements[0] + 1);
|
||||
for (let iElem = 0; iElem < nElements; iElem++) {
|
||||
const a = elements[iElem];
|
||||
while (iRange < nRanges && ranges.to[iRange] <= a) iRange++;
|
||||
const qualifies = iRange < nRanges && ranges.from[iRange] <= a;
|
||||
if (qualifies) {
|
||||
out.push(a);
|
||||
outFirstAtomIndex.value ??= iAtom;
|
||||
outFirstElementIndex.value ??= iElem;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -121,11 +123,11 @@ export const AtomRanges = {
|
||||
for (let iRange = 0; iRange < nRanges; iRange++) {
|
||||
const from = ranges.from[iRange];
|
||||
const to = ranges.to[iRange];
|
||||
for (let iAtom = SortedArray.findPredecessorIndex(atoms, from); iAtom < nAtoms; iAtom++) {
|
||||
const a = atoms[iAtom];
|
||||
for (let iElem = SortedArray.findPredecessorIndex(elements, from); iElem < nElements; iElem++) {
|
||||
const a = elements[iElem];
|
||||
if (a < to) {
|
||||
out.push(a);
|
||||
outFirstAtomIndex.value ??= iAtom;
|
||||
outFirstElementIndex.value ??= iElem;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -7,17 +7,42 @@
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex } from '../../../mol-model/structure';
|
||||
import { CoarseElements } from '../../../mol-model/structure/model/properties/coarse';
|
||||
import { filterInPlace, range, sortIfNeeded } from '../../../mol-util/array';
|
||||
import { Mapping, MultiMap, NumberMap } from './utils';
|
||||
|
||||
|
||||
/** Auxiliary data structure for efficiently finding chains/residues/atoms in a model by their properties */
|
||||
export interface IndicesAndSortings {
|
||||
atomic?: AtomicIndicesAndSortings,
|
||||
spheres?: CoarseIndicesAndSortings,
|
||||
gaussians?: CoarseIndicesAndSortings,
|
||||
}
|
||||
|
||||
export const IndicesAndSortings = {
|
||||
/** Get `IndicesAndSortings` for a model (use a cached value or create if not available yet) */
|
||||
get(model: Model): IndicesAndSortings {
|
||||
return model._dynamicPropertyData['indices-and-sortings'] ??= this.create(model);
|
||||
},
|
||||
|
||||
/** Create `IndicesAndSortings` for a model */
|
||||
create(model: Model): IndicesAndSortings {
|
||||
return {
|
||||
atomic: createAtomicIndicesAndSortings(model),
|
||||
spheres: createCoarseIndicesAndSortings(model.coarseHierarchy.spheres),
|
||||
gaussians: createCoarseIndicesAndSortings(model.coarseHierarchy.gaussians),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/** Auxiliary data structure for efficiently finding chains/residues/atoms in an atomic model by their properties */
|
||||
export interface AtomicIndicesAndSortings {
|
||||
chainsByLabelEntityId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByLabelAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByAuthAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
residuesSortedByLabelSeqId: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesSortedByAuthSeqId: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesSortedBySourceIndex: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesByInsCode: Mapping<ChainIndex, Mapping<string, readonly ResidueIndex[]>>,
|
||||
residuesByLabelCompId: Mapping<ChainIndex, Mapping<string, readonly ResidueIndex[]>>,
|
||||
/** Indicates if each residue is listed only once in `residuesByLabelCompId` (i.e. if each residue has only one label_comp_id) */
|
||||
@@ -26,95 +51,162 @@ export interface IndicesAndSortings {
|
||||
/** Indicates if each residue is listed only once in `residuesByAuthCompId` (i.e. if each residue has only one auth_comp_id) */
|
||||
residuesByAuthCompIdIsPure: boolean,
|
||||
atomsById: Mapping<number, ElementIndex>,
|
||||
atomsByIndex: Mapping<number, ElementIndex>,
|
||||
atomsBySourceIndex: Mapping<number, ElementIndex>,
|
||||
}
|
||||
|
||||
export const IndicesAndSortings = {
|
||||
/** Get `IndicesAndSortings` for a model (use a cached value or create if not available yet) */
|
||||
get(model: Model): IndicesAndSortings {
|
||||
return model._dynamicPropertyData['indices-and-sortings'] ??= IndicesAndSortings.create(model);
|
||||
},
|
||||
/** Create `AtomicIndicesAndSortings` for a model */
|
||||
function createAtomicIndicesAndSortings(model: Model): AtomicIndicesAndSortings | undefined {
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
if (nAtoms === 0) return undefined;
|
||||
|
||||
/** Create `IndicesAndSortings` for a model */
|
||||
create(model: Model): IndicesAndSortings {
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
const nChains = h.chains._rowCount;
|
||||
const { label_entity_id, label_asym_id, auth_asym_id } = h.chains;
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = h.residues;
|
||||
const { label_comp_id, auth_comp_id } = h.atoms;
|
||||
const { Present } = Column.ValueKind;
|
||||
const nChains = h.chains._rowCount;
|
||||
const { label_entity_id, label_asym_id, auth_asym_id } = h.chains;
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = h.residues;
|
||||
const { label_comp_id, auth_comp_id } = h.atoms;
|
||||
const { Present } = Column.ValueKind;
|
||||
|
||||
const chainsByLabelEntityId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByLabelAsymId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByAuthAsymId = new MultiMap<string, ChainIndex>();
|
||||
const residuesSortedByLabelSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesSortedByAuthSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesByInsCode = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
const residuesByLabelCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByLabelCompIdIsPure = true;
|
||||
const residuesByAuthCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByAuthCompIdIsPure = true;
|
||||
const atomsById = new NumberMap<number, ElementIndex>(nAtoms + 1);
|
||||
const atomsByIndex = new NumberMap<number, ElementIndex>(nAtoms);
|
||||
const chainsByLabelEntityId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByLabelAsymId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByAuthAsymId = new MultiMap<string, ChainIndex>();
|
||||
const residuesSortedByLabelSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesSortedByAuthSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesSortedBySourceIndex = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesByInsCode = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
const residuesByLabelCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByLabelCompIdIsPure = true;
|
||||
const residuesByAuthCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByAuthCompIdIsPure = true;
|
||||
const atomsById = new NumberMap<number, ElementIndex>(nAtoms + 1);
|
||||
const atomsBySourceIndex = new NumberMap<number, ElementIndex>(nAtoms);
|
||||
|
||||
const _labelCompIdSet = new Set<string>();
|
||||
const _authCompIdSet = new Set<string>();
|
||||
const _labelCompIdSet = new Set<string>();
|
||||
const _authCompIdSet = new Set<string>();
|
||||
|
||||
for (let iChain = 0 as ChainIndex; iChain < nChains; iChain++) {
|
||||
chainsByLabelEntityId.add(label_entity_id.value(iChain), iChain);
|
||||
chainsByLabelAsymId.add(label_asym_id.value(iChain), iChain);
|
||||
chainsByAuthAsymId.add(auth_asym_id.value(iChain), iChain);
|
||||
for (let iChain = 0 as ChainIndex; iChain < nChains; iChain++) {
|
||||
chainsByLabelEntityId.add(label_entity_id.value(iChain), iChain);
|
||||
chainsByLabelAsymId.add(label_asym_id.value(iChain), iChain);
|
||||
chainsByAuthAsymId.add(auth_asym_id.value(iChain), iChain);
|
||||
|
||||
const iResFrom = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain]];
|
||||
const iResTo = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain + 1] - 1] + 1;
|
||||
const iResFrom = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain]];
|
||||
const iResTo = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain + 1] - 1] + 1;
|
||||
|
||||
const residuesWithLabelSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => label_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByLabelSeqId.set(iChain, Sorting.create(residuesWithLabelSeqId, label_seq_id.value));
|
||||
const residuesWithLabelSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => label_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByLabelSeqId.set(iChain, Sorting.create(residuesWithLabelSeqId, label_seq_id.value));
|
||||
|
||||
const residuesWithAuthSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => auth_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByAuthSeqId.set(iChain, Sorting.create(residuesWithAuthSeqId, auth_seq_id.value));
|
||||
const residuesWithAuthSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => auth_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByAuthSeqId.set(iChain, Sorting.create(residuesWithAuthSeqId, auth_seq_id.value));
|
||||
|
||||
const residuesHereByInsCode = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByLabelCompId = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByAuthCompId = new MultiMap<string, ResidueIndex>();
|
||||
for (let iRes = iResFrom; iRes < iResTo; iRes++) {
|
||||
if (pdbx_PDB_ins_code.valueKind(iRes) === Present) {
|
||||
residuesHereByInsCode.add(pdbx_PDB_ins_code.value(iRes), iRes);
|
||||
}
|
||||
const iAtomFrom = h.residueAtomSegments.offsets[iRes];
|
||||
const iAtomTo = h.residueAtomSegments.offsets[iRes + 1];
|
||||
for (let iAtom = iAtomFrom; iAtom < iAtomTo; iAtom++) {
|
||||
_labelCompIdSet.add(label_comp_id.value(iAtom));
|
||||
_authCompIdSet.add(auth_comp_id.value(iAtom));
|
||||
}
|
||||
if (_labelCompIdSet.size > 1) residuesByLabelCompIdIsPure = false;
|
||||
if (_authCompIdSet.size > 1) residuesByAuthCompIdIsPure = false;
|
||||
for (const labelCompId of _labelCompIdSet) residuesHereByLabelCompId.add(labelCompId, iRes);
|
||||
for (const authCompId of _authCompIdSet) residuesHereByAuthCompId.add(authCompId, iRes);
|
||||
_labelCompIdSet.clear();
|
||||
_authCompIdSet.clear();
|
||||
const residuesWithSourceIndex = range(iResFrom, iResTo) as ResidueIndex[];
|
||||
residuesSortedBySourceIndex.set(iChain, Sorting.create(residuesWithSourceIndex, h.residueSourceIndex.value));
|
||||
|
||||
const residuesHereByInsCode = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByLabelCompId = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByAuthCompId = new MultiMap<string, ResidueIndex>();
|
||||
for (let iRes = iResFrom; iRes < iResTo; iRes++) {
|
||||
if (pdbx_PDB_ins_code.valueKind(iRes) === Present) {
|
||||
residuesHereByInsCode.add(pdbx_PDB_ins_code.value(iRes), iRes);
|
||||
}
|
||||
residuesByInsCode.set(iChain, residuesHereByInsCode);
|
||||
residuesByLabelCompId.set(iChain, residuesHereByLabelCompId);
|
||||
residuesByAuthCompId.set(iChain, residuesHereByAuthCompId);
|
||||
const iAtomFrom = h.residueAtomSegments.offsets[iRes];
|
||||
const iAtomTo = h.residueAtomSegments.offsets[iRes + 1];
|
||||
for (let iAtom = iAtomFrom; iAtom < iAtomTo; iAtom++) {
|
||||
_labelCompIdSet.add(label_comp_id.value(iAtom));
|
||||
_authCompIdSet.add(auth_comp_id.value(iAtom));
|
||||
}
|
||||
if (_labelCompIdSet.size > 1) residuesByLabelCompIdIsPure = false;
|
||||
if (_authCompIdSet.size > 1) residuesByAuthCompIdIsPure = false;
|
||||
for (const labelCompId of _labelCompIdSet) residuesHereByLabelCompId.add(labelCompId, iRes);
|
||||
for (const authCompId of _authCompIdSet) residuesHereByAuthCompId.add(authCompId, iRes);
|
||||
_labelCompIdSet.clear();
|
||||
_authCompIdSet.clear();
|
||||
}
|
||||
residuesByInsCode.set(iChain, residuesHereByInsCode);
|
||||
residuesByLabelCompId.set(iChain, residuesHereByLabelCompId);
|
||||
residuesByAuthCompId.set(iChain, residuesHereByAuthCompId);
|
||||
}
|
||||
|
||||
const atomId = model.atomicConformation.atomId.value;
|
||||
const atomIndex = h.atomSourceIndex.value;
|
||||
for (let iAtom = 0 as ElementIndex; iAtom < nAtoms; iAtom++) {
|
||||
atomsById.set(atomId(iAtom), iAtom);
|
||||
atomsByIndex.set(atomIndex(iAtom), iAtom);
|
||||
}
|
||||
const atomId = model.atomicConformation.atomId.value;
|
||||
const atomIndex = h.atomSourceIndex.value;
|
||||
for (let iAtom = 0 as ElementIndex; iAtom < nAtoms; iAtom++) {
|
||||
atomsById.set(atomId(iAtom), iAtom);
|
||||
atomsBySourceIndex.set(atomIndex(iAtom), iAtom);
|
||||
}
|
||||
|
||||
return {
|
||||
chainsByLabelEntityId, chainsByLabelAsymId, chainsByAuthAsymId,
|
||||
residuesSortedByLabelSeqId, residuesSortedByAuthSeqId, residuesByInsCode,
|
||||
residuesByLabelCompId, residuesByLabelCompIdIsPure, residuesByAuthCompId, residuesByAuthCompIdIsPure,
|
||||
atomsById, atomsByIndex,
|
||||
};
|
||||
return {
|
||||
chainsByLabelEntityId, chainsByLabelAsymId, chainsByAuthAsymId,
|
||||
residuesSortedByLabelSeqId, residuesSortedByAuthSeqId, residuesSortedBySourceIndex, residuesByInsCode,
|
||||
residuesByLabelCompId, residuesByLabelCompIdIsPure, residuesByAuthCompId, residuesByAuthCompIdIsPure,
|
||||
atomsById, atomsBySourceIndex,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** Auxiliary data structure for efficiently finding chains/elements in a coarse model by their properties */
|
||||
export interface CoarseIndicesAndSortings {
|
||||
/** Coarse equivalent to `model.atomicHierarchy.chains` */
|
||||
chains: {
|
||||
/** Number of chains */
|
||||
count: number,
|
||||
/** Maps chain index to `label_entity_id` value */
|
||||
label_entity_id: string[],
|
||||
/** Maps chain index to `label_asym_id` value */
|
||||
label_asym_id: string[],
|
||||
},
|
||||
};
|
||||
chainsByEntityId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
/** Coarse elements (per chain) sorted by `seq_id_begin`.
|
||||
* This is used to get the range of elements which may overlap with a certain seq_id interval.
|
||||
*
|
||||
* (Filtering coarse elements by seq_id range is an interval search problem, so the worst-case-efficient solution would be to use a data structure optimized for that.
|
||||
* But that would be overkill if we expect that in most cases the coarse elements cover non-overlapping seq_id ranges.
|
||||
* So the current solution should be sufficient (fast for non-overlapping elements, while still correct if there are overlaps).) */
|
||||
elementsSortedBySeqIdBegin: Mapping<ChainIndex,
|
||||
Sorting<ElementIndex, number> & {
|
||||
/** Non-decreasing upper bound for `seq_id_end` values of elements as listed in `keys` (`seq_id_end.value(keys[i]) <= endUpperBounds[i]`) */
|
||||
endUpperBounds: SortedArray
|
||||
}>,
|
||||
}
|
||||
|
||||
/** Create `CoarseIndicesAndSortings` for a coarse elements hierarchy */
|
||||
function createCoarseIndicesAndSortings(coarseElements: CoarseElements): CoarseIndicesAndSortings | undefined {
|
||||
if (coarseElements.count === 0) return undefined;
|
||||
const { entity_id, asym_id, seq_id_begin, seq_id_end, chainElementSegments } = coarseElements;
|
||||
const { Present } = Column.ValueKind;
|
||||
const nChains = Math.max(chainElementSegments.count, 0); // chainElementSegments.count is -1 when there are no coarse elements
|
||||
|
||||
const chainsByEntityId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByAsymId = new MultiMap<string, ChainIndex>();
|
||||
const elementsSortedBySeqIdBegin = new Map<ChainIndex, Sorting<ElementIndex, number> & { endUpperBounds: SortedArray }>();
|
||||
const chains = {
|
||||
count: nChains,
|
||||
label_entity_id: new Array<string>(nChains),
|
||||
label_asym_id: new Array<string>(nChains),
|
||||
};
|
||||
|
||||
for (let iChain = 0 as ChainIndex; iChain < nChains; iChain++) {
|
||||
const iElemFrom = chainElementSegments.offsets[iChain];
|
||||
const iElemTo = chainElementSegments.offsets[iChain + 1];
|
||||
const entityId = entity_id.value(iElemFrom);
|
||||
const asymId = asym_id.value(iElemFrom);
|
||||
chains.label_entity_id[iChain] = entityId;
|
||||
chains.label_asym_id[iChain] = asymId;
|
||||
chainsByEntityId.add(entityId, iChain);
|
||||
chainsByAsymId.add(asymId, iChain);
|
||||
|
||||
const elementsWithSeqIds = filterInPlace(range(iElemFrom, iElemTo) as ElementIndex[], iElem => seq_id_begin.valueKind(iElem) === Present && seq_id_end.valueKind(iElem) === Present);
|
||||
const sorting = Sorting.create(elementsWithSeqIds, seq_id_begin.value);
|
||||
const endBounds = sorting.keys.map(seq_id_end.value);
|
||||
// Ensure non-decreasing endBounds:
|
||||
for (let i = 1; i < endBounds.length; i++) {
|
||||
if (endBounds[i - 1] > endBounds[i]) {
|
||||
endBounds[i] = endBounds[i - 1];
|
||||
}
|
||||
}
|
||||
elementsSortedBySeqIdBegin.set(iChain, { ...sorting, endUpperBounds: SortedArray.ofSortedArray(endBounds) });
|
||||
}
|
||||
|
||||
return { chains, chainsByEntityId, chainsByAsymId, elementsSortedBySeqIdBegin };
|
||||
}
|
||||
|
||||
|
||||
/** Represents a set of things (keys) of type `K`, sorted by some property (value) of type `V` */
|
||||
|
||||
@@ -8,11 +8,12 @@ import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ElementIndex, Model, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
|
||||
import { getPhysicalRadius } from '../../../mol-theme/size/physical';
|
||||
import { arrayExtend } from '../../../mol-util/array';
|
||||
import { AtomRanges } from './atom-ranges';
|
||||
import { ElementRanges } from './element-ranges';
|
||||
import { IndicesAndSortings } from './indexing';
|
||||
import { MVSAnnotationRow } from './schemas';
|
||||
import { getAtomRangesForRows } from './selections';
|
||||
import { getAtomRangesForRows, getGaussianRangesForRows, getSphereRangesForRows } from './selections';
|
||||
import { isDefined } from './utils';
|
||||
|
||||
|
||||
@@ -24,63 +25,86 @@ export interface TextProps {
|
||||
depth: number,
|
||||
/** Relative text size */
|
||||
scale: number,
|
||||
/** Index of the first atom within structure, to which this text is bound (for coloring and similar purposes) */
|
||||
/** Index of the first element within structure, to which this text is bound (for coloring and similar purposes) */
|
||||
group: number,
|
||||
}
|
||||
|
||||
const tmpVec = Vec3();
|
||||
const tmpArray: number[] = [];
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
const outAtoms: ElementIndex[] = [];
|
||||
const outFirstAtomIndex: { value?: number } = {};
|
||||
const outElements: ElementIndex[] = [];
|
||||
const outFirstElementIndex: { value?: number } = {};
|
||||
|
||||
/** Helper for caching atom ranges qualifying to a group of annotation rows, per `Unit`. */
|
||||
class AtomRangesCache {
|
||||
private readonly cache: { [key: string]: AtomRanges } = {};
|
||||
/** Helper for caching element ranges qualifying to a group of annotation rows, per `Unit`. */
|
||||
class ElementRangesCache {
|
||||
private readonly cache: { [key: string]: ElementRanges } = {};
|
||||
private readonly hasOperators: boolean;
|
||||
|
||||
constructor(private readonly rows: MVSAnnotationRow[]) {
|
||||
this.hasOperators = rows.some(row => isDefined(row.instance_id));
|
||||
}
|
||||
|
||||
get(unit: Unit): AtomRanges {
|
||||
get(unit: Unit): ElementRanges {
|
||||
const instanceId = unit.conformation.operator.instanceId;
|
||||
const key = this.hasOperators ? `${unit.model.id}:${instanceId}` : unit.model.id;
|
||||
return this.cache[key] ??= getAtomRangesForRows(this.rows, unit.model, instanceId, IndicesAndSortings.get(unit.model));
|
||||
const key = `${unit.model.id}:${unit.kind}:${this.hasOperators ? instanceId : '*'}`;
|
||||
return this.cache[key] ??= this.compute(unit);
|
||||
}
|
||||
private compute(unit: Unit): ElementRanges {
|
||||
const instanceId = unit.conformation.operator.instanceId;
|
||||
const indices = IndicesAndSortings.get(unit.model);
|
||||
switch (unit.kind) {
|
||||
case Unit.Kind.Atomic:
|
||||
return getAtomRangesForRows(this.rows, unit.model, instanceId, indices);
|
||||
case Unit.Kind.Spheres:
|
||||
return getSphereRangesForRows(this.rows, unit.model, instanceId, indices);
|
||||
case Unit.Kind.Gaussians:
|
||||
return getGaussianRangesForRows(this.rows, unit.model, instanceId, indices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Approximate number of heavy atoms per protein residue (I got 7.55 from 2e2n) */
|
||||
const AVG_ATOMS_PER_RESIDUE = 8;
|
||||
|
||||
/** Return `TextProps` (position, size, etc.) for a text that is to be bound to a substructure of `structure` defined by union of `rows`.
|
||||
* Derives `center` and `depth` from the boundary sphere of the substructure, `scale` from the number of heavy atoms in the substructure. */
|
||||
export function textPropsForSelection(structure: Structure, sizeFunction: (location: StructureElement.Location) => number, rows: MVSAnnotationRow[], onlyInModel?: Model): TextProps | undefined {
|
||||
export function textPropsForSelection(structure: Structure, rows: MVSAnnotationRow[], onlyInModel?: Model): TextProps | undefined {
|
||||
const loc = StructureElement.Location.create(structure);
|
||||
const { units } = structure;
|
||||
const { type_symbol } = StructureProperties.atom;
|
||||
tmpArray.length = 0;
|
||||
let includedAtoms = 0;
|
||||
let includedElements = 0;
|
||||
let includedHeavyAtoms = 0;
|
||||
let group: number | undefined = undefined;
|
||||
let atomSize: number | undefined = undefined;
|
||||
const atomRangesCache = new AtomRangesCache(rows);
|
||||
/** Used for `depth` in case the selection has only 1 element (hence bounding sphere radius is 0) */
|
||||
let singularRadius: number | undefined = undefined;
|
||||
const elementRangesCache = new ElementRangesCache(rows);
|
||||
for (let iUnit = 0, nUnits = units.length; iUnit < nUnits; iUnit++) {
|
||||
const unit = units[iUnit];
|
||||
if (onlyInModel && unit.model.id !== onlyInModel.id) continue;
|
||||
const ranges = atomRangesCache.get(unit);
|
||||
const coarseElements = unit.kind === Unit.Kind.Spheres ? unit.model.coarseHierarchy.spheres : unit.kind === Unit.Kind.Gaussians ? unit.model.coarseHierarchy.gaussians : undefined;
|
||||
const ranges = elementRangesCache.get(unit);
|
||||
ElementRanges.selectElementsInRanges(unit.elements, ranges, outElements, outFirstElementIndex);
|
||||
loc.unit = unit;
|
||||
AtomRanges.selectAtomsInRanges(unit.elements, ranges, outAtoms, outFirstAtomIndex);
|
||||
for (const atom of outAtoms) {
|
||||
loc.element = atom;
|
||||
unit.conformation.position(atom, tmpVec);
|
||||
arrayExtend(tmpArray, tmpVec);
|
||||
group ??= structure.serialMapping.cumulativeUnitElementCount[iUnit] + outFirstAtomIndex.value!;
|
||||
atomSize ??= sizeFunction(loc);
|
||||
includedAtoms++;
|
||||
if (type_symbol(loc) !== 'H') includedHeavyAtoms++;
|
||||
for (const iElem of outElements) {
|
||||
loc.element = iElem;
|
||||
arrayExtend(tmpArray, unit.conformation.position(iElem, tmpVec));
|
||||
group ??= structure.serialMapping.cumulativeUnitElementCount[iUnit] + outFirstElementIndex.value!;
|
||||
singularRadius ??= getPhysicalRadius(unit, iElem) * 1.2;
|
||||
if (coarseElements) {
|
||||
// coarse
|
||||
const nResidues = coarseElements.seq_id_end.value(iElem) - coarseElements.seq_id_begin.value(iElem) + 1;
|
||||
includedHeavyAtoms += nResidues * AVG_ATOMS_PER_RESIDUE;
|
||||
} else {
|
||||
// atomic
|
||||
if (type_symbol(loc) !== 'H') includedHeavyAtoms++;
|
||||
}
|
||||
includedElements++;
|
||||
}
|
||||
}
|
||||
if (includedAtoms > 0) {
|
||||
const { center, radius } = (includedAtoms > 1) ? boundarySphere(tmpArray) : { center: Vec3.fromArray(Vec3(), tmpArray, 0), radius: 1.1 * atomSize! };
|
||||
const scale = (includedHeavyAtoms || includedAtoms) ** (1 / 3);
|
||||
if (includedElements > 0) {
|
||||
const { center, radius } = (includedElements > 1) ? boundarySphere(tmpArray) : { center: Vec3.fromArray(Vec3(), tmpArray, 0), radius: singularRadius! };
|
||||
const scale = (includedHeavyAtoms || includedElements) ** (1 / 3);
|
||||
return { center, depth: radius, scale, group: group! };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,8 @@ const AllAtomicCifAnnotationSchema = {
|
||||
end_auth_seq_id: int,
|
||||
label_comp_id: str,
|
||||
auth_comp_id: str,
|
||||
// residue_index: int, // 0-based residue index in the source file // TODO this is defined in Python builder but not supported by Molstar yet
|
||||
/** 0-based residue index in the source file */
|
||||
residue_index: int,
|
||||
|
||||
/** Atom name like 'CA', 'N', 'O'... */
|
||||
label_atom_id: str,
|
||||
@@ -89,11 +90,11 @@ const FieldsForSchemas = {
|
||||
entity: ['group_id', 'label_entity_id'],
|
||||
chain: ['group_id', 'label_entity_id', 'label_asym_id'],
|
||||
auth_chain: ['group_id', 'auth_asym_id'],
|
||||
residue: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id'],
|
||||
auth_residue: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code'],
|
||||
residue: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id', 'residue_index'],
|
||||
auth_residue: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code', 'residue_index'],
|
||||
residue_range: ['group_id', 'label_entity_id', 'label_asym_id', 'beg_label_seq_id', 'end_label_seq_id'],
|
||||
auth_residue_range: ['group_id', 'auth_asym_id', 'beg_auth_seq_id', 'end_auth_seq_id'],
|
||||
atom: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id', 'label_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
auth_atom: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code', 'auth_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
atom: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id', 'residue_index', 'label_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
auth_atom: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code', 'residue_index', 'auth_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
all_atomic: Object.keys(AllAtomicCifAnnotationSchema) as (keyof typeof AllAtomicCifAnnotationSchema)[],
|
||||
} satisfies { [schema in MVSAnnotationSchema]: (keyof typeof AllAtomicCifAnnotationSchema)[] };
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
/**
|
||||
* 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 { Column } from '../../../mol-data/db';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex, StructureElement } from '../../../mol-model/structure';
|
||||
import { CoarseElements } from '../../../mol-model/structure/model/properties/coarse';
|
||||
import { Expression } from '../../../mol-script/language/expression';
|
||||
import { arrayExtend, filterInPlace, range } from '../../../mol-util/array';
|
||||
import { AtomRanges } from './atom-ranges';
|
||||
import { IndicesAndSortings, Sorting } from './indexing';
|
||||
import { arrayExtend, filterInPlace, range, sortIfNeeded } from '../../../mol-util/array';
|
||||
import { ElementRanges } from './element-ranges';
|
||||
import { AtomicIndicesAndSortings, CoarseIndicesAndSortings, IndicesAndSortings, Sorting } from './indexing';
|
||||
import { MVSAnnotationRow } from './schemas';
|
||||
import { isAnyDefined, isDefined } from './utils';
|
||||
|
||||
@@ -18,67 +20,72 @@ import { isAnyDefined, isDefined } from './utils';
|
||||
const EmptyArray: readonly any[] = [];
|
||||
|
||||
|
||||
/** Return atom ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getAtomRangesForRow(row: MVSAnnotationRow, model: Model, instanceId: string, indices: IndicesAndSortings): AtomRanges {
|
||||
if (isDefined(row.instance_id) && row.instance_id !== instanceId) return AtomRanges.empty();
|
||||
// ATOMIC SELECTIONS
|
||||
|
||||
/** Return atom ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getAtomRangesForRow(row: MVSAnnotationRow, model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges | undefined {
|
||||
if (!indices.atomic) return undefined;
|
||||
if (isDefined(row.instance_id) && row.instance_id !== instanceId) return undefined;
|
||||
|
||||
const atomicIndices = indices.atomic;
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
if (nAtoms === 0) return undefined;
|
||||
|
||||
const hasAtomIds = isAnyDefined(row.atom_id, row.atom_index);
|
||||
const hasAtomFilter = isAnyDefined(row.label_atom_id, row.auth_atom_id, row.type_symbol)
|
||||
|| isDefined(row.label_comp_id) && !indices.residuesByLabelCompIdIsPure
|
||||
|| isDefined(row.auth_comp_id) && !indices.residuesByAuthCompIdIsPure;
|
||||
|| isDefined(row.label_comp_id) && !atomicIndices.residuesByLabelCompIdIsPure
|
||||
|| isDefined(row.auth_comp_id) && !atomicIndices.residuesByAuthCompIdIsPure;
|
||||
const hasResidueFilter = isAnyDefined(row.label_seq_id, row.auth_seq_id, row.pdbx_PDB_ins_code,
|
||||
row.beg_label_seq_id, row.end_label_seq_id, row.beg_auth_seq_id, row.end_auth_seq_id,
|
||||
row.label_comp_id, row.auth_comp_id);
|
||||
row.label_comp_id, row.auth_comp_id, row.residue_index);
|
||||
const hasChainFilter = isAnyDefined(row.label_asym_id, row.auth_asym_id, row.label_entity_id);
|
||||
|
||||
if (hasAtomIds) {
|
||||
const theAtom = getTheAtomForRow(model, row, indices);
|
||||
return theAtom !== undefined ? AtomRanges.single(theAtom, theAtom + 1 as ElementIndex) : AtomRanges.empty();
|
||||
const theAtom = getTheAtomForRow(model, row, atomicIndices);
|
||||
return theAtom !== undefined ? ElementRanges.single(theAtom, theAtom + 1 as ElementIndex) : undefined;
|
||||
}
|
||||
|
||||
if (!hasChainFilter && !hasResidueFilter && !hasAtomFilter) {
|
||||
return AtomRanges.single(0 as ElementIndex, nAtoms as ElementIndex);
|
||||
return ElementRanges.single(0 as ElementIndex, nAtoms as ElementIndex);
|
||||
}
|
||||
|
||||
const qualifyingChains = getQualifyingChains(model, row, indices);
|
||||
const qualifyingChains = getQualifyingChains(model, row, atomicIndices);
|
||||
if (!hasResidueFilter && !hasAtomFilter) {
|
||||
const chainOffsets = h.chainAtomSegments.offsets;
|
||||
const ranges = AtomRanges.empty();
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iChain of qualifyingChains) {
|
||||
AtomRanges.add(ranges, chainOffsets[iChain], chainOffsets[iChain + 1]);
|
||||
ElementRanges.add(ranges, chainOffsets[iChain], chainOffsets[iChain + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingResidues = getQualifyingResidues(model, row, indices, qualifyingChains);
|
||||
const qualifyingResidues = getQualifyingResidues(model, row, atomicIndices, qualifyingChains);
|
||||
if (!hasAtomFilter) {
|
||||
const residueOffsets = h.residueAtomSegments.offsets;
|
||||
const ranges = AtomRanges.empty();
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iRes of qualifyingResidues) {
|
||||
AtomRanges.add(ranges, residueOffsets[iRes], residueOffsets[iRes + 1]);
|
||||
ElementRanges.add(ranges, residueOffsets[iRes], residueOffsets[iRes + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingAtoms = getQualifyingAtoms(model, row, indices, qualifyingResidues);
|
||||
const ranges = AtomRanges.empty();
|
||||
const qualifyingAtoms = getQualifyingAtoms(model, row, atomicIndices, qualifyingResidues);
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iAtom of qualifyingAtoms) {
|
||||
AtomRanges.add(ranges, iAtom, iAtom + 1 as ElementIndex);
|
||||
ElementRanges.add(ranges, iAtom, iAtom + 1 as ElementIndex);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/** Return atom ranges in `model` which satisfy criteria given by any of `rows` (atoms that satisfy more rows are still included only once) */
|
||||
export function getAtomRangesForRows(rows: MVSAnnotationRow[], model: Model, instanceId: string, indices: IndicesAndSortings): AtomRanges {
|
||||
return AtomRanges.union(rows.map(row => getAtomRangesForRow(row, model, instanceId, indices)));
|
||||
export function getAtomRangesForRows(rows: MVSAnnotationRow[], model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges {
|
||||
return ElementRanges.union(rows.map(row => getAtomRangesForRow(row, model, instanceId, indices)));
|
||||
}
|
||||
|
||||
|
||||
/** Return an array of chain indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingChains(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): readonly ChainIndex[] {
|
||||
function getQualifyingChains(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings): readonly ChainIndex[] {
|
||||
const { auth_asym_id, label_entity_id, _rowCount: nChains } = model.atomicHierarchy.chains;
|
||||
let result: readonly ChainIndex[] | undefined = undefined;
|
||||
if (isDefined(row.label_asym_id)) {
|
||||
@@ -103,10 +110,10 @@ function getQualifyingChains(model: Model, row: MVSAnnotationRow, indices: Indic
|
||||
}
|
||||
|
||||
/** Return an array of residue indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings, fromChains: readonly ChainIndex[]): ResidueIndex[] {
|
||||
function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings, fromChains: readonly ChainIndex[]): ResidueIndex[] {
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = model.atomicHierarchy.residues;
|
||||
const { label_comp_id, auth_comp_id } = model.atomicHierarchy.atoms;
|
||||
const { residueAtomSegments, chainAtomSegments } = model.atomicHierarchy;
|
||||
const { residueAtomSegments, chainAtomSegments, residueSourceIndex } = model.atomicHierarchy;
|
||||
const { Present } = Column.ValueKind;
|
||||
const result: ResidueIndex[] = [];
|
||||
for (const iChain of fromChains) {
|
||||
@@ -123,6 +130,14 @@ function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: Ind
|
||||
residuesHere = Sorting.getKeysWithValue(sorting, row.auth_seq_id);
|
||||
}
|
||||
}
|
||||
if (isDefined(row.residue_index)) {
|
||||
if (residuesHere) {
|
||||
residuesHere = residuesHere.filter(i => residueSourceIndex.value(i) === row.residue_index);
|
||||
} else {
|
||||
const sorting = indices.residuesSortedBySourceIndex.get(iChain)!;
|
||||
residuesHere = Sorting.getKeysWithValue(sorting, row.residue_index);
|
||||
}
|
||||
}
|
||||
if (isDefined(row.pdbx_PDB_ins_code)) {
|
||||
if (residuesHere) {
|
||||
residuesHere = residuesHere.filter(i => pdbx_PDB_ins_code.value(i) === row.pdbx_PDB_ins_code);
|
||||
@@ -193,11 +208,12 @@ function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: Ind
|
||||
}
|
||||
arrayExtend(result, residuesHere);
|
||||
}
|
||||
sortIfNeeded(result, (a, b) => a - b);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return an array of atom indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingAtoms(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings, fromResidues: readonly ResidueIndex[]): ElementIndex[] {
|
||||
function getQualifyingAtoms(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings, fromResidues: readonly ResidueIndex[]): ElementIndex[] {
|
||||
const { label_atom_id, auth_atom_id, type_symbol, label_comp_id, auth_comp_id } = model.atomicHierarchy.atoms;
|
||||
const residueAtomSegments_offsets = model.atomicHierarchy.residueAtomSegments.offsets;
|
||||
const result: ElementIndex[] = [];
|
||||
@@ -225,12 +241,12 @@ function getQualifyingAtoms(model: Model, row: MVSAnnotationRow, indices: Indice
|
||||
|
||||
/** Return index of atom in `model` which satistfies criteria given by `row`, if any.
|
||||
* Only works when `row.atom_id` and/or `row.atom_index` is defined (otherwise use `getAtomRangesForRow`). */
|
||||
function getTheAtomForRow(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): ElementIndex | undefined {
|
||||
function getTheAtomForRow(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings): ElementIndex | undefined {
|
||||
let iAtom: ElementIndex | undefined = undefined;
|
||||
if (!isDefined(row.atom_id) && !isDefined(row.atom_index)) throw new Error('ArgumentError: at least one of row.atom_id, row.atom_index must be defined.');
|
||||
if (isDefined(row.atom_id) && isDefined(row.atom_index)) {
|
||||
const a1 = indices.atomsById.get(row.atom_id);
|
||||
const a2 = indices.atomsByIndex.get(row.atom_index);
|
||||
const a2 = indices.atomsBySourceIndex.get(row.atom_index);
|
||||
if (a1 !== a2) return undefined;
|
||||
iAtom = a1;
|
||||
}
|
||||
@@ -238,7 +254,7 @@ function getTheAtomForRow(model: Model, row: MVSAnnotationRow, indices: IndicesA
|
||||
iAtom = indices.atomsById.get(row.atom_id);
|
||||
}
|
||||
if (isDefined(row.atom_index)) {
|
||||
iAtom = indices.atomsByIndex.get(row.atom_index);
|
||||
iAtom = indices.atomsBySourceIndex.get(row.atom_index);
|
||||
}
|
||||
if (iAtom === undefined) return undefined;
|
||||
if (!atomQualifies(model, iAtom, row)) return undefined;
|
||||
@@ -261,11 +277,13 @@ export function atomQualifies(model: Model, iAtom: ElementIndex, row: MVSAnnotat
|
||||
const label_seq_id = (h.residues.label_seq_id.valueKind(iRes) === Column.ValueKind.Present) ? h.residues.label_seq_id.value(iRes) : undefined;
|
||||
const auth_seq_id = (h.residues.auth_seq_id.valueKind(iRes) === Column.ValueKind.Present) ? h.residues.auth_seq_id.value(iRes) : undefined;
|
||||
const pdbx_PDB_ins_code = h.residues.pdbx_PDB_ins_code.value(iRes);
|
||||
const residue_index = h.residueSourceIndex.value(iRes);
|
||||
if (!matches(row.label_seq_id, label_seq_id)) return false;
|
||||
if (!matches(row.auth_seq_id, auth_seq_id)) return false;
|
||||
if (!matches(row.pdbx_PDB_ins_code, pdbx_PDB_ins_code)) return false;
|
||||
if (!matchesRange(row.beg_label_seq_id, row.end_label_seq_id, label_seq_id)) return false;
|
||||
if (!matchesRange(row.beg_auth_seq_id, row.end_auth_seq_id, auth_seq_id)) return false;
|
||||
if (!matches(row.residue_index, residue_index)) return false;
|
||||
|
||||
const label_comp_id = h.atoms.label_comp_id.value(iAtom);
|
||||
const auth_comp_id = h.atoms.auth_comp_id.value(iAtom);
|
||||
@@ -299,6 +317,124 @@ function matchesRange<T>(requiredMin: T | undefined | null, requiredMax: T | und
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// COARSE SELECTIONS
|
||||
|
||||
/** Return sphere ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getSphereRangesForRow(row: MVSAnnotationRow, model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges | undefined {
|
||||
if (!indices.spheres) return undefined;
|
||||
if (isDefined(row.instance_id) && row.instance_id !== instanceId) return undefined;
|
||||
return getCoarseElementRangesForRow(row, model.coarseHierarchy.spheres, indices.spheres);
|
||||
}
|
||||
|
||||
/** Return sphere ranges in `model` which satisfy criteria given by any of `rows` (spheres that satisfy more rows are still included only once) */
|
||||
export function getSphereRangesForRows(rows: MVSAnnotationRow[], model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges {
|
||||
return ElementRanges.union(rows.map(row => getSphereRangesForRow(row, model, instanceId, indices)));
|
||||
}
|
||||
|
||||
/** Return gaussian ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getGaussianRangesForRow(row: MVSAnnotationRow, model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges | undefined {
|
||||
if (!indices.gaussians) return undefined;
|
||||
if (isDefined(row.instance_id) && row.instance_id !== instanceId) return undefined;
|
||||
return getCoarseElementRangesForRow(row, model.coarseHierarchy.gaussians, indices.gaussians);
|
||||
}
|
||||
|
||||
/** Return gaussian ranges in `model` which satisfy criteria given by any of `rows` (gaussians that satisfy more rows are still included only once) */
|
||||
export function getGaussianRangesForRows(rows: MVSAnnotationRow[], model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges {
|
||||
return ElementRanges.union(rows.map(row => getGaussianRangesForRow(row, model, instanceId, indices)));
|
||||
}
|
||||
|
||||
/** Return ranges of coarse elements (spheres or gaussians) which satisfy criteria given by `row` */
|
||||
export function getCoarseElementRangesForRow(row: MVSAnnotationRow, coarseElements: CoarseElements, indices: CoarseIndicesAndSortings): ElementRanges | undefined {
|
||||
const nElements = coarseElements.count;
|
||||
if (nElements === 0) return undefined;
|
||||
|
||||
const hasResidueFilter = isAnyDefined(row.label_seq_id, row.beg_label_seq_id, row.end_label_seq_id);
|
||||
const hasChainFilter = isAnyDefined(row.label_asym_id, row.label_entity_id);
|
||||
const hasInvalidFilter = isAnyDefined(
|
||||
row.auth_asym_id,
|
||||
row.auth_seq_id, row.pdbx_PDB_ins_code, row.beg_auth_seq_id, row.end_auth_seq_id, row.label_comp_id, row.auth_comp_id, row.residue_index,
|
||||
row.label_atom_id, row.auth_atom_id, row.type_symbol, row.atom_id, row.atom_index);
|
||||
|
||||
if (hasInvalidFilter) {
|
||||
printCoarseSelectorWarning();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!hasChainFilter && !hasResidueFilter) {
|
||||
return ElementRanges.single(0 as ElementIndex, nElements as ElementIndex);
|
||||
}
|
||||
|
||||
const qualifyingChains = getQualifyingCoarseChains(coarseElements, row, indices);
|
||||
if (!hasResidueFilter) {
|
||||
const chainOffsets = coarseElements.chainElementSegments.offsets;
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iChain of qualifyingChains) {
|
||||
ElementRanges.add(ranges, chainOffsets[iChain], chainOffsets[iChain + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingElements = getQualifyingCoarseElements(coarseElements, row, indices, qualifyingChains);
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iElem of qualifyingElements) {
|
||||
ElementRanges.add(ranges, iElem, iElem + 1 as ElementIndex);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
|
||||
/** Return an array of chain indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingCoarseChains(coarseElements: CoarseElements, row: MVSAnnotationRow, indices: CoarseIndicesAndSortings): readonly ChainIndex[] {
|
||||
let result: readonly ChainIndex[] | undefined = undefined;
|
||||
if (isDefined(row.label_asym_id)) {
|
||||
result = indices.chainsByAsymId.get(row.label_asym_id) ?? EmptyArray;
|
||||
}
|
||||
if (isDefined(row.label_entity_id)) {
|
||||
if (result) {
|
||||
result = result.filter(iChain => coarseElements.entity_id.value(coarseElements.chainElementSegments.offsets[iChain]) === row.label_entity_id);
|
||||
} else {
|
||||
result = indices.chainsByEntityId.get(row.label_entity_id) ?? EmptyArray;
|
||||
}
|
||||
}
|
||||
result ??= range(coarseElements.chainElementSegments.count) as ChainIndex[];
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return an array of residue indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingCoarseElements(coarseElements: CoarseElements, row: MVSAnnotationRow, indices: CoarseIndicesAndSortings, fromChains: readonly ChainIndex[]): ElementIndex[] {
|
||||
const result: ElementIndex[] = [];
|
||||
for (const iChain of fromChains) {
|
||||
const sorting = indices.elementsSortedBySeqIdBegin.get(iChain)!;
|
||||
const queryStart = Math.max(row.label_seq_id ?? -Infinity, row.beg_label_seq_id ?? -Infinity);
|
||||
const queryEnd = Math.min(row.label_seq_id ?? Infinity, row.end_label_seq_id ?? Infinity); // inclusive
|
||||
const iStart = SortedArray.findPredecessorIndex(sorting.endUpperBounds, queryStart); // select elements potentially ending >=queryStart (necessary condition)
|
||||
const iStop = SortedArray.findPredecessorIndex(sorting.values, queryEnd + 1); // select elements starting <=queryEnd (necessary and suffient condition) // exclusive
|
||||
|
||||
for (let i = iStart; i < iStop; i++) {
|
||||
const iElem = sorting.keys[i];
|
||||
if (coarseElements.seq_id_end.value(iElem) >= queryStart) { // rechecking seq_id_end, as the condition was not sufficient
|
||||
result.push(iElem);
|
||||
}
|
||||
}
|
||||
// This implementation can yield some elements even when queryStart>queryEnd (e.g. { beg_label_seq_id: 70, end_label_seq_id: 58, label_seq_id: 60 } -> sphere 51-100 qualifies ).
|
||||
// This is on purpose, to have the same behavior as MolScript.
|
||||
}
|
||||
sortIfNeeded(result, (a, b) => a - b);
|
||||
return result;
|
||||
}
|
||||
|
||||
let coarseSelectorWarningPrinted = false;
|
||||
function printCoarseSelectorWarning() {
|
||||
if (!coarseSelectorWarningPrinted) {
|
||||
console.warn('Using unsupported selector fields (auth_asym_id, auth_seq_id, pdbx_PDB_ins_code, beg_auth_seq_id, end_auth_seq_id, label_comp_id, auth_comp_id, residue_index, label_atom_id, auth_atom_id, type_symbol, atom_id, atom_index) on a coarse structure. The resulting selection will be empty.');
|
||||
coarseSelectorWarningPrinted = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// GENERAL
|
||||
|
||||
/** Convert an annotation row into a MolScript expression */
|
||||
export function rowToExpression(row: MVSAnnotationRow): Expression {
|
||||
return StructureElement.Schema.toExpression(row);
|
||||
@@ -311,43 +447,3 @@ export function rowsToExpression(rows: readonly MVSAnnotationRow[]): Expression
|
||||
items: rows as StructureElement.SchemaItem[]
|
||||
});
|
||||
}
|
||||
|
||||
/** Data structure for an array divided into contiguous groups */
|
||||
interface GroupedArray<T> {
|
||||
/** Number of groups */
|
||||
count: number,
|
||||
/** Get size of i-th group as `offsets[i+1]-offsets[i]`.
|
||||
* Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
offsets: number[],
|
||||
/** Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
grouped: T[],
|
||||
}
|
||||
|
||||
/** Return row indices grouped by `row.group_id`. Rows with `row.group_id===undefined` are treated as separate groups. */
|
||||
export function groupRows(rows: readonly MVSAnnotationRow[]): GroupedArray<number> {
|
||||
let counter = 0;
|
||||
const groupMap = new Map<string, number>();
|
||||
const groups: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const group_id = rows[i].group_id;
|
||||
if (!isDefined(group_id)) {
|
||||
groups.push(counter++);
|
||||
} else {
|
||||
const groupIndex = groupMap.get(group_id);
|
||||
if (groupIndex === undefined) {
|
||||
groupMap.set(group_id, counter);
|
||||
groups.push(counter);
|
||||
counter++;
|
||||
} else {
|
||||
groups.push(groupIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rowIndices = range(rows.length).sort((i, j) => groups[i] - groups[j]);
|
||||
const offsets: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (i === 0 || groups[rowIndices[i]] !== groups[rowIndices[i - 1]]) offsets.push(i);
|
||||
}
|
||||
offsets.push(rowIndices.length);
|
||||
return { count: offsets.length - 1, offsets, grouped: rowIndices };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import { hashString } from '../../../mol-data/util';
|
||||
import { StateObject } from '../../../mol-state';
|
||||
import { range } from '../../../mol-util/array';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { decodeColor as _decodeColor } from '../../../mol-util/color/utils';
|
||||
|
||||
|
||||
@@ -100,33 +100,13 @@ export type ElementOfSet<S> = S extends Set<infer T> ? T : never
|
||||
|
||||
/** Convert `colorString` (either X11 color name like 'magenta' or hex code like '#ff00ff') to Color.
|
||||
* Return `undefined` if `colorString` cannot be converted. */
|
||||
export function decodeColor(colorString: string | undefined | null): Color | undefined {
|
||||
export function decodeColor(colorString: string | number | undefined | null): Color | undefined {
|
||||
if (typeof colorString === 'number') {
|
||||
return Color(colorString);
|
||||
}
|
||||
return _decodeColor(colorString);
|
||||
}
|
||||
|
||||
/** Regular expression matching a hexadecimal color string, e.g. '#FF1100' or '#f10' */
|
||||
const hexColorRegex = /^#([0-9A-F]{3}){1,2}$/i;
|
||||
|
||||
/** Hexadecimal color string, e.g. '#FF1100' (the type matches more than just valid HexColor strings) */
|
||||
export type HexColor = `#${string}`
|
||||
|
||||
export const HexColor = {
|
||||
/** Decide if a string is a valid hexadecimal color string (6-digit or 3-digit, e.g. '#FF1100' or '#f10') */
|
||||
is(str: any): str is HexColor {
|
||||
return typeof str === 'string' && hexColorRegex.test(str);
|
||||
},
|
||||
};
|
||||
|
||||
/** Named color string, e.g. 'red' */
|
||||
export type ColorName = keyof ColorNames
|
||||
|
||||
export const ColorName = {
|
||||
/** Decide if a string is a valid named color string */
|
||||
is(str: any): str is ColorName {
|
||||
return str in ColorNames;
|
||||
},
|
||||
};
|
||||
|
||||
export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject>): Record<string, StateObject.From<T>['data']> {
|
||||
const ret: any = {};
|
||||
|
||||
@@ -149,4 +129,71 @@ export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], depe
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMVSReferenceObject<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject> | undefined, ref: string): StateObject | undefined {
|
||||
if (!dependencies) return undefined;
|
||||
|
||||
for (const key of Object.keys(dependencies)) {
|
||||
const o = dependencies[key];
|
||||
let okType = false;
|
||||
for (const t of type) {
|
||||
if (t.is(o)) {
|
||||
okType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!okType || !o.tags) continue;
|
||||
for (const tag of o.tags) {
|
||||
if (tag.startsWith('mvs-ref:')) {
|
||||
if (tag.substring(8) === ref) return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 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 };
|
||||
},
|
||||
};
|
||||
|
||||
8
src/extensions/mvs/index.ts
Normal file
8
src/extensions/mvs/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export * from './mvs-data';
|
||||
export * from './load';
|
||||
@@ -46,7 +46,7 @@ export function loadTreeVirtual<TTree extends Tree, TContext>(
|
||||
) {
|
||||
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
|
||||
loadTreeInUpdate(updateRoot, tree, loadingActions, context, options);
|
||||
const stateTree: StateTree = updateRoot.update.getTree();
|
||||
const stateTree: StateTree = updateRoot.update.getTree({ useHashVersion: true });
|
||||
const stateSnapshot: State.Snapshot = { tree: StateTree.toJSON(stateTree) };
|
||||
const pluginStateSnapshot: PluginState.Snapshot = { id: UUID.create22(), data: stateSnapshot };
|
||||
return pluginStateSnapshot;
|
||||
|
||||
@@ -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';
|
||||
@@ -62,6 +62,19 @@ export function transformFromRotationTranslation(rotation: number[] | null | und
|
||||
return T;
|
||||
}
|
||||
|
||||
export function decomposeRotationMatrix(rotation: number[] | null | undefined) {
|
||||
if (rotation && rotation.length !== 9) throw new Error(`'rotation' param for 'transform' node must be array of 9 elements, found ${rotation}`);
|
||||
if (rotation) {
|
||||
const rotMatrix = Mat3.fromArray(Mat3(), rotation, 0);
|
||||
ensureRotationMatrix(rotMatrix, rotMatrix);
|
||||
const quat = Quat.fromMat3(Quat(), rotMatrix);
|
||||
const axis = Vec3();
|
||||
const angle = Quat.getAxisAngle(axis, quat) * 180 / Math.PI;
|
||||
return { axis, angle };
|
||||
}
|
||||
return { axis: Vec3.create(1, 0, 0), angle: 0 };
|
||||
}
|
||||
|
||||
/** Adjust values in a close-to-rotation matrix `a` to ensure it is a proper rotation matrix
|
||||
* (i.e. its columns and rows are orthonormal and determinant equal to 1, within available precission). */
|
||||
function ensureRotationMatrix(out: Mat3, a: Mat3) {
|
||||
@@ -111,11 +124,32 @@ function transformProps(node: MolstarSubtree, kind: 'transform' | 'instance') {
|
||||
for (const transform of transforms) {
|
||||
let matrix: Mat4 | undefined = transform.params.matrix as Mat4 | undefined;
|
||||
if (!matrix) {
|
||||
const { rotation, translation } = transform.params;
|
||||
const { rotation, translation, rotation_center } = transform.params;
|
||||
if (rotation_center) {
|
||||
const axisAngle = decomposeRotationMatrix(rotation);
|
||||
result.push({
|
||||
params: {
|
||||
transform: {
|
||||
name: 'components',
|
||||
params: {
|
||||
translation: translation ? Vec3.fromArray(Vec3(), translation, 0) : Vec3.create(0, 0, 0),
|
||||
angle: axisAngle.angle,
|
||||
axis: axisAngle.axis,
|
||||
rotationCenter: rotation_center === 'centroid'
|
||||
? { name: 'centroid', params: {} }
|
||||
: { name: 'point', params: { point: Vec3.fromArray(Vec3(), rotation_center, 0) } }
|
||||
}
|
||||
}
|
||||
},
|
||||
ref: transform.ref
|
||||
});
|
||||
continue;
|
||||
}
|
||||
matrix = transformFromRotationTranslation(rotation, translation);
|
||||
}
|
||||
result.push({ params: { transform: { name: 'matrix', params: { data: matrix, transpose: false } } }, ref: transform.ref });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -162,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 });
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -185,7 +219,7 @@ export function collectInlineTooltips(tree: MolstarSubtree<'structure'>, context
|
||||
text: node.params.text,
|
||||
selector: {
|
||||
name: 'annotation',
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues },
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues, label: p.label || 'Annotation' },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -219,7 +253,7 @@ export function collectInlineLabels(tree: MolstarSubtree<'structure'>, context:
|
||||
params: {
|
||||
selector: {
|
||||
name: 'annotation',
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues },
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues, label: p.label || 'Annotation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -304,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),
|
||||
};
|
||||
}
|
||||
@@ -335,9 +371,20 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
type: { name: 'cartoon', params: { alpha, tubularHelices: params.tubular_helices } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'backbone':
|
||||
return {
|
||||
type: { name: 'backbone', params: { alpha } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
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 {
|
||||
type: { name: 'line', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'spacefill':
|
||||
return {
|
||||
@@ -346,13 +393,18 @@ 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':
|
||||
case 'surface': {
|
||||
return {
|
||||
type: { name: 'molecular-surface', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
type: {
|
||||
name: params.surface_type === 'gaussian' ? 'gaussian-surface' : 'molecular-surface',
|
||||
params: { alpha, ignoreHydrogens: params.ignore_hydrogens }
|
||||
},
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
@@ -364,8 +416,8 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
|
||||
if (clip) {
|
||||
base.type!.params = { ...base.type?.params, clip };
|
||||
}
|
||||
if (node.custom?.molstar_reprepresentation_params) {
|
||||
base.type!.params = { ...base.type!.params, ...node.custom.molstar_reprepresentation_params };
|
||||
if (node.custom?.molstar_representation_params) {
|
||||
base.type!.params = { ...base.type!.params, ...node.custom.molstar_representation_params };
|
||||
}
|
||||
return base;
|
||||
}
|
||||
@@ -431,7 +483,7 @@ function getClipObject(node: MolstarNode<'clip'>): Clip.Props['objects'][number]
|
||||
}
|
||||
}
|
||||
|
||||
export function clippingForNode(node: MolstarSubtree<'representation' | 'volume_representation'>): Clip.Props | undefined {
|
||||
export function clippingForNode(node: MolstarSubtree<'representation' | 'volume_representation' | 'primitives' | 'primitives_from_uri'>): Clip.Props | undefined {
|
||||
const children = getChildren(node).filter(c => c.kind === 'clip');
|
||||
if (!children.length) return;
|
||||
|
||||
@@ -446,8 +498,16 @@ function hasMolStarUseDefaultColoring(node: MolstarNode): boolean {
|
||||
return 'molstar_use_default_coloring' in node.custom || 'molstar_color_theme_name' in node.custom;
|
||||
}
|
||||
|
||||
function customColoring(custom: any) {
|
||||
if (custom?.molstar_use_default_coloring) return undefined;
|
||||
return {
|
||||
name: custom?.molstar_color_theme_name ?? undefined,
|
||||
params: custom?.molstar_color_theme_params ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */
|
||||
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] | undefined {
|
||||
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation' | 'volume'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] | undefined {
|
||||
if (node?.kind === 'representation') {
|
||||
const children = getChildren(node).filter(c => c.kind === 'color' || c.kind === 'color_from_uri' || c.kind === 'color_from_source') as MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>[];
|
||||
if (children.length === 0) {
|
||||
@@ -456,12 +516,7 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
params: { value: decodeColor(DefaultColor) },
|
||||
};
|
||||
} else if (children.length === 1 && hasMolStarUseDefaultColoring(children[0])) {
|
||||
if (children[0].custom?.molstar_use_default_coloring) return undefined;
|
||||
const custom = children[0].custom;
|
||||
return {
|
||||
name: custom?.molstar_color_theme_name ?? undefined,
|
||||
params: custom?.molstar_color_theme_params ?? {},
|
||||
};
|
||||
return customColoring(children[0].custom);
|
||||
} else if (children.length === 1 && appliesColorToWholeRepr(children[0])) {
|
||||
return colorThemeForNode(children[0], context);
|
||||
} else {
|
||||
@@ -469,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 {
|
||||
@@ -479,6 +534,10 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
}
|
||||
}
|
||||
if (node?.kind === 'color') {
|
||||
if (hasMolStarUseDefaultColoring(node)) {
|
||||
return customColoring(node.custom);
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'uniform',
|
||||
params: { value: decodeColor(node.params.color) },
|
||||
@@ -500,16 +559,12 @@ 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)!;
|
||||
|
||||
function palettePropsFromMVSPalette(palette: MolstarNode<'color_from_uri' | 'color_from_source'>['params']['palette']): MVSAnnotationColorThemeProps['palette'] {
|
||||
export function palettePropsFromMVSPalette(palette: MolstarNode<'color_from_uri' | 'color_from_source'>['params']['palette']): MVSAnnotationColorThemeProps['palette'] {
|
||||
if (!palette) {
|
||||
return { name: 'direct', params: {} };
|
||||
}
|
||||
|
||||
@@ -8,15 +8,18 @@
|
||||
|
||||
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { Download, ParseCcp4, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB } from '../../mol-plugin-state/transforms/model';
|
||||
import { Download, ParseCcp4, ParseCif, ParseDx, ParsePrmtop, ParsePsf, ParseTop } from '../../mol-plugin-state/transforms/data';
|
||||
import { CoordinatesFromDcd, CoordinatesFromLammpstraj, CoordinatesFromNctraj, CoordinatesFromTrr, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TopologyFromPrmtop, TopologyFromPsf, TopologyFromTop, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { VolumeFromCcp4, VolumeFromDensityServerCif } from '../../mol-plugin-state/transforms/volume';
|
||||
import { VolumeFromCcp4, VolumeFromDensityServerCif, VolumeFromDx } from '../../mol-plugin-state/transforms/volume';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectSelector, StateTree } from '../../mol-state';
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { MolViewSpec } from './behavior';
|
||||
import { createPluginStateSnapshotCamera, modifyCanvasProps } from './camera';
|
||||
import { createPluginStateSnapshotCamera, modifyCanvasProps, resetCanvasProps } from './camera';
|
||||
import { MVSAnnotationsProvider } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponent } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
|
||||
@@ -24,15 +27,18 @@ import { CustomLabelProps, CustomLabelRepresentationProvider } from './component
|
||||
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
|
||||
import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-prop';
|
||||
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData, MVSShapeRepresentation3D } from './components/primitives';
|
||||
import { MVSTrajectoryWithCoordinates } from './components/trajectory';
|
||||
import { generateStateTransition } from './helpers/animation';
|
||||
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
|
||||
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
|
||||
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, MVSData_States, SnapshotMetadata } from './mvs-data';
|
||||
import { validateTree } from './tree/generic/tree-schema';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, clippingForNode, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
|
||||
import { MVSAnimationNode, MVSAnimationSchema } from './tree/animation/animation-tree';
|
||||
import { validateTree } from './tree/generic/tree-validation';
|
||||
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
|
||||
import { type MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
export interface MVSLoadOptions {
|
||||
@@ -40,39 +46,65 @@ 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. */
|
||||
sanityChecks?: boolean,
|
||||
/** Base for resolving relative URLs/URIs. May itself be a relative URL (relative to the window URL). */
|
||||
sourceUrl?: string,
|
||||
|
||||
doNotReportErrors?: boolean
|
||||
}
|
||||
|
||||
export function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
const task = Task.create('Load MVS', ctx => _loadMVS(ctx, plugin, data, options));
|
||||
return plugin.runTask(task);
|
||||
}
|
||||
|
||||
/** Load a MolViewSpec (MVS) state(s) into the Mol* plugin as plugin state snapshots. */
|
||||
export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
plugin.errorContext.clear('mvs');
|
||||
try {
|
||||
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
|
||||
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
|
||||
|
||||
// Stop any currently running audio
|
||||
plugin.managers.markdownExtensions.audio.dispose();
|
||||
|
||||
// Reset canvas props to default so that modifyCanvasProps works as expected
|
||||
resetCanvasProps(plugin);
|
||||
|
||||
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
|
||||
const multiData: MVSData_States = data.kind === 'multiple' ? data : MVSData.stateToStates(data);
|
||||
const entries: PluginStateSnapshotManager.Entry[] = [];
|
||||
for (let i = 0; i < multiData.snapshots.length; i++) {
|
||||
const snapshot = multiData.snapshots[i];
|
||||
const previousSnapshot = i > 0 ? multiData.snapshots[i - 1] : multiData.snapshots[multiData.snapshots.length - 1];
|
||||
validateTree(MVSTreeSchema, snapshot.root, 'MVS');
|
||||
validateTree(MVSTreeSchema, snapshot.root, 'MVS', plugin);
|
||||
if (snapshot.animation) {
|
||||
validateTree(MVSAnimationSchema, snapshot.animation, 'Animation', plugin);
|
||||
}
|
||||
if (options.sanityChecks) mvsSanityCheck(snapshot.root);
|
||||
const molstarTree = convertMvsToMolstar(snapshot.root, options.sourceUrl);
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar', plugin);
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
snapshot.root,
|
||||
snapshot.animation,
|
||||
{ ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms },
|
||||
options
|
||||
);
|
||||
entries.push(entry);
|
||||
await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length);
|
||||
entries.push({
|
||||
...entry,
|
||||
_transientData: { sourceMvsSnapshot: snapshot }
|
||||
});
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'Loading MVS...', current: i, max: multiData.snapshots.length });
|
||||
}
|
||||
}
|
||||
if (!options.appendSnapshots) {
|
||||
plugin.managers.snapshot.clear();
|
||||
@@ -80,6 +112,7 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVS
|
||||
for (const entry of entries) {
|
||||
plugin.managers.snapshot.add(entry);
|
||||
}
|
||||
|
||||
if (entries.length > 0) {
|
||||
await PluginCommands.State.Snapshots.Apply(plugin, { id: entries[0].snapshot.id });
|
||||
}
|
||||
@@ -101,24 +134,71 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVS
|
||||
}
|
||||
}
|
||||
|
||||
async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext, parentEntry: PluginStateSnapshotManager.Entry, parent: Snapshot, options: MVSLoadOptions, snapshotIndex: number, snapshotCount: number) {
|
||||
const transitions = await generateStateTransition(ctx, parent, snapshotIndex, snapshotCount);
|
||||
if (!transitions?.frames.length) return;
|
||||
|
||||
const animation: PluginState.StateTransition = {
|
||||
autoplay: !!transitions.tree.params?.autoplay,
|
||||
loop: !!transitions.tree.params?.loop,
|
||||
frames: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < transitions.frames.length; i++) {
|
||||
const frame = transitions.frames[i];
|
||||
const molstarTree = convertMvsToMolstar(frame[0], options.sourceUrl);
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
parent.animation,
|
||||
{ ...parent.metadata, previousTransitionDurationMs: transitions.frametimeMs },
|
||||
options
|
||||
);
|
||||
|
||||
StateTree.reuseTransformParams(entry.snapshot.data!.tree, parentEntry.snapshot.data!.tree);
|
||||
|
||||
animation.frames.push({
|
||||
durationInMs: frame[1],
|
||||
data: entry.snapshot.data!,
|
||||
camera: transitions.tree.params?.include_camera ? entry.snapshot.camera : undefined,
|
||||
canvas3d: transitions.tree.params?.include_canvas ? entry.snapshot.canvas3d : undefined,
|
||||
});
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Loading animation for snapshot ${snapshotIndex + 1}/${snapshotCount}...`, current: i + 1, max: transitions.frames.length });
|
||||
}
|
||||
}
|
||||
|
||||
parentEntry.snapshot.transition = animation;
|
||||
}
|
||||
|
||||
function molstarTreeToEntry(
|
||||
plugin: PluginContext,
|
||||
tree: MolstarTree,
|
||||
mvsTree: MVSTree,
|
||||
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, mvsTree.custom) : undefined,
|
||||
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);
|
||||
|
||||
if (tree.custom?.molstar_on_load_markdown_commands) {
|
||||
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
|
||||
}
|
||||
|
||||
const entryParams: PluginStateSnapshotManager.EntryParams = {
|
||||
key: metadata.key,
|
||||
name: metadata.title,
|
||||
@@ -165,31 +245,112 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
},
|
||||
parse(updateParent: UpdateTarget, node: MolstarNode<'parse'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
} else if (format === 'pdb') {
|
||||
return updateParent;
|
||||
} else if (format === 'map') {
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
switch (format) {
|
||||
case 'cif':
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
case 'gro':
|
||||
case 'xyz':
|
||||
case 'mol':
|
||||
case 'sdf':
|
||||
case 'mol2':
|
||||
case 'xtc':
|
||||
case 'lammpstrj':
|
||||
case 'dcd':
|
||||
case 'nctraj':
|
||||
case 'trr':
|
||||
return updateParent;
|
||||
case 'psf':
|
||||
return UpdateTarget.apply(updateParent, ParsePsf, {});
|
||||
case 'prmtop':
|
||||
return UpdateTarget.apply(updateParent, ParsePrmtop, {});
|
||||
case 'top':
|
||||
return UpdateTarget.apply(updateParent, ParseTop, {});
|
||||
case 'map':
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
case 'dx':
|
||||
case 'dxbin':
|
||||
return UpdateTarget.apply(updateParent, ParseDx, {});
|
||||
default:
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
coordinates(updateParent: UpdateTarget, node: MolstarNode<'coordinates'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
switch (format) {
|
||||
case 'nctraj':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromNctraj);
|
||||
case 'dcd':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromDcd);
|
||||
case 'trr':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromTrr);
|
||||
case 'xtc':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromXtc);
|
||||
case 'lammpstrj':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromLammpstraj);
|
||||
default:
|
||||
console.error(`Unknown format in "coordinates" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
trajectory(updateParent: UpdateTarget, node: MolstarNode<'trajectory'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
|
||||
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
|
||||
blockIndex: node.params.block_index ?? undefined,
|
||||
});
|
||||
} else if (format === 'pdb') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "trajectory" node: "${format}"`);
|
||||
return undefined;
|
||||
switch (format) {
|
||||
case 'cif':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
|
||||
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
|
||||
blockIndex: node.params.block_index ?? undefined,
|
||||
});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { isPdbqt: format === 'pdbqt' });
|
||||
case 'gro':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromGRO);
|
||||
case 'xyz':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromXYZ);
|
||||
case 'mol':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMOL);
|
||||
case 'sdf':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromSDF);
|
||||
case 'mol2':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMOL2);
|
||||
case 'lammpstrj':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromLammpsTrajData);
|
||||
default:
|
||||
console.error(`Unknown format in "trajectory" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
trajectory_with_coordinates(updateParent: UpdateTarget, node: MolstarNode<'trajectory_with_coordinates'>): UpdateTarget | undefined {
|
||||
const result = UpdateTarget.apply(updateParent, MVSTrajectoryWithCoordinates, {
|
||||
coordinatesRef: node.params.coordinates_ref,
|
||||
});
|
||||
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
|
||||
},
|
||||
topology_with_coordinates(updateParent: UpdateTarget, node: MolstarNode<'topology_with_coordinates'>): UpdateTarget | undefined {
|
||||
let parsed: UpdateTarget;
|
||||
const format = node.params.format;
|
||||
switch (format) {
|
||||
case 'psf':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromPsf, {});
|
||||
break;
|
||||
case 'prmtop':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromPrmtop, {});
|
||||
break;
|
||||
case 'top':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromTop, {});
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown format in "topology_with_coordinates" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
const result = UpdateTarget.apply(parsed, MVSTrajectoryWithCoordinates, {
|
||||
coordinatesRef: node.params.coordinates_ref,
|
||||
});
|
||||
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
|
||||
},
|
||||
model(updateParent: UpdateTarget, node: MolstarSubtree<'model'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const annotations = collectAnnotationReferences(node, context);
|
||||
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {
|
||||
@@ -213,18 +374,16 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
const transformed = transformAndInstantiateStructure(struct, node);
|
||||
const annotationTooltips = collectAnnotationTooltips(node, context);
|
||||
const inlineTooltips = collectInlineTooltips(node, context);
|
||||
if (annotationTooltips.length + inlineTooltips.length > 0) {
|
||||
UpdateTarget.apply(struct, CustomStructureProperties, {
|
||||
properties: {
|
||||
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
|
||||
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
|
||||
},
|
||||
autoAttach: [
|
||||
MVSAnnotationTooltipsProvider.descriptor.name,
|
||||
CustomTooltipsProvider.descriptor.name,
|
||||
],
|
||||
});
|
||||
}
|
||||
UpdateTarget.apply(struct, CustomStructureProperties, {
|
||||
properties: {
|
||||
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
|
||||
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
|
||||
},
|
||||
autoAttach: [
|
||||
MVSAnnotationTooltipsProvider.descriptor.name,
|
||||
CustomTooltipsProvider.descriptor.name,
|
||||
],
|
||||
}); // CustomStructureProperties must be applied even when `annotationTooltips` and `inlineTooltips` are empty, otherwise tooltips would persists across MVS snapshots
|
||||
const inlineLabels = collectInlineLabels(node, context);
|
||||
if (inlineLabels.length > 0) {
|
||||
const nearestReprNode = context.nearestReprMap?.get(node);
|
||||
@@ -272,6 +431,8 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
let volume: UpdateTarget;
|
||||
if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Ccp4)) {
|
||||
volume = UpdateTarget.apply(updateParent, VolumeFromCcp4, {});
|
||||
} else if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Dx)) {
|
||||
volume = UpdateTarget.apply(updateParent, VolumeFromDx, {});
|
||||
} else if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Cif)) {
|
||||
volume = UpdateTarget.apply(updateParent, VolumeFromDensityServerCif, { blockHeader: node.params.channel_id || undefined });
|
||||
} else {
|
||||
@@ -312,21 +473,26 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
},
|
||||
primitives(updateParent: UpdateTarget, tree: MolstarSubtree<'primitives'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const refs = getPrimitiveStructureRefs(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree });
|
||||
return applyPrimitiveVisuals(data, refs);
|
||||
const clip = clippingForNode(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree as any });
|
||||
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
|
||||
return applyPrimitiveVisuals(data, refs, clip);
|
||||
},
|
||||
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const refs = new Set(tree.params.references);
|
||||
const clip = clippingForNode(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
|
||||
return applyPrimitiveVisuals(data, new Set(tree.params.references));
|
||||
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
|
||||
return applyPrimitiveVisuals(data, refs, clip);
|
||||
},
|
||||
};
|
||||
|
||||
function applyPrimitiveVisuals(data: UpdateTarget, refs: Set<string>) {
|
||||
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh' }, { state: { isGhost: true } }), refs);
|
||||
function applyPrimitiveVisuals(data: UpdateTarget, refs: Set<string>, clip: Clip.Props | undefined) {
|
||||
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh', clip }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(mesh, MVSShapeRepresentation3D);
|
||||
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels' }, { state: { isGhost: true } }), refs);
|
||||
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels', clip }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(labels, MVSShapeRepresentation3D);
|
||||
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines' }, { state: { isGhost: true } }), refs);
|
||||
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines', clip }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(lines, MVSShapeRepresentation3D);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* 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 Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { treeValidationIssues } from './tree/generic/tree-schema';
|
||||
import { treeValidationIssues } from './tree/generic/tree-validation';
|
||||
import { treeToString } from './tree/generic/tree-utils';
|
||||
import { MVSAnimationSchema, MVSAnimationTree } from './tree/animation/animation-tree';
|
||||
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
|
||||
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
@@ -53,6 +55,8 @@ export interface Snapshot {
|
||||
root: MVSTree,
|
||||
/** Associated metadata */
|
||||
metadata: SnapshotMetadata,
|
||||
/** Optional animation */
|
||||
animation?: MVSAnimationTree,
|
||||
}
|
||||
|
||||
/** MVSData with a single state */
|
||||
@@ -189,7 +193,14 @@ function majorVersion(semanticVersion: string | number): number | undefined {
|
||||
|
||||
function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: { noExtra?: boolean } = {}): string[] | undefined {
|
||||
if (snapshot.root === undefined) return [`"root" missing in snapshot`];
|
||||
return treeValidationIssues(MVSTreeSchema, snapshot.root, options);
|
||||
const state = treeValidationIssues(MVSTreeSchema, snapshot.root, options);
|
||||
const animation = 'animation' in snapshot && snapshot.animation !== undefined
|
||||
? treeValidationIssues(MVSAnimationSchema, snapshot.animation, options)
|
||||
: undefined;
|
||||
if (state && animation) return [...state, ...animation];
|
||||
if (state) return state;
|
||||
if (animation) return animation;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
|
||||
|
||||
195
src/extensions/mvs/tree/animation/animation-tree.ts
Normal file
195
src/extensions/mvs/tree/animation/animation-tree.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, int, list, OptionalField, RequiredField, str, union, nullable, literal, ValueFor, dict } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
|
||||
import { ColorT, ContinuousPalette, DiscretePalette, Matrix, Vector3 } from '../mvs/param-types';
|
||||
|
||||
type Easing =
|
||||
| 'linear'
|
||||
| 'bounce-in' | 'bounce-out' | 'bounce-in-out'
|
||||
| 'circle-in' | 'circle-out' | 'circle-in-out'
|
||||
| 'cubic-in' | 'cubic-out' | 'cubic-in-out'
|
||||
| 'exp-in' | 'exp-out' | 'exp-in-out'
|
||||
| 'quad-in' | 'quad-out' | 'quad-in-out'
|
||||
| 'sin-in' | 'sin-out' | 'sin-in-out'
|
||||
const Easing = literal<Easing>(
|
||||
'linear',
|
||||
'bounce-in', 'bounce-out', 'bounce-in-out',
|
||||
'circle-in', 'circle-out', 'circle-in-out',
|
||||
'cubic-in', 'cubic-out', 'cubic-in-out',
|
||||
'exp-in', 'exp-out', 'exp-in-out',
|
||||
'quad-in', 'quad-out', 'quad-in-out',
|
||||
'sin-in', 'sin-out', 'sin-in-out',
|
||||
);
|
||||
|
||||
export type MVSAnimationEasing = ValueFor<typeof Easing>;
|
||||
|
||||
const _Noise = {
|
||||
/** Magnitude of the noise to apply to the interpolated value. */
|
||||
noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the interpolated value.')
|
||||
// support cummulative noise?
|
||||
};
|
||||
|
||||
const _Common = {
|
||||
/** Reference to the node. */
|
||||
target_ref: RequiredField(str, 'Reference to the node.'),
|
||||
/** Value accessor. */
|
||||
property: RequiredField(union(str, list(union(str, int))), 'Value accessor.'),
|
||||
/** Start time of the transition in milliseconds. */
|
||||
start_ms: OptionalField(float, 0, 'Start time of the transition in milliseconds.'),
|
||||
/** Duration of the transition in milliseconds. */
|
||||
duration_ms: RequiredField(float, 'Duration of the transition in milliseconds.'),
|
||||
};
|
||||
|
||||
const _Frequency = {
|
||||
/** Determines how many times the interpolation loops. Current T = frequency * t mod 1. */
|
||||
frequency: OptionalField(int, 1, 'Determines how many times the interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
};
|
||||
|
||||
const _Easing = {
|
||||
/** Easing function to use for the transition. */
|
||||
easing: OptionalField(Easing, 'linear', 'Easing function to use for the transition.'),
|
||||
};
|
||||
|
||||
const ScalarInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If a list of values is provided, each element will be interpolated separately. If unset, parent state value is used. */
|
||||
start: OptionalField(nullable(union(float, list(float))), null, 'Start value. If a list of values is provided, each element will be interpolated separately. If unset, parent state value is used.'),
|
||||
/** End value. If a list of values is provided, each element will be interpolated separately. If unset, only noise is applied. */
|
||||
end: OptionalField(nullable(union(float, list(float))), null, 'End value. If a list of values is provided, each element will be interpolated separately. If unset, only noise is applied.'),
|
||||
/** Whether to round the values to the closest integer. Useful for example for trajectory animation. */
|
||||
discrete: OptionalField(bool, false, 'Whether to round the values to the closest integer. Useful for example for trajectory animation.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const Vec3Interpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If unset, parent state value is used. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...). */
|
||||
start: OptionalField(nullable(list(float)), null, 'Start value. If unset, parent state value is used. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...).'),
|
||||
/** End value. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...). If unset, only noise is applied. */
|
||||
end: OptionalField(nullable(list(float)), null, 'End value. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...). If unset, only noise is applied.'),
|
||||
/** Whether to use spherical interpolation. */
|
||||
spherical: OptionalField(bool, false, 'Whether to use spherical interpolation.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const RotationMatrixInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If unset, parent state value is used. */
|
||||
start: OptionalField(nullable(Matrix), null, 'Start value. If unset, parent state value is used.'),
|
||||
/** End value. If unset, only noise is applied. */
|
||||
end: OptionalField(nullable(Matrix), null, 'End value. If unset, only noise is applied.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const ColorInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If unset, parent state value is used. */
|
||||
start: OptionalField(union(nullable(ColorT), dict(union(int, str), ColorT)), null, 'Start value. If unset, parent state value is used.'),
|
||||
/** End value. */
|
||||
end: OptionalField(union(nullable(ColorT), dict(union(int, str), ColorT)), null, 'End value.'),
|
||||
/** Palette to sample colors from. Overrides start and end values. */
|
||||
palette: OptionalField(nullable(union(DiscretePalette, ContinuousPalette)), null, 'Palette to sample colors from. Overrides start and end values.'),
|
||||
};
|
||||
|
||||
const TransformationMatrixInterpolation = {
|
||||
..._Common,
|
||||
/** Pivot point for rotation and scale. */
|
||||
pivot: OptionalField(nullable(Vector3), null, 'Pivot point for rotation and scale.'),
|
||||
/** Start rotation value. If unset, parent state value is used. */
|
||||
rotation_start: OptionalField(nullable(Matrix), null, 'Start rotation value. If unset, parent state value is used.'),
|
||||
/** End rotation value. If unset, only noise is applied */
|
||||
rotation_end: OptionalField(nullable(Matrix), null, 'End rotation value. If unset, only noise is applied.'),
|
||||
/** Magnitude of the noise to apply to the rotation. */
|
||||
rotation_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the rotation.'),
|
||||
/** Easing function to use for the rotation. */
|
||||
rotation_easing: OptionalField(Easing, 'linear', 'Easing function to use for the rotation.'),
|
||||
/** Determines how many times the rotation interpolation loops. Current T = frequency * t mod 1. */
|
||||
rotation_frequency: OptionalField(int, 1, 'Determines how many times the rotation interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
rotation_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
/** Start translation value. If unset, parent state value is used. */
|
||||
translation_start: OptionalField(nullable(Vector3), null, 'Start translation value. If unset, parent state value is used.'),
|
||||
/** End translation value. If unset, only noise is applied. */
|
||||
translation_end: OptionalField(nullable(Vector3), null, 'End translation value. If unset, only noise is applied.'),
|
||||
/** Magnitude of the noise to apply to the translation. */
|
||||
translation_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the translation.'),
|
||||
/** Easing function to use for the translation. */
|
||||
translation_easing: OptionalField(Easing, 'linear', 'Easing function to use for the translation.'),
|
||||
/** Determines how many times the translation interpolation loops. Current T = frequency * t mod 1. */
|
||||
translation_frequency: OptionalField(int, 1, 'Determines how many times the translation interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
translation_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
/** Start scale value. If unset, parent state value is used. */
|
||||
scale_start: OptionalField(nullable(Vector3), null, 'Start scale value. If unset, parent state value is used.'),
|
||||
/** End scale value. If unset, only noise is applied. */
|
||||
scale_end: OptionalField(nullable(Vector3), null, 'End scale value. If unset, only noise is applied.'),
|
||||
/** Magnitude of the noise to apply to the scale. */
|
||||
scale_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the scale.'),
|
||||
/** Easing function to use for the scale. */
|
||||
scale_easing: OptionalField(Easing, 'linear', 'Easing function to use for the scale.'),
|
||||
/** Determines how many times the scale interpolation loops. Current T = frequency * t mod 1. */
|
||||
scale_frequency: OptionalField(int, 1, 'Determines how many times the scale interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
scale_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
};
|
||||
|
||||
export const MVSAnimationSchema = TreeSchema({
|
||||
rootKind: 'animation',
|
||||
nodes: {
|
||||
animation: {
|
||||
description: 'Animation root node',
|
||||
parent: [],
|
||||
params: SimpleParamsSchema({
|
||||
/** Frame time in milliseconds. */
|
||||
frame_time_ms: OptionalField(float, 1000 / 60, 'Frame time in milliseconds.'),
|
||||
/** Total duration of the animation. If not specified, computed as maximum of all transitions. */
|
||||
duration_ms: OptionalField(nullable(float), null, 'Total duration of the animation. If not specified, computed as maximum of all transitions.'),
|
||||
/** Determines whether the animation should autoplay when a snapshot is loaded */
|
||||
autoplay: OptionalField(bool, true, 'Determines whether the animation should autoplay when a snapshot is loaded.'),
|
||||
/** Determines whether the animation should loop when it reaches the end. */
|
||||
loop: OptionalField(bool, false, 'Determines whether the animation should loop when it reaches the end.'),
|
||||
/** Determines whether the camera state should be included in the animation. */
|
||||
include_camera: OptionalField(bool, false, 'Determines whether the camera state should be included in the animation.'),
|
||||
/** Determines whether the canvas state should be included in the animation. */
|
||||
include_canvas: OptionalField(bool, false, 'Determines whether the canvas state should be included in the animation.'),
|
||||
}),
|
||||
},
|
||||
interpolate: {
|
||||
description: 'This node enables interpolating between values',
|
||||
parent: ['animation'],
|
||||
params: UnionParamsSchema(
|
||||
'kind',
|
||||
'Interpolation kind',
|
||||
{
|
||||
scalar: SimpleParamsSchema(ScalarInterpolation),
|
||||
vec3: SimpleParamsSchema(Vec3Interpolation),
|
||||
rotation_matrix: SimpleParamsSchema(RotationMatrixInterpolation),
|
||||
transform_matrix: SimpleParamsSchema(TransformationMatrixInterpolation),
|
||||
color: SimpleParamsSchema(ColorInterpolation),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type MVSAnimationKind = keyof typeof MVSAnimationSchema.nodes
|
||||
export type MVSAnimationNode<TKind extends MVSAnimationKind = MVSAnimationKind> = NodeFor<typeof MVSAnimationSchema, TKind>
|
||||
export type MVSAnimationTree = TreeFor<typeof MVSAnimationSchema>
|
||||
export type MVSAnimationNodeParams<TKind extends MVSAnimationKind> = ParamsOfKind<MVSAnimationTree, TKind>
|
||||
export type MVSAnimationSubtree<TKind extends MVSAnimationKind = MVSAnimationKind> = SubtreeOfKind<MVSAnimationTree, TKind>
|
||||
@@ -6,10 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
import { PathReporter } from "io-ts/lib/PathReporter.js";
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
|
||||
|
||||
/** All types that can be used in tree node params.
|
||||
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
|
||||
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {};
|
||||
@@ -80,7 +78,16 @@ export function nullable<V>(type: iots.Type<V>): iots.Type<V | null> {
|
||||
return union(type, iots.null);
|
||||
}
|
||||
|
||||
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
|
||||
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue'.
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* export type MyColor = 'red' | 'green' | 'blue';
|
||||
* export const MyColor = literal<MyColor>('red', 'green', 'blue');
|
||||
* ```
|
||||
*
|
||||
* (it looks stupid to repeat the list of values but it will result in nicer type bundle (for MolViewStories))
|
||||
*/
|
||||
export function literal<V extends string | number | boolean>(...values: V[]) {
|
||||
if (values.length === 0) {
|
||||
throw new Error(`literal type must have at least one value`);
|
||||
@@ -138,10 +145,46 @@ 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;
|
||||
} else {
|
||||
return PathReporter.report(validation);
|
||||
return reportErrors(validation.left);
|
||||
}
|
||||
}
|
||||
|
||||
// Inlining `reportErrors` instead of `import { PathReporter } from 'io-ts/PathReporter'`;
|
||||
// because it breaks Deno usage.
|
||||
|
||||
function reportErrors(errors: iots.Errors): string[] | undefined {
|
||||
if (errors.length === 0) return undefined;
|
||||
return errors.map(getMessage);
|
||||
}
|
||||
|
||||
function getMessage(e: iots.ValidationError) {
|
||||
return e.message !== undefined
|
||||
? e.message
|
||||
: `Invalid value ${stringifyError(e.value)} supplied to ${getContextPath(e.context)}`;
|
||||
}
|
||||
|
||||
function getContextPath(context: iots.ValidationError['context']) {
|
||||
return context.map(a => `${a.key}: ${a.type.name}`).join('/');
|
||||
}
|
||||
|
||||
function getFunctionName(f: Function & { displayName?: string }) {
|
||||
return f.displayName || f.name || `<function ${f.length}>`;
|
||||
}
|
||||
|
||||
function stringifyError(v: any) {
|
||||
if (typeof v === 'function') {
|
||||
return getFunctionName(v);
|
||||
}
|
||||
if (typeof v === 'number' && !isFinite(v)) {
|
||||
if (isNaN(v)) {
|
||||
return 'NaN';
|
||||
}
|
||||
return v > 0 ? 'Infinity' : '-Infinity';
|
||||
}
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
@@ -4,12 +4,8 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
|
||||
import { Field } from './field-schema';
|
||||
import { AllRequired, ParamsSchema, SimpleParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
|
||||
import { treeToString } from './tree-utils';
|
||||
|
||||
import { mapObjectMap } from '../../../../mol-util/object';
|
||||
import { AllRequired, ParamsSchema, ValuesFor } from './params-schema';
|
||||
|
||||
/** Type of "custom" of a tree node (key-value storage with arbitrary JSONable values) */
|
||||
export type CustomProps = Partial<Record<string, any>>
|
||||
@@ -114,120 +110,3 @@ export type NodeFor<TTreeSchema extends TreeSchema, TKind extends keyof ParamsSc
|
||||
|
||||
/** Type of tree which conforms to tree schema `TTreeSchema` */
|
||||
export type TreeFor<TTreeSchema extends TreeSchema> = Tree<NodeFor<TTreeSchema>, RootFor<TTreeSchema> & NodeFor<TTreeSchema>>
|
||||
|
||||
|
||||
/** Return `undefined` if a tree conforms to the given schema,
|
||||
* return validation issues (as a list of lines) if it does not conform.
|
||||
* If `options.requireAll`, all parameters (including optional) must have a value provided.
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
|
||||
* If `options.anyRoot` is true, the kind of the root node is not enforced.
|
||||
*/
|
||||
export function treeValidationIssues(schema: TreeSchema, tree: Tree, options: { requireAll?: boolean, noExtra?: boolean, anyRoot?: boolean, parent?: string } = {}): string[] | undefined {
|
||||
if (!isPlainObject(tree)) return [`Node must be an object, not ${tree}`];
|
||||
if (!options.anyRoot && tree.kind !== schema.rootKind) return [`Invalid root node kind "${tree.kind}", root must be of kind "${schema.rootKind}"`];
|
||||
const nodeSchema = schema.nodes[tree.kind];
|
||||
if (!nodeSchema) return [`Unknown node kind "${tree.kind}"`];
|
||||
if (nodeSchema.parent && (options.parent !== undefined) && !nodeSchema.parent.includes(options.parent)) {
|
||||
return [`Node of kind "${tree.kind}" cannot appear as a child of "${options.parent}". Allowed parents for "${tree.kind}" are: ${nodeSchema.parent.map(s => `"${s}"`).join(', ')}`];
|
||||
}
|
||||
const issues = paramsValidationIssues(nodeSchema.params, getParams(tree), options);
|
||||
if (issues) return [`Invalid parameters for node of kind "${tree.kind}":`, ...issues.map(s => ' ' + s)];
|
||||
if (tree.custom !== undefined && (typeof tree.custom !== 'object' || tree.custom === null)) {
|
||||
return [`Invalid "custom" for node of kind "${tree.kind}": must be an object, not ${tree.custom}.`];
|
||||
}
|
||||
for (const child of getChildren(tree)) {
|
||||
const issues = treeValidationIssues(schema, child, { ...options, anyRoot: true, parent: tree.kind });
|
||||
if (issues) return issues;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Validate a tree against the given schema.
|
||||
* Do nothing if OK; print validation issues on console and throw an error is the tree does not conform.
|
||||
* Include `label` in the printed output. */
|
||||
export function validateTree(schema: TreeSchema, tree: Tree, label: string): void {
|
||||
const issues = treeValidationIssues(schema, tree, { noExtra: true });
|
||||
if (issues) {
|
||||
console.warn(`Invalid ${label} tree:\n${treeToString(tree)}`);
|
||||
console.error(`${label} tree validation issues:`);
|
||||
for (const line of issues) {
|
||||
console.error(' ', line);
|
||||
}
|
||||
throw new Error('FormatError');
|
||||
}
|
||||
}
|
||||
|
||||
/** Return documentation for a tree schema as plain text */
|
||||
export function treeSchemaToString<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, false);
|
||||
}
|
||||
/** Return documentation for a tree schema as markdown text */
|
||||
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, true);
|
||||
}
|
||||
function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean = false): string {
|
||||
const out: string[] = [];
|
||||
const bold = (str: string) => markdown ? `**${str}**` : str;
|
||||
const code = (str: string) => markdown ? `\`${str}\`` : str;
|
||||
const h1 = markdown ? '## ' : ' - ';
|
||||
const p1 = markdown ? '' : ' ';
|
||||
const h2 = markdown ? '- ' : ' - ';
|
||||
const p2 = markdown ? ' ' : ' ';
|
||||
const h3 = markdown ? ' - ' : ' - ';
|
||||
const p3 = markdown ? ' ' : ' ';
|
||||
const newline = markdown ? '\n\n' : '\n';
|
||||
out.push(`Tree schema:`);
|
||||
for (const kind in schema.nodes) {
|
||||
const { description, params, parent } = schema.nodes[kind];
|
||||
out.push(`${h1}${code(kind)}`);
|
||||
if (kind === schema.rootKind) {
|
||||
out.push(`${p1}[Root of the tree must be of this kind]`);
|
||||
}
|
||||
if (description) {
|
||||
out.push(`${p1}${description}`);
|
||||
}
|
||||
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
|
||||
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
|
||||
if (params.type === 'simple') {
|
||||
formatSimpleParams(out, params, { h: h2, p: p2, code, bold });
|
||||
} else {
|
||||
const key = params.discriminator;
|
||||
const casesStr = Object.keys(params.cases).join(' | ');
|
||||
out.push(`${h2}${bold(code(key + ': '))}${code(casesStr)}`);
|
||||
if (params.discriminatorDescription) {
|
||||
out.push(`${p2}${params.discriminatorDescription}`);
|
||||
}
|
||||
out.push(`${p2}[This parameter determines the rest of parameters]`);
|
||||
for (const case_ in params.cases) {
|
||||
const caseStr = `${params.discriminator}: "${case_}"`;
|
||||
out.push(`${p2}${bold(`Case ${code(caseStr)}:`)}`);
|
||||
formatSimpleParams(out, params.cases[case_], { h: h3, p: p3, code, bold });
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.join(newline);
|
||||
}
|
||||
|
||||
function formatSimpleParams(out: string[], params: SimpleParamsSchema, formatting: { h: string, p: string, code: (str: string) => string, bold: (str: string) => string }): void {
|
||||
const { h, p, code, bold } = formatting;
|
||||
for (const key in params.fields) {
|
||||
const field = params.fields[key];
|
||||
out.push(`${h}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(formatFieldType(field))}`);
|
||||
const defaultValue = field.required ? undefined : field.default;
|
||||
if (field.description) {
|
||||
out.push(`${p}${field.description}`);
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
out.push(`${p}Default: ${code(onelinerJsonString(defaultValue))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatFieldType(field: Field): string {
|
||||
const typeString = field.type.name;
|
||||
if (typeString.startsWith('(') && typeString.endsWith(')')) {
|
||||
return typeString.slice(1, -1);
|
||||
} else {
|
||||
return typeString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function copyTree<T extends Tree>(root: T): T {
|
||||
* nodes of kind `C` will be converted to `Y` with a child `Z` (original children moved to `Z`),
|
||||
* nodes of other kinds will just be copied. */
|
||||
export type ConversionRules<A extends Tree, B extends Tree> = {
|
||||
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => Subtree<B>[]
|
||||
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => { subtree: Subtree<B>[] }
|
||||
};
|
||||
|
||||
/** Apply a set of conversion rules to a tree to change to a different schema. */
|
||||
@@ -94,12 +94,12 @@ export function convertTree<A extends Tree, B extends Tree>(root: A, conversions
|
||||
const mapping = new Map<Subtree<A>, Subtree<B>>();
|
||||
let convertedRoot: Subtree<B>;
|
||||
dfs<A>(root, (node, parent) => {
|
||||
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => Subtree<B>[]) | undefined;
|
||||
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => { subtree: Subtree<B>[] }) | undefined;
|
||||
if (conversion) {
|
||||
const convertidos = conversion(node, parent);
|
||||
if (!parent && convertidos.length === 0) throw new Error('Cannot convert root to empty path');
|
||||
const converted = conversion(node, parent);
|
||||
if (!parent && converted?.subtree.length === 0) throw new Error('Cannot convert root to empty path');
|
||||
let convParent = parent ? mapping.get(parent) : undefined;
|
||||
for (const conv of convertidos) {
|
||||
for (const conv of converted.subtree) {
|
||||
if (convParent) {
|
||||
(convParent.children ??= []).push(conv);
|
||||
} else {
|
||||
@@ -153,12 +153,14 @@ export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, treeSchema:
|
||||
type TTree = TreeFor<S>;
|
||||
const rules: ConversionRules<TTree, TTree> = {};
|
||||
for (const kind in treeSchema.nodes) {
|
||||
rules[kind as Kind<Subtree<TTree>>] = node => [{
|
||||
kind: node.kind,
|
||||
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
|
||||
custom: node.custom,
|
||||
ref: node.ref,
|
||||
} as Node as any];
|
||||
rules[kind as Kind<Subtree<TTree>>] = node => ({
|
||||
subtree: [{
|
||||
kind: node.kind,
|
||||
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
|
||||
custom: node.custom,
|
||||
ref: node.ref,
|
||||
} as Node as any]
|
||||
});
|
||||
}
|
||||
return convertTree(tree, rules) as any;
|
||||
}
|
||||
|
||||
125
src/extensions/mvs/tree/generic/tree-validation.ts
Normal file
125
src/extensions/mvs/tree/generic/tree-validation.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
import { isPlainObject } from '../../../../mol-util/object';
|
||||
import { Field } from './field-schema';
|
||||
import { SimpleParamsSchema, paramsValidationIssues } from './params-schema';
|
||||
import { getChildren, getParams, Tree, TreeSchema } from './tree-schema';
|
||||
import { treeToString } from './tree-utils';
|
||||
|
||||
/** Return `undefined` if a tree conforms to the given schema,
|
||||
* return validation issues (as a list of lines) if it does not conform.
|
||||
* If `options.requireAll`, all parameters (including optional) must have a value provided.
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
|
||||
* If `options.anyRoot` is true, the kind of the root node is not enforced.
|
||||
*/
|
||||
export function treeValidationIssues(schema: TreeSchema, tree: Tree, options: { requireAll?: boolean, noExtra?: boolean, anyRoot?: boolean, parent?: string } = {}): string[] | undefined {
|
||||
if (!isPlainObject(tree)) return [`Node must be an object, not ${tree}`];
|
||||
if (!options.anyRoot && tree.kind !== schema.rootKind) return [`Invalid root node kind "${tree.kind}", root must be of kind "${schema.rootKind}"`];
|
||||
const nodeSchema = schema.nodes[tree.kind];
|
||||
if (!nodeSchema) return [`Unknown node kind "${tree.kind}"`];
|
||||
if (nodeSchema.parent && (options.parent !== undefined) && !nodeSchema.parent.includes(options.parent)) {
|
||||
return [`Node of kind "${tree.kind}" cannot appear as a child of "${options.parent}". Allowed parents for "${tree.kind}" are: ${nodeSchema.parent.map(s => `"${s}"`).join(', ')}`];
|
||||
}
|
||||
const issues = paramsValidationIssues(nodeSchema.params, getParams(tree), options);
|
||||
if (issues) return [`Invalid parameters for node of kind "${tree.kind}":`, ...issues.map(s => ' ' + s)];
|
||||
if (tree.custom !== undefined && (typeof tree.custom !== 'object' || tree.custom === null)) {
|
||||
return [`Invalid "custom" for node of kind "${tree.kind}": must be an object, not ${tree.custom}.`];
|
||||
}
|
||||
for (const child of getChildren(tree)) {
|
||||
const issues = treeValidationIssues(schema, child, { ...options, anyRoot: true, parent: tree.kind });
|
||||
if (issues) return issues;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Validate a tree against the given schema.
|
||||
* Do nothing if OK; print validation issues on console and throw an error is the tree does not conform.
|
||||
* Include `label` in the printed output. */
|
||||
export function validateTree(schema: TreeSchema, tree: Tree, label: string, plugin: PluginContext): void {
|
||||
const issues = treeValidationIssues(schema, tree, { noExtra: true });
|
||||
if (issues) {
|
||||
console.warn(`Invalid ${label} tree:\n${treeToString(tree)}`);
|
||||
console.error(`${label} tree validation issues:`);
|
||||
plugin.log.error(`${label} tree validation issues:`);
|
||||
for (const line of issues) {
|
||||
console.error(' ', line);
|
||||
plugin.log.error(line);
|
||||
}
|
||||
throw new Error('FormatError');
|
||||
}
|
||||
}
|
||||
|
||||
/** Return documentation for a tree schema as plain text */
|
||||
export function treeSchemaToString<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, false);
|
||||
}
|
||||
/** Return documentation for a tree schema as markdown text */
|
||||
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, true);
|
||||
}
|
||||
function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean = false): string {
|
||||
const out: string[] = [];
|
||||
const bold = (str: string) => markdown ? `**${str}**` : str;
|
||||
const code = (str: string) => markdown ? `\`${str}\`` : str;
|
||||
const h1 = markdown ? '## ' : ' - ';
|
||||
const p1 = markdown ? '' : ' ';
|
||||
const h2 = markdown ? '- ' : ' - ';
|
||||
const p2 = markdown ? ' ' : ' ';
|
||||
const h3 = markdown ? ' - ' : ' - ';
|
||||
const p3 = markdown ? ' ' : ' ';
|
||||
const newline = markdown ? '\n\n' : '\n';
|
||||
out.push(`Tree schema:`);
|
||||
for (const kind in schema.nodes) {
|
||||
const { description, params, parent } = schema.nodes[kind];
|
||||
out.push(`${h1}${code(kind)}`);
|
||||
if (kind === schema.rootKind) {
|
||||
out.push(`${p1}[Root of the tree must be of this kind]`);
|
||||
}
|
||||
if (description) {
|
||||
out.push(`${p1}${description}`);
|
||||
}
|
||||
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
|
||||
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
|
||||
if (params.type === 'simple') {
|
||||
formatSimpleParams(out, params, { h: h2, p: p2, code, bold });
|
||||
} else {
|
||||
const key = params.discriminator;
|
||||
const casesStr = Object.keys(params.cases).join(' | ');
|
||||
out.push(`${h2}${bold(code(key + ': '))}${code(casesStr)}`);
|
||||
if (params.discriminatorDescription) {
|
||||
out.push(`${p2}${params.discriminatorDescription}`);
|
||||
}
|
||||
out.push(`${p2}[This parameter determines the rest of parameters]`);
|
||||
for (const case_ in params.cases) {
|
||||
const caseStr = `${params.discriminator}: "${case_}"`;
|
||||
out.push(`${p2}${bold(`Case ${code(caseStr)}:`)}`);
|
||||
formatSimpleParams(out, params.cases[case_], { h: h3, p: p3, code, bold });
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.join(newline);
|
||||
}
|
||||
|
||||
function formatSimpleParams(out: string[], params: SimpleParamsSchema, formatting: { h: string, p: string, code: (str: string) => string, bold: (str: string) => string }): void {
|
||||
const { h, p, code, bold } = formatting;
|
||||
for (const key in params.fields) {
|
||||
const field = params.fields[key];
|
||||
out.push(`${h}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(formatFieldType(field))}`);
|
||||
const defaultValue = field.required ? undefined : field.default;
|
||||
if (field.description) {
|
||||
out.push(`${p}${field.description}`);
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
out.push(`${p}Default: ${code(onelinerJsonString(defaultValue))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatFieldType(field: Field): string {
|
||||
const typeString = field.type.name;
|
||||
if (typeString.startsWith('(') && typeString.endsWith(')')) {
|
||||
return typeString.slice(1, -1);
|
||||
} else {
|
||||
return typeString;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user