Compare commits

...

129 Commits

Author SHA1 Message Date
dsehnal
7f8be5b8c6 4.14.1 2025-05-09 07:02:52 +02:00
David Sehnal
2ab6e4b2e7 No error in Transformer.create (#1512)
* No error in Transformer.create

* header
2025-05-09 06:59:15 +02:00
dsehnal
4a88546181 changelog 2025-05-07 16:08:17 +02:00
dsehnal
edbc70cf6e 4.14.0 2025-05-07 16:07:49 +02:00
dsehnal
c22ad2910c npm audit 2025-05-07 16:04:36 +02:00
dsehnal
28a2b52e3c changelog 2025-05-07 16:03:53 +02:00
Gianluca Tomasello
470227af43 Avoid grid expansion when requiring unit cell on volume server (#1502)
* Avoid grid expansion when requiring unit cell on volume server

* Increment version and use specific changelog

* change header
2025-05-07 14:49:08 +02:00
dsehnal
a0ccf46939 remove extra import 2025-05-05 08:10:03 +02:00
dsehnal
0ce8931fc5 Fix switching representation type in Volume UI 2025-05-04 18:28:19 +02:00
David Sehnal
3ddb29fc6f Add format selection option to image export UI (#1504)
* Add format selection option to image export UI

* check if webp is supported
2025-05-04 14:54:17 +02:00
David Sehnal
f34f879cf1 MVS: support updating transform states (#1505)
* MVS: support updating transform states

* changelog

* changelog

* pr feedback

* util functions & is_hidden extension
2025-05-02 12:53:38 +02:00
David Sehnal
6da9557531 Fix StructConn.residueCantorPairs (#1500) 2025-05-01 11:29:24 +02:00
Paul Lewallen
29e6d69d21 Update package.json (#1497)
Add engines field to specify required Node.js version (>=18.0.0)
2025-04-26 07:46:50 +02:00
David Sehnal
6b2b87e6c5 fix Viewer.loadTrajectory (#1496) 2025-04-24 10:44:59 +02:00
dsehnal
5299d5c0c4 changelog 2025-04-14 18:37:52 +02:00
dsehnal
7bab95f4cc 4.13.0 2025-04-14 18:04:43 +02:00
David Sehnal
35e78ce638 MVS Stories (#1484)
* ability to select a story

* rename to mvs-stories

* update kinase story

* fix ae example

* tweaks

* tweak
2025-04-14 18:00:37 +02:00
Chetan Mishra
3abbcb6949 added toggle for hiding screenshot controls (#1483)
* added toggle for hiding screenshot button

* whoops did the todolist but forgot to commit

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2025-04-14 14:38:47 +02:00
David Sehnal
c3fc893ad0 mvs: fix state builder for volumes (#1485) 2025-04-13 13:07:52 +02:00
midlik
80415a2771 MVS: support loading extensions when loading multistate files (#1470)
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2025-04-01 07:15:26 +02:00
etongfu
bfef69e2e4 Dev script update (#1473)
* feat: support host option for build-dev script

* feat: ust update the code related to the host

---------

Co-authored-by: tongfu.e <tongfu.e@xtalpi.com>
2025-04-01 07:14:39 +02:00
David Sehnal
a265a579be Update support for QA metrics (#1476)
* generalize ma_qa_metric support

* do not assume PAE is symmetric

* changelog

* Update src/extensions/model-archive/quality-assessment/prop.ts

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>

* Update src/extensions/model-archive/quality-assessment/prop.ts

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>

* lint

---------

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>
2025-03-31 18:51:18 +02:00
ludovic autin
c2af1b0b22 Colors from user and file (#1466)
* color, and granularity for selectrion.

* Load json colors per ingredient {"name":{"x":0,"y":0,"z":0}}

* change icon

* use functional components and simple function.
2025-03-28 18:51:42 +01:00
Sagar Pathak
b739876726 Update index.md (#1472)
Fix command typo: changed 'npm build' to 'npm run build'
2025-03-24 17:57:01 +01:00
David Sehnal
56851cc328 Improve struct_conn handling (#1468)
* improve struct_conn handling

* fix

* fix

* check same model

* Update src/mol-model-formats/structure/property/bonds/struct_conn.ts

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>

---------

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>
2025-03-24 16:49:11 +01:00
dsehnal
b719568555 mobile-friendly Kinase Story 2025-03-24 08:47:42 +01:00
Alexander Rose
f842f19912 Merge branch 'master' of https://github.com/molstar/molstar 2025-03-23 16:28:55 -07:00
Alexander Rose
d6b0dd910c guard against overly large grids 2025-03-23 16:28:52 -07:00
Alexander Rose
395eb8e8d0 Merge pull request #1454 from agdturner/pwa
For viewer to be installable as a Progressive Web App with a Service …
2025-03-23 16:28:03 -07:00
Alexander Rose
e4376c6737 try fix package-lock 2025-03-23 16:10:03 -07:00
Alexander Rose
6f23fcba8a Merge branch 'master' of https://github.com/molstar/molstar into pr/agdturner/1454 2025-03-23 16:06:17 -07:00
dsehnal
7d19bfdb9b Tweaks: kinase story edgecase fix, do not include volseg extension in Viewer by default 2025-03-17 09:30:30 +01:00
David Sehnal
6e4065f779 Interactions extension (#1463)
* move schema based element addressing from MVS to the core library

* proof of concept

* tweak

* initial visual

* tweak

* tweak structureElementLocationToSchemaItem

* show receptor residues as ball and stick

* wip data model

* custom interactions support

* add example

* wip visuals

* covalent links

* tweak

* fix bug

* cache unit features

* Add structureRef to Molecule.Structure.Selections

* optimize compute

* support multi-structure selections

* analyzeTrajectory support

* readme

* tweak

* headers

* docs

* Optimize StructureElementSchema & remove fromLoci conversion (not production ready for now, will be separate PR)

* move schema code to `StructureElement.Schema` and add fromExpression/Query/Schame functions to Loci and Bundle

* improve selection docs

* adjust covalent bond data model

* tweak fromExpression

* fix type

* remove hydrogen reference

* Fix MVS.rowsToExpression
2025-03-17 09:25:18 +01:00
Alexander Rose
c42a68b560 fix package-lock 2025-03-15 16:44:16 -07:00
Alexander Rose
c86d913b83 Merge branch 'master' of https://github.com/molstar/molstar into pr/agdturner/1454 2025-03-15 16:42:38 -07:00
Alexander Rose
0f6fa5fe15 simplify, add pwa files during deploy 2025-03-15 16:38:04 -07:00
Alexander Rose
f664cb02b1 Fix pickPadding and pickScale not updating PickHelper 2025-03-15 14:34:47 -07:00
David Sehnal
00a53de6e2 Use StructureElement.Bundle for measurements (#1453)
* Revert "Support measurements for coarse models"

This reverts commit a8a84e1dbf.

* add MultiStructureSelectionFromBundle and use it for measurements
2025-03-12 07:15:03 +01:00
Paul Pillot
b1ce5c158e fix: bond iterator starts from the same interbonds for every element (#1464)
This is causing an issue when there are multiple units having different inter-unit bonds: the `interBondCount` value is based on the `interBondIndices` length which is correct. But the iteration always starts from the `edges` index === 0. This is only correct for the first pairs of units. The iteration must be done over the `interBondIndices` to get the proper edges.

By returning indices for edges that do not correspond to the given element, the `otherIndex` property can be of bounds.
2025-03-12 06:41:59 +01:00
dsehnal
26826b61f9 fix lint error 2025-03-10 11:06:06 +01:00
Lukas Polak
f44f954a76 Implement ColorScale.createDiscrete, fix UI color palette bugs (#1459) 2025-03-09 10:53:10 +01:00
Alexander Rose
78aae8a2b4 pwa tweaks
- keep build process simple, write SW in JS
- support molstar version SW
- add `Viewer.loadFiles` to open supported files
- cleanup pwa icons
- add more file_handlers to pwa manifest
2025-03-08 15:00:23 -08:00
David Sehnal
92267d3264 MVS example tweaks (#1457)
* kinase story

* ihm-restraints

* fix typo

* tweak wwpdb links
2025-03-05 15:46:33 +01:00
Andy Turner
73ed45564e For viewer to be installable as a Progressive Web App with a Service Worker transpiled from TypeScript 2025-03-04 09:05:51 +00:00
David Sehnal
8bc2ebbeff Experimental esbuild support (#1452)
* wip esbuild

* esbuild all apps and examples

* wip dev build

* dev builds

* tweaks
2025-03-04 07:22:44 +01:00
Alexander Rose
5306d5d15a ignore renderables with empty draw count 2025-03-01 14:04:33 -08:00
Alexander Rose
3c3fb461c8 fix element-point visual not using child unit 2025-03-01 14:03:31 -08:00
dsehnal
62b3281282 fix webpack config 2025-03-01 19:20:34 +01:00
dsehnal
e85fadf15b tweak 2025-03-01 13:14:45 +01:00
dsehnal
a8a84e1dbf Support measurements for coarse models 2025-03-01 13:11:50 +01:00
David Sehnal
153599ef89 ihm-restraints example: show entity labels (#1450)
* ihm-restraints example: show entity labels

* noEntityLabels option
2025-03-01 08:37:51 +01:00
dsehnal
b67eda7cb5 4.12.1 2025-02-28 08:11:41 +01:00
dsehnal
8b431c50be npm fix 2025-02-28 08:08:58 +01:00
dsehnal
e195b048a1 4.12.0 2025-02-28 08:04:38 +01:00
dsehnal
ae5bb81b27 changelog 2025-02-28 08:01:21 +01:00
David Sehnal
8c04c57bc5 MVS Animation example (#1444)
* wip kinase story

* wip storyboard

* wip kinases

* import

* tweaks

* finish kinase story

* refactoring

* changelog

* tweaks

* tweak
2025-02-27 19:41:18 +01:00
David Sehnal
ec46a444f1 I/HM Example Improvements (#1448)
* add CoarseIndex

* IHM validation snapshots

* tweaks

* header
2025-02-25 06:44:48 +01:00
Alexander Rose
559e0326b6 fix image/slice group handling
- correctly update group count
- ignore undefined group when picking
- ensure group cover anti-aliased slice part
2025-02-22 11:06:01 -08:00
Sebastian Bittrich
82b93bc2a8 prim: fix arrow orientation and behavior along [0,-1,0] (#1447) 2025-02-22 09:11:59 +01:00
David Sehnal
62f940bc48 MVS: angle primitive (#1446) 2025-02-21 18:44:23 +01:00
David Sehnal
4e0be8e7b4 MVS: Volume Server support + Support carbohydrate representation + Camera section in Screenshot / State (#1445)
* MVS: Volume Server support + Camera section in Screenshot / State

* support carbohydrate repr
2025-02-21 07:51:10 +01:00
David Sehnal
128502edf0 MVS: Basic volumetric data support (#1443)
* mvs: basic volumetric data support

* headers
2025-02-19 09:03:04 +01:00
David Sehnal
aad4d4a86c MVS: IHM support (#1433)
* MVS: IHM support

* pr feedback

* fix cyclic import

* ihm.overlaps-seq-id-range

* Add ihm-restraints example
2025-02-18 08:18:18 +01:00
Alexander Rose
9bc7e27243 Merge pull request #1419 from molstar/slice-rotate
Add support for rotating `slice` representation around an axis & structure plane representation
2025-02-17 18:05:05 -08:00
Alexander Rose
a5111356c1 Add transform property to clip objects 2025-02-17 17:59:14 -08:00
Alexander Rose
9b11f7ffde use defaultColor for NaN values 2025-02-16 13:30:11 -08:00
Alexander Rose
93ce6d2807 Merge branch 'master' of https://github.com/molstar/molstar into slice-rotate 2025-02-16 13:27:56 -08:00
Alexander Rose
5c9d5d3a3d fix Vec3 prop check in setUpdateState 2025-02-16 13:27:09 -08:00
David Sehnal
f40307db39 Remove static uses of ColorTheme and SizeTheme fields (#1442) 2025-02-16 18:17:01 +01:00
Alexander Rose
4e6000fa6c Merge branch 'master' of https://github.com/molstar/molstar into slice-rotate 2025-02-15 16:59:16 -08:00
Alexander Rose
26e5817bf2 slice & plane-image improvements
- support trimming & rotation for boxes with non 90 deg angles
- cleanup & document params
2025-02-15 16:57:35 -08:00
Alexander Rose
8469be80d0 fix uniform slice coloring out of bounds 2025-02-15 16:40:05 -08:00
David Sehnal
029edc95c8 MVS: Additional Primitives (#1437)
* ellipsis

* box

* improve ellipsis

* improve ellipsis

* arrow primitive

* tweak arrow

* ellipsoid

* changelog

* pr feedback
2025-02-12 16:33:06 +01:00
David Sehnal
dd9aaf055f Components example (#1425)
* components example, initial code

* refactoring

* readme

* usage example

* changelog

* tweak
2025-02-11 18:08:15 +01:00
Alexander Rose
fcfb2d940c Merge branch 'master' of https://github.com/molstar/molstar into slice-rotate 2025-02-09 14:06:29 -08:00
Alexander Rose
f5b5109d0f Improve logic when to cull in renderer 2025-02-01 19:13:59 -08:00
Sebastian Bittrich
ca99c800f1 fix PDBj structure data URL (#1430) 2025-01-31 08:13:52 +01:00
Alexander Rose
dbd5570370 4.11.0 2025-01-26 11:10:06 -08:00
Alexander Rose
3f805c7a82 changelog 2025-01-26 11:07:06 -08:00
Alexander Rose
12c71dc5ba Handle Firefox's limit on vertex ids per draw 2025-01-25 16:04:34 -08:00
Alexander Rose
2fe3a926aa package updates 2025-01-25 15:59:44 -08:00
Alexander Rose
60c2096575 scss tweak 2025-01-25 15:53:52 -08:00
Alexander Rose
f4e9df5e4d schema updates 2025-01-25 15:26:06 -08:00
Alexander Rose
c304b82772 changelog 2025-01-25 15:18:39 -08:00
Alexander Rose
9edd171350 Merge pull request #1421 from molstar/volume-dot
add volume dot representation
2025-01-25 15:16:36 -08:00
Alexander Rose
f7d1bd7c04 PR tweaks 2025-01-25 14:27:59 -08:00
Alexander Rose
7422c255ab Merge branch 'master' of https://github.com/molstar/molstar into volume-dot 2025-01-25 14:26:44 -08:00
Alexander Rose
5497215784 Merge pull request #1423 from giagitom/fix-cartoon-tubular-helices
Fix tubular helices issue
2025-01-25 14:25:40 -08:00
giagitom
577bf1c77c Fix tubular helices issue 2025-01-22 16:27:40 +01:00
David Sehnal
c9bddccaf7 MVS: Initial support for customizable representation parameters (#1417)
* MVS: Initial support for customizable representation parameters

* pr feedback
2025-01-21 13:53:24 +01:00
midlik
ac292f9267 Sequence panel focus marker (#1392)
* Sequence view show focused residues

* wip

* Factor out MarkerColors

* Marker array supports "focus" marker type

* Sequence panel - separate focus marker from highlight and select

* Revert changes in mol-util/marker-actions.ts

* Simplify sequence.tsx

* Sequence panel markers follow renderer highlight colors

* Sequence panel focused range in bold

* Focus add with Ctrl, extend with Shift

* Update CHANGELOG
2025-01-21 13:15:06 +01:00
Alexander Rose
f0b8d75b10 add volume dot representation
- add volume-value size theme
2025-01-19 17:47:21 -08:00
Alexander Rose
0dacbcb3bc fix vertex based themes for spheres shader 2025-01-19 17:45:52 -08:00
Alexander Rose
0789241ea3 Add plane structure representation
- Can be colored with any structure theme
- Can be colored with the `external-volume` theme
- Can show atoms as a cutout
- Supports principal axes and bounding box as a reference frame
2025-01-18 19:36:49 -08:00
Alexander Rose
ddb0799dc4 Merge branch 'master' of https://github.com/molstar/molstar into slice-rotate 2025-01-18 17:30:36 -08:00
Alexander Rose
cbfa341fa3 Merge pull request #1411 from midlik/quick-styles
Quick styles
2025-01-18 16:57:03 -08:00
Alexander Rose
1c19bd90df Merge branch 'master' into quick-styles 2025-01-18 16:56:56 -08:00
Alexander Rose
300e5c8985 Merge pull request #1407 from sbittrich/master
Rebrand PDB-Dev as PDB-IHM
2025-01-18 16:55:24 -08:00
Alexander Rose
0861a78db6 Merge branch 'master' into master 2025-01-18 16:55:08 -08:00
Alexander Rose
a8e403ad85 Add support for rotating slice representation around an axis
- Add default color support for palette based themes
- Add support for trimming `image` geometry to a box
- Improve/fix iso-level support of `slice` representation
2025-01-18 16:45:22 -08:00
Ventura Rivera
4e350496b2 Sequence Viewer Mode Customization (#1412)
* adding flags for viewport selection controls

* adding logic to remove/keep selection controls

* adding viewport selection control feature

* package updates

* moving selectionTool options out of viewport

* updating selection controls with new properties

* updating property name to match nomenclature

* adding option for custom selection controls

* adding custom selection controls

* minor property name update

* adding property for granularity options

* reassigning StructureSelectionParams.granularity.options if custom granularity options

* adding entry for custom granularity options

* reusing Loci.Granularity to specify valid strings for granularityOptions

* Update src/mol-plugin-ui/spec.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* merging molstar/molstar:main into ventura-rivera/molstar:main

* moving granularityOption logic into componentDidMount

* moving granularityOption logic to componentDidMount and creating structureSelectionParams state

* adding modeOptions and defaultMode properties to sequenceViewer

* adding logic to customize mode controls

* CHANGELOG

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2025-01-18 10:59:07 +01:00
Alexander Rose
dd21ddcc80 math helpers 2025-01-17 22:47:42 -08:00
Alexander Rose
a88121f779 add Quat uniform mapping to vec4 2025-01-17 22:47:27 -08:00
Adam Midlik
1ab91d1979 Quick Styles: stateless UI 2025-01-17 11:17:14 +00:00
Adam Midlik
267788388d Quick Styles: UI subsection headers 2025-01-17 10:11:52 +00:00
Adam Midlik
43c0333be3 Quick Styles: postpone UI update while animation running 2025-01-14 10:33:25 +00:00
JonStargaryen
3b90a269b0 rebranding from PDB-Dev to PDB-IHM 2025-01-13 11:21:00 -08:00
Ventura Rivera
4aa5e1d7fc Structure Selection Granularity Options Customization (#1410)
* adding flags for viewport selection controls

* adding logic to remove/keep selection controls

* adding viewport selection control feature

* package updates

* moving selectionTool options out of viewport

* updating selection controls with new properties

* updating property name to match nomenclature

* adding option for custom selection controls

* adding custom selection controls

* minor property name update

* adding property for granularity options

* reassigning StructureSelectionParams.granularity.options if custom granularity options

* adding entry for custom granularity options

* reusing Loci.Granularity to specify valid strings for granularityOptions

* Update src/mol-plugin-ui/spec.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* merging molstar/molstar:main into ventura-rivera/molstar:main

* moving granularityOption logic into componentDidMount

* moving granularityOption logic to componentDidMount and creating structureSelectionParams state

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2025-01-13 16:39:25 +01:00
Adam Midlik
679db48938 Merge branch 'master' into quick-styles 2025-01-13 15:09:42 +00:00
Adam Midlik
2f96b42df7 Updated CHANGELOG 2025-01-13 15:08:12 +00:00
Adam Midlik
dd6f3bd76e Increase scrollbar visibility 2025-01-13 14:41:38 +00:00
Adam Midlik
f1b7e478c7 Remove unused stuff 2025-01-13 13:32:02 +00:00
Adam Midlik
416442aa27 entity-id color theme overrideWater param 2025-01-13 11:59:36 +00:00
Alexander Rose
a5f65b6e6f use value for uniform coloring of slices
- recovers previous behavior
2025-01-12 18:10:14 -08:00
Alexander Rose
938ac0cc8f fix image rendering issues with marking
- handle pixels without a group
- take fog into account
2025-01-11 14:47:06 -08:00
Alexander Rose
7dacf60478 plane mesh helpers 2025-01-11 14:40:16 -08:00
Alexander Rose
9cdb8a3a92 Merge pull request #1399 from molstar/improve-volume-coloring
improve volume coloring
2025-01-11 14:35:17 -08:00
Alexander Rose
242982e661 Merge branch 'master' into improve-volume-coloring 2025-01-11 14:34:58 -08:00
Alexander Rose
6da20a6989 Fix Plane3D.projectPoint 2025-01-11 09:21:33 -08:00
Alexander Rose
f27b651230 tweak spec.components.selectionTools.hide names 2025-01-11 09:17:30 -08:00
Alexander Rose
7c818c0cc9 add location related comments 2025-01-11 08:58:59 -08:00
Adam Midlik
e7d7ba26b0 Quick Styles: Add cartoon 2025-01-10 16:16:36 +00:00
Adam Midlik
7e64121059 Quick styles: two-row UI 2025-01-10 15:59:20 +00:00
Adam Midlik
894bba1d3a Quick styles: Stylized independent from preset 2025-01-10 11:14:23 +00:00
Adam Midlik
d9db775fe8 Toggle button different hover color when on and off 2025-01-10 11:07:13 +00:00
Ventura Rivera
a7fbc7b4c4 Remove viewport selection controls (#1408)
* adding flags for viewport selection controls

* adding logic to remove/keep selection controls

* adding viewport selection control feature

* package updates

* moving selectionTool options out of viewport

* updating selection controls with new properties

* updating property name to match nomenclature

* adding option for custom selection controls

* adding custom selection controls

* minor property name update
2025-01-10 11:06:14 +01:00
Alexander Rose
c0596298d6 Add support for position-location to volume-value color theme 2025-01-04 09:26:30 -08:00
Alexander Rose
8f32dde599 review tweaks 2025-01-04 09:25:01 -08:00
Alexander Rose
4d8f00900d improve volume coloring
- Add `volume-data` theme that colors positions by volume data
- Add support for color themes to `slice` representation
- Improve/fix palette support in volume color themes
2025-01-03 17:07:13 -08:00
237 changed files with 42493 additions and 23080 deletions

View File

@@ -5,17 +5,127 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [v4.14.1] - 2025-05-09
- Do not raise error when creating duplicate state transformers and print console warning instead
## [v4.14.0] - 2025-05-07
- Fix `Viewer.loadTrajectory` when loading a topology file
- Fix `StructConn.residueCantorPairs` to not include identity pairs
- Add format selection option to image export UI (PNG, WebP, JPEG)
- Add `StateBuilder.To.updateState`
- MVS:
- Support updating transform states
- Add support for `is_hidden` custom state as an extension
- Add `queryMVSRef` and `createMVSRefMap` utility functions
- Fix switching representation type in Volume UI
- VolumeServer: Avoid grid expansion when requiring unit cell (avoids including an extra layer of cells outside the unit cell query box)
## [v4.13.0] - 2025-04-14
- Support `--host` option for build-dev.mjs script
- Add `Viewer.loadFiles` to open supported files
- Support installing the viewer as a Progressive Web App (PWA)
- `ihm-restraints` example: show entity labels
- Fix `element-point` visual not using child unit
- Ignore `renderables` with empty draw count
- Add experimental support for `esbuild` for development
- Use `npm run dev` for faster development builds
- Use `StructureElement.Bundle` instead of expressions to serialize measurement elements
- Fixes measurements not being supported for coarse models
- Implementation of `ColorScale.createDiscrete` (#1458)
- Add `ColorScale.createDiscrete` to the `uncertainty` color theme
- Fix color palette shown in the UI (for non-gradient palettes)
- Fix colors description in the UI (when using custom thresholds)
- Fix an edge case in the UI when the user deletes all colors from the color list
- Add `interactions` extension and a corresponding example that utilizes it
- Add element source index to default atomic granularity hover labels
- Add `StructureElement.Schema` based on corresponding MolViewSpec implementation that allows data-driven selection of structural elements
- Add `StructureElement.Loci/Bundle.fromExpression/Query/Schema` helper functions
- Add `addLinkCylinderMesh` (from `createLinkCylinderMesh`)
- Add `Unit.transientCache` and `Unit.getCopy`
- Fix `ElementBondIterator` indices mapping logic for inter-unit bonds
- Fix `pickPadding` and `pickScale` not updating `PickHelper`
- MolViewSpec extension: support loading extensions when loading multistate files
- Do not add bonds for pairs of residues that have a `struct_conn` entry
- Improved `ma_qa_metric` support
- Parse all local metrics
- Ability to select alternate metrics in the pLDDT/qmean themes
- Do not assume PAE plot is symmetric
- Added `PluginConfig.Viewport.ShowScreenshotControls` to control visibility of screenshot controls
- Fix MolViewSpec builder for volumes.
- Generalize `mvs-kinase-story` example to `mvs-stories`
- Add TATA-binding protein story
- Improve the Kinase story
- Fix alpha orbitals example
## [v4.12.0] - 2025-02-28
- Fix PDBj structure data URL
- Improve logic when to cull in renderer
- Add `atom.ihm.has-seq-id` and `atom.ihm.overlaps-seq-id-range` symbol to the query language
- MolViewSpec extension:
- Add box, arrow, ellipse, ellipsoid, angle primitives
- Add basic support for volumetric data (map, Volume Server)
- Add support for `molstar_color_theme_name` custom extension
- Better IH/M support:
- Support `coarse` components
- Support `spacefill` representation
- Support `carbohydrate` representation
- Support for `custom.molstar_use_default_coloring` property on Color node.
- Use `atom.ihm.has-seq-id` and `atom.ihm.overlaps-seq-id-range` for matching `label_seq_id` locations to support querying coarse elements.
- Add ihm-restraints example
- Add `mvs-kinase-story` example
- Remove static uses of `ColorTheme` and `SizeTheme` fields. Should resolvent "undefined" errors in certain builds
- Add `transform` property to clip objects
- Add support for trimming `image` geometry to a box
- Improve/fix iso-level support of `slice` representation
- Add support for rotating `slice` representation around an axis
- Add default color support for palette based themes
- Add `plane` structure representation
- Can be colored with any structure theme
- Can be colored with the `external-volume` theme
- Can show atoms as a cutout
- Supports principal axes and bounding box as a reference frame
- Add `Camera` section to "Screenshot / State" controls
- Add `CoarseIndex` for fast lookup of coarse elements
## [v4.11.0] - 2025-01-26
- Fix for tubular helices issue (Fixes #1422)
- Volume UI improvements
- Render all volume entries instead of selecting them one-by-one
- Toggle visibility of all volumes
- More accessible iso value control
- Render all volume entries instead of selecting them one-by-one
- Toggle visibility of all volumes
- More accessible iso value control
- Support wheel event on sliders
- MolViewSpec extension:
- Add validation for discriminated union params
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
- Add validation for discriminated union params
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
- UI configuration options
- Support removal of independent selection controls in the viewport
- Support custom selection controls
- Support for custom granularity dropdown options
- Support for custom Sequence Viewer mode options
- Add `external-structure` theme that colors any geometry by structure properties
- Support float and half-float data type for direct-volume rendering and GPU isosurface extraction
- Minor documentation updates
- Add support for position-location to `volume-value` color theme
- Add support for color themes to `slice` representation
- Improve/fix palette support in volume color themes
- Fix `Plane3D.projectPoint`
- Fix marking related `image` rendering issues
- Handle pixels without a group
- Take fog into account
- MolViewSpec extension: Initial support for customizable representation parameters
- Quick Styles section reorganized
- UI color improvements (scrollbar contrast, toggle button hover color)
- Add `overrideWater` param for entity-id color theme
- Renames PDB-Dev to PDB-IHM and adjusts data source
- Fix vertex based themes for spheres shader
- Add volume dot representation
- Add volume-value size theme
- Sequence panel: Mark focused loci (bold+underline)
- Change modifier key behavior in Normal Mode (default = select only, Ctrl/Cmd = add to selection, Shift = extend last selected range)
- Handle Firefox's limit on vertex ids per draw (#1116)
- Fix behavior of `Vec3.makeRotation(out, a, b)` when `a ≈ -b`
## [v4.10.0] - 2024-12-15
@@ -83,7 +193,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Fix `findPredecessorIndex` bug when repeating values
- MolViewSpec: Support for transparency and custom properties
- MolViewSpec: MVP Support for geometrical primitives (mesh, lines, line, label, distance measurement)
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-Dev loader
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-IHM/PDB-Dev loader
- Fix Sequence View in Safari 18
- Improve performance of `IndexPairBonds` assignment when operator keys are available
- ModelArchive QualityAssessment extension:
@@ -136,7 +246,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Improve entity-id coloring for structures with multiple models from the same source (#1221)
- Wrap screenshot & image generation in a `Task`
- AlphaFold DB: Add BinaryCIF support when fetching data
- PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
- PDB-IHM/PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
- Fix polymer-gap visual coloring with cartoon theme
- Add formal-charge color theme (#328)
- Add more coloring options to cartoon theme

View File

@@ -84,7 +84,16 @@ Wipes the `build` and `lib` directories and `.tsbuildinfo` files.
Runs the cleanup script prior to building the project, forcing a full rebuild of the project.
Use these commands to resolve occassional build failures which may arise after some dependency updates. Once done, `npm run build` should work again. Note that full rebuilds take more time to complete.
Use these commands to resolve occasional build failures which may arise after some dependency updates. Once done, `npm run build` should work again. Note that full rebuilds take more time to complete.
### Develop with `esbuild`
Experimental support for faster builds with `esbuild`
- `npm run dev:all` - watch mode for all apps and examples
- `npm run dev:viewer` - watch mode for viewer
- `npm run dev:apps` - watch mode for all apps
- `npm run dev:examples` - watch mode for all examples
- `npm run dev -- -a <app name 1> <app name 2> -e <example name 1> ...` - watch mode for specified apps/examples. `-a`/`-e` with without any names will build everything.
### Build for production:
NODE_ENV=production npm run build
@@ -103,7 +112,6 @@ From the root of the project:
and navigate to `build/viewer`
### Code generation
**CIF schemas**

216
build-dev.mjs Normal file
View File

@@ -0,0 +1,216 @@
/**
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Eric E <etongfu@@outlook.com>
*/
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as path from 'path';
import * as argparse from 'argparse';
import { sassPlugin } from 'esbuild-sass-plugin';
import * as os from 'os';
const AllApps = [
'viewer',
'docking-viewer',
'mesoscale-explorer'
];
const AllExamples = [
'proteopedia-wrapper',
'basic-wrapper',
'lighting',
'alpha-orbitals',
'alphafolddb-pae',
'mvs-stories',
'ihm-restraints',
'interactions',
];
function mkDir(dir) {
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
} catch (error) {
console.error(`Failed to create directory ${dir}:`, error);
process.exit(1);
}
}
function handleFileError(error, operation, path) {
console.error(`Failed to ${operation} ${path}:`, error);
process.exit(1);
}
function fileLoaderPlugin(options) {
mkDir(options.out);
return {
name: 'file-loader',
setup(build) {
build.onLoad({ filter: /\.jpg$/ }, async (args) => {
try {
const name = path.basename(args.path);
mkDir(path.resolve(options.out, 'images'));
await fs.promises.copyFile(args.path, path.resolve(options.out, 'images', name));
return {
contents: `images/${name}`,
loader: 'text',
};
} catch (error) {
handleFileError(error, 'copy', args.path);
}
});
build.onLoad({ filter: /\.(html|ico)$/ }, async (args) => {
const name = path.basename(args.path);
await fs.promises.copyFile(args.path, path.resolve(options.out, name));
return {
contents: '',
loader: 'empty',
};
});
},
};
}
function examplesCssRenamePlugin({ root }) {
return {
name: 'example-css-rename',
setup(build) {
build.onEnd(async () => {
if (fs.existsSync(path.resolve(root, 'index.css'))) {
await fs.promises.rename(
path.resolve(root, 'index.css'),
path.resolve(root, 'molstar.css')
);
}
});
}
};
}
async function watch(name, kind) {
const prefix = kind === 'app'
? `./build/${name}`
: `./build/examples/${name}`;
let entry = `./src/${kind}s/${name}/index.ts`;
if (!fs.existsSync(entry)) {
entry = `./src/${kind}s/${name}/index.tsx`;
}
const ctx = await esbuild.context({
entryPoints: [entry],
tsconfig: './tsconfig.json',
bundle: true,
globalName: 'molstar',
outfile: kind === 'app'
? `./build/${name}/molstar.js`
: `./build/examples/${name}/index.js`,
plugins: [
fileLoaderPlugin({ out: prefix }),
sassPlugin({
type: 'css',
silenceDeprecations: ['import'],
logger: {
warn: (msg) => console.warn(msg),
debug: () => { },
}
}),
...(kind === 'example' ? [examplesCssRenamePlugin({ root: prefix })] : []),
],
external: ['crypto', 'fs', 'path', 'stream'],
loader: {
},
color: true,
logLevel: 'info',
});
await ctx.rebuild();
await ctx.watch();
}
const argParser = new argparse.ArgumentParser({
add_help: true,
description: 'Mol* development build'
});
argParser.add_argument('--apps', '-a', {
help: 'Apps to build.',
required: false,
nargs: '*',
});
argParser.add_argument('--examples', '-e', {
help: 'Examples to build.',
required: false,
nargs: '*',
});
argParser.add_argument('--port', '-p', {
help: 'Port.',
required: false,
default: 1338,
type: 'int',
});
argParser.add_argument('--host', {
help: 'Show all available host addresses.',
required: false,
action: 'store_true',
});
const args = argParser.parse_args();
const apps = (!args.apps ? [] : (args.apps.length ? args.apps : AllApps)).filter(a => AllApps.includes(a));
const examples = (!args.examples ? [] : (args.examples.length ? args.examples : AllExamples)).filter(e => AllExamples.includes(e));
console.log('Apps:', apps);
console.log('Examples:', examples);
console.log('');
function getLocalIPs() {
const interfaces = os.networkInterfaces();
const ips = [];
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
// Skip internal and non-IPv4 addresses
if (iface.internal || iface.family !== 'IPv4') continue;
ips.push(iface.address);
}
}
return ips;
}
async function main() {
const promises = [];
for (const app of apps) promises.push(watch(app, 'app'));
for (const example of examples) promises.push(watch(example, 'example'));
console.log('Initial build...');
await Promise.all(promises);
console.log('Done.');
const ctx = await esbuild.context({});
ctx.serve({
servedir: './',
port: args.port,
host: '0.0.0.0', // Always listen on all interfaces
});
console.log('');
console.log(`Server URL: http://localhost:${args.port}`);
if (args.host) {
console.log('Available host addresses:');
const ips = getLocalIPs();
ips.forEach(ip => console.log(` http://${ip}:${args.port}`));
}
console.log('');
console.log('Watching for changes...');
console.log('');
console.log('Press Ctrl+C to stop.');
}
main().catch(console.error);

15
data/pwa/README.md Normal file
View File

@@ -0,0 +1,15 @@
The files in this directory are used for deploying the Mol* Viewer as a PWA (Progressive Web App) at https://molstar.org/viewer/. They may serve as an example for creating your own PWA but wont work as-is. See `/script/deploy.js` for where these files are copied and how they are transformed during deployment.
## PWA features
- The Service Worker will cache static resources so the Viewer can be used without internet access. This works without installing, i.e., also in Firefox.
- Once installed, file types listed in the Manifest can be opened from, e.g., the Windows File Explorer.
## Notes for development
In Chrome you can see a list of installed PWAs at chrome://apps/. A right-click opens a menu with an option uninstall.
The Chrome Dev Tools have a section 'Application' to inspect and manage PWA aspects like the Manifest and Service Workers.

BIN
data/pwa/logo-144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,39 @@
{
"id": "https://molstar.org/viewer/",
"name": "Mol* Viewer",
"short_name": "Mol*",
"description": "Mol* Viewer: a modern web app for 3D visualization and analysis of large biomolecular structures.",
"start_url": "./index.html",
"theme_color": "#eeece7",
"background_color": "#eeece7",
"display": "standalone",
"icons": [
{
"src": "favicon.ico",
"sizes": "48x48"
},
{
"src": "logo-144.png",
"sizes": "144x144"
}
],
"file_handlers": [
{
"action": "./index.html",
"accept": {
"application/vnd.molstar": [".molx", ".molj"],
"text/plain": [
".mol", ".mol2", ".sdf", ".sd", ".pdb", ".ent", ".pdbqt", ".cif", ".mcif", ".mmcif", ".xyz", ".gro", ".lammpstrj",
".cub", ".cube", ".dx"
],
"application/octet-stream": [
".bcif",
".dxbin", ".ccp4", ".mrc", ".map", ".dsn6", ".brix"
]
}
}
],
"launch_handler": {
"client_mode": ["auto"]
}
}

44
data/pwa/pwa.js Normal file
View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Andy Turner <agdturner@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
window.addEventListener('molstarViewerCreated', e => {
const viewer = e.detail.viewer;
// Handle incoming files
if ('launchQueue' in window) {
launchQueue.setConsumer((launchParams) => {
if (!launchParams.files.length) return;
const files = [];
for (const fileHandle of launchParams.files) {
files.push(fileHandle.getFile());
}
Promise.all(files).then((files) => {
viewer.loadFiles(files);
});
});
}
});
// Register Progressive Web App service worker.
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('./sw.js')
.then(function (registration) {
// Registration was successful
if (molstar.isDebugMode) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}
}, function (err) {
// registration failed :(
if (molstar.isDebugMode) {
console.error('ServiceWorker registration failed: ', err);
}
});
});
}

60
data/pwa/sw.js Normal file
View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Andy Turner <agdturner@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
/** version from package.json, to be filled in during deployment */
const VERSION = '__MOLSTAR_VERSION__';
const CACHE_NAME = `molstar-viewer-${VERSION}`;
// The static resources that the app needs to function.
const APP_STATIC_RESOURCES = [
'favicon.ico',
'index.html',
'molstar.css',
'molstar.js',
'manifest.webmanifest',
'logo-144.png',
'pwa.js'
];
async function cacheStaticResources() {
const cache = await caches.open(CACHE_NAME);
await cache.addAll(APP_STATIC_RESOURCES);
await self.skipWaiting(); // Ensures the new service worker takes control immediately.
}
async function deleteOldCaches() {
const keys = await caches.keys();
await Promise.all(
keys.map((key) => {
if (key !== CACHE_NAME) {
return caches.delete(key);
}
}),
);
await self.clients.claim(); // Ensures the new service worker takes control immediately.
}
async function respondWithCacheFirst(request) {
// Try to match the request with the cache
const cachedResponse = await caches.match(request);
return cachedResponse || fetch(request);
}
self.addEventListener('install', (event) => {
// console.log(`Service Worker version ${VERSION} installed.`);
event.waitUntil(cacheStaticResources());
});
self.addEventListener('activate', (event) => {
// console.log(`Service Worker version ${VERSION} activated.`);
event.waitUntil(deleteOldCaches());
});
self.addEventListener('fetch', (event) => {
event.respondWith(respondWithCacheFirst(event.request));
});

View File

@@ -0,0 +1,4 @@
# Interactions extension
The Interactions extension enables computing or providing custom interactions between multiple selections/structures.
For usage, see the [example source code](https://github.com/molstar/molstar/tree/master/src/examples/interactions).

View File

@@ -25,7 +25,7 @@ import { PluginContext } from 'molstar/lib/mol-plugin/context';
git clone https://github.com/molstar/molstar.git
cd molstar
npm install
npm build
npm run build
```
--------------------

View File

@@ -70,7 +70,6 @@ This relies on the concept of `Expression` which is basically a intermediate rep
### 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';
import { Expression } from 'molstar/lib/mol-script/language/expression';
import { StructureSelectionQuery } from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query'
@@ -82,8 +81,8 @@ export function select_multiple() {
const groups: Expression[] = [];
for (var chain of args) {
groups.push(MS.struct.generator.atomGroups({
"chain-test": MS.core.rel.eq([MolScriptBuilder.struct.atomProperty.macromolecular.auth_asym_id(), chain[0]]),
"residue-test": MS.core.rel.inRange([MolScriptBuilder.struct.atomProperty.macromolecular.label_seq_id(), chain[1], chain[2]])
'chain-test': MS.core.rel.eq([MolScriptBuilder.struct.atomProperty.macromolecular.auth_asym_id(), chain[0]]),
'residue-test': MS.core.rel.inRange([MolScriptBuilder.struct.atomProperty.macromolecular.label_seq_id(), chain[1], chain[2]])
}));
}
var sq = StructureSelectionQuery('residue_range_10_15_in_A_and_F', MS.struct.combinator.merge(groups))
@@ -98,13 +97,52 @@ Inspect these examples to get a better feeling for this syntax: `https://github.
Furthermore, a query made this way can be converted to a `Loci` object which is important in many parts of the libary:
```typescript
// Select residue 124 of chain A and convert to Loci
const Q = MolScriptBuilder;
var sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
'chain-test' : Q.core.rel.eq([Q.struct.atomProperty.macromolecular.auth_asym_id(), A]),
"residue-test": Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), 124]),
}), objdata)
'chain-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.auth_asym_id(), A]),
'residue-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), 124]),
}), objdata)
let loci = StructureSelection.toLociWithSourceUnits(sel);
```
## Query Functions
Instead of building expressions, query functions can be created directly, e.g.:
```ts
import { atoms } from 'mol-model/structure/query/queries/generators';
const query = atoms({
residueTest: ctx => {
const seqId = StructureProperties.residue.label_seq_id(ctx.element);
return seqId > 10 && seqId < 25;
},
});
const selection = query(new QueryContext(structure));
// ...
```
## 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.:
```ts
const ala121: StructureElement.Schema = { label_asym_id: 'A', label_seq_id: 121 };
const residues: StructureElement.Schema = {
items: {
auth_asym_id: ['A', 'B'],
auth_seq_id: [10, 11],
}
};
const loci = StructureElement.Loci.fromSchema(structure, residues);
```
Usually, a code editor such as VS Code will auto-suggest all the available field names.
## Helper Functions
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.

View File

@@ -56,6 +56,7 @@ nav:
- MolViewSpec: 'extensions/mvs/index.md'
- wwPDB StructConn: 'extensions/struct-conn.md'
- Tunnels: 'extensions/tunnels.md'
- Interactions: 'extensions/interactions.md'
- Misc:
- Interesting PDB entries: misc/interesting-pdb-entries.md
- Exporting component data: exporting-components.md

View File

@@ -0,0 +1,456 @@
forcefield_3904
RDKit 3D
49 52 0 0 0 0 0 0 0 0999 V2000
7.1950 23.7840 6.1780 C 0 0 0 0 0 0 0 0 0 0 0 0
5.7810 23.6440 6.1320 O 0 0 0 0 0 0 0 0 0 0 0 0
5.0380 24.7500 6.4550 C 0 0 0 0 0 0 0 0 0 0 0 0
4.8270 25.8570 5.6380 C 0 0 0 0 0 0 0 0 0 0 0 0
4.0460 26.9150 6.0970 C 0 0 0 0 0 0 0 0 0 0 0 0
3.4760 26.8670 7.3710 C 0 0 0 0 0 0 0 0 0 0 0 0
3.6720 25.7630 8.2160 C 0 0 0 0 0 0 0 0 0 0 0 0
3.0720 25.7780 9.4800 N 0 0 0 0 0 0 0 0 0 0 0 0
1.8090 25.4470 9.9650 C 0 0 0 0 0 0 0 0 0 0 0 0
0.8080 26.1730 9.4160 N 0 0 0 0 0 0 0 0 0 0 0 0
-0.4770 25.9480 9.8040 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.5110 26.6810 9.2540 O 0 0 0 0 0 0 0 0 0 0 0 0
-1.0950 27.9270 8.6960 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.1890 28.9860 8.9110 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.6300 30.4110 8.7600 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.7130 31.4650 8.9780 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.4040 31.2910 10.3270 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.9520 29.8780 10.4990 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.8690 28.8240 10.2810 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.7400 24.9710 10.7610 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.8850 24.4940 11.3660 N 0 0 0 0 0 0 0 0 0 0 0 0
-1.4690 23.5570 12.1930 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.1090 23.4080 12.1480 N 0 0 0 0 0 0 0 0 0 0 0 0
0.3700 24.3040 11.2380 C 0 0 0 0 0 0 0 0 0 0 0 0
1.6480 24.4860 10.8910 N 0 0 0 0 0 0 0 0 0 0 0 0
4.4600 24.7130 7.7330 C 0 0 0 0 0 0 0 0 0 0 0 0
7.5263 23.8168 7.2264 H 0 0 0 0 0 0 0 0 0 0 0 0
7.6649 22.9280 5.6716 H 0 0 0 0 0 0 0 0 0 0 0 0
7.4879 24.7155 5.6716 H 0 0 0 0 0 0 0 0 0 0 0 0
5.2751 25.8961 4.6342 H 0 0 0 0 0 0 0 0 0 0 0 0
3.8776 27.7913 5.4538 H 0 0 0 0 0 0 0 0 0 0 0 0
2.8616 27.7104 7.7192 H 0 0 0 0 0 0 0 0 0 0 0 0
3.6420 26.0930 10.2520 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.1673 28.2555 9.1874 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.9189 27.8009 7.6175 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.9466 28.8274 8.1294 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.8310 30.5615 9.5009 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.2424 30.5222 7.7366 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.2524 32.4632 8.9392 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.4689 31.3496 8.1873 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.6781 31.4924 11.1285 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.2475 31.9954 10.3749 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.3529 29.7723 11.5179 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.7404 29.7213 9.7481 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.1096 28.9239 11.0705 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.3427 27.8318 10.3148 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.1334 22.9663 12.8408 H 0 0 0 0 0 0 0 0 0 0 0 0
0.4510 22.7600 12.6820 H 0 0 0 0 0 0 0 0 0 0 0 0
4.6312 23.8345 8.3724 H 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0
2 3 1 0
3 4 2 0
4 5 1 0
5 6 2 0
6 7 1 0
7 8 1 0
8 9 1 0
9 10 2 0
10 11 1 0
11 12 1 0
12 13 1 0
13 14 1 0
14 15 1 0
15 16 1 0
16 17 1 0
17 18 1 0
18 19 1 0
11 20 2 0
20 21 1 0
21 22 2 0
22 23 1 0
23 24 1 0
24 25 2 0
7 26 2 0
26 3 1 0
25 9 1 0
19 14 1 0
24 20 1 0
1 27 1 0
1 28 1 0
1 29 1 0
4 30 1 0
5 31 1 0
6 32 1 0
8 33 1 0
13 34 1 0
13 35 1 0
14 36 1 0
15 37 1 0
15 38 1 0
16 39 1 0
16 40 1 0
17 41 1 0
17 42 1 0
18 43 1 0
18 44 1 0
19 45 1 0
19 46 1 0
22 47 1 0
23 48 1 0
26 49 1 0
M END
> <ligandCode> (1)
forcefield_3904
> <ligandName> (1)
klr_22
$$$$
forcefield_3905
RDKit 3D
49 52 0 0 0 0 0 0 0 0999 V2000
6.5460 25.1350 3.8360 N 0 0 0 0 0 0 0 0 0 0 0 0
5.2550 25.5560 3.9960 C 0 0 0 0 0 0 0 0 0 0 0 0
4.5630 25.8360 3.0240 O 0 0 0 0 0 0 0 0 0 0 0 0
4.7190 25.6170 5.3820 C 0 0 0 0 0 0 0 0 0 0 0 0
4.0120 26.7570 5.7730 C 0 0 0 0 0 0 0 0 0 0 0 0
3.4880 26.8570 7.0690 C 0 0 0 0 0 0 0 0 0 0 0 0
3.6630 25.8340 8.0050 C 0 0 0 0 0 0 0 0 0 0 0 0
3.1640 25.8720 9.3110 N 0 0 0 0 0 0 0 0 0 0 0 0
1.9510 25.5250 9.9030 C 0 0 0 0 0 0 0 0 0 0 0 0
0.8900 26.1930 9.3960 N 0 0 0 0 0 0 0 0 0 0 0 0
-0.3560 25.9480 9.8850 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.4490 26.6220 9.3800 O 0 0 0 0 0 0 0 0 0 0 0 0
-1.1050 27.7730 8.6070 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.7010 29.0180 9.2810 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.5340 30.2520 8.3770 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.5490 31.3650 8.6570 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.0430 31.3400 10.0950 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.7320 30.0170 10.4330 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.1730 28.8170 9.6630 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.5160 25.0110 10.9050 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.5980 24.5310 11.6140 N 0 0 0 0 0 0 0 0 0 0 0 0
-1.0930 23.6490 12.4490 C 0 0 0 0 0 0 0 0 0 0 0 0
0.2630 23.5390 12.3140 N 0 0 0 0 0 0 0 0 0 0 0 0
0.6490 24.4020 11.3310 C 0 0 0 0 0 0 0 0 0 0 0 0
1.8910 24.6070 10.8850 N 0 0 0 0 0 0 0 0 0 0 0 0
4.3890 24.7010 7.5990 C 0 0 0 0 0 0 0 0 0 0 0 0
4.9170 24.5890 6.3060 C 0 0 0 0 0 0 0 0 0 0 0 0
7.0340 24.6220 4.5560 H 0 0 0 0 0 0 0 0 0 0 0 0
6.8360 24.9760 2.8790 H 0 0 0 0 0 0 0 0 0 0 0 0
3.8658 27.5811 5.0592 H 0 0 0 0 0 0 0 0 0 0 0 0
2.9275 27.7591 7.3554 H 0 0 0 0 0 0 0 0 0 0 0 0
3.7800 26.2290 10.0270 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.0105 27.8693 8.5543 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.5056 27.6720 7.5875 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.1446 29.1858 10.2149 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.5231 30.6584 8.5280 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.7027 29.9170 7.3429 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.0722 32.3370 8.4626 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.4156 31.2068 7.9983 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.1848 31.4776 10.7693 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.7762 32.1504 10.2201 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.6091 29.8266 11.5094 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.7879 30.1213 10.1428 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.2575 27.9194 10.2932 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.7539 28.7064 8.7355 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.6897 23.0698 13.1690 H 0 0 0 0 0 0 0 0 0 0 0 0
0.8800 22.9330 12.8360 H 0 0 0 0 0 0 0 0 0 0 0 0
4.5467 23.8809 8.3150 H 0 0 0 0 0 0 0 0 0 0 0 0
5.4866 23.6927 6.0193 H 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0
2 3 2 0
2 4 1 0
4 5 2 0
5 6 1 0
6 7 2 0
7 8 1 0
8 9 1 0
9 10 2 0
10 11 1 0
11 12 1 0
12 13 1 0
13 14 1 0
14 15 1 0
15 16 1 0
16 17 1 0
17 18 1 0
18 19 1 0
11 20 2 0
20 21 1 0
21 22 2 0
22 23 1 0
23 24 1 0
24 25 2 0
7 26 1 0
26 27 2 0
27 4 1 0
25 9 1 0
19 14 1 0
24 20 1 0
1 28 1 0
1 29 1 0
5 30 1 0
6 31 1 0
8 32 1 0
13 33 1 0
13 34 1 0
14 35 1 0
15 36 1 0
15 37 1 0
16 38 1 0
16 39 1 0
17 40 1 0
17 41 1 0
18 42 1 0
18 43 1 0
19 44 1 0
19 45 1 0
22 46 1 0
23 47 1 0
26 48 1 0
27 49 1 0
M END
> <ligandCode> (2)
forcefield_3905
> <ligandName> (2)
1oiy-1
$$$$
forcefield_14264
RDKit 3D
50 53 0 0 0 0 0 0 0 0999 V2000
4.9220 23.4040 3.0090 N 0 0 0 0 0 0 0 0 0 0 0 0
5.8970 24.7200 3.4040 S 0 0 0 0 0 6 0 0 0 0 0 0
7.2120 24.2200 3.7260 O 0 0 0 0 0 0 0 0 0 0 0 0
5.6670 25.6980 2.3670 O 0 0 0 0 0 0 0 0 0 0 0 0
5.1310 25.2890 4.8970 C 0 0 0 0 0 0 0 0 0 0 0 0
4.3720 26.4580 4.8910 C 0 0 0 0 0 0 0 0 0 0 0 0
3.7730 26.8990 6.0760 C 0 0 0 0 0 0 0 0 0 0 0 0
3.9290 26.1970 7.2780 C 0 0 0 0 0 0 0 0 0 0 0 0
3.3430 26.5790 8.4890 N 0 0 0 0 0 0 0 0 0 0 0 0
2.1400 26.2820 9.1290 C 0 0 0 0 0 0 0 0 0 0 0 0
1.0830 26.9670 8.6370 N 0 0 0 0 0 0 0 0 0 0 0 0
-0.1520 26.7740 9.1730 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.2430 27.4630 8.6800 O 0 0 0 0 0 0 0 0 0 0 0 0
-0.9280 28.7750 8.2140 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.2060 29.6320 8.2420 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.2120 29.0810 9.2730 C 0 0 0 0 0 0 0 0 0 0 0 0
-4.1810 30.1570 9.7450 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.4520 31.3060 10.4440 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.0250 31.5210 9.9360 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.8770 31.1190 8.4740 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.3060 25.8730 10.2240 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.3770 25.4460 10.9810 N 0 0 0 0 0 0 0 0 0 0 0 0
-0.8720 24.5730 11.8270 C 0 0 0 0 0 0 0 0 0 0 0 0
0.4750 24.4190 11.6520 N 0 0 0 0 0 0 0 0 0 0 0 0
0.8530 25.2430 10.6320 C 0 0 0 0 0 0 0 0 0 0 0 0
2.0880 25.3990 10.1410 N 0 0 0 0 0 0 0 0 0 0 0 0
4.7090 25.0260 7.2550 C 0 0 0 0 0 0 0 0 0 0 0 0
5.3130 24.5730 6.0780 C 0 0 0 0 0 0 0 0 0 0 0 0
4.1960 23.3080 3.7210 H 0 0 0 0 0 0 0 0 0 0 0 0
5.4460 22.5390 2.8810 H 0 0 0 0 0 0 0 0 0 0 0 0
4.2447 27.0304 3.9603 H 0 0 0 0 0 0 0 0 0 0 0 0
3.1666 27.8167 6.0634 H 0 0 0 0 0 0 0 0 0 0 0 0
3.8380 27.2640 9.0420 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.1661 29.2261 8.8668 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.5358 28.7227 7.1876 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.6885 29.5690 7.2555 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.7847 28.2635 8.8106 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.6467 28.7218 10.1456 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.7231 30.5555 8.8747 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.8753 29.7014 10.4664 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.0232 32.2317 10.2804 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.3803 31.0434 11.5098 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.7667 32.5851 10.0403 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.3511 30.8895 10.5336 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.5603 31.7316 7.8675 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.8284 31.2816 8.1841 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.4619 24.0333 12.5825 H 0 0 0 0 0 0 0 0 0 0 0 0
1.0910 23.8110 12.1720 H 0 0 0 0 0 0 0 0 0 0 0 0
4.8468 24.4538 8.1843 H 0 0 0 0 0 0 0 0 0 0 0 0
5.9261 23.6598 6.0846 H 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0
2 3 2 0
2 4 2 0
2 5 1 0
5 6 2 0
6 7 1 0
7 8 2 0
8 9 1 0
9 10 1 0
10 11 2 0
11 12 1 0
12 13 1 0
13 14 1 0
14 15 1 0
15 16 1 0
16 17 1 0
17 18 1 0
18 19 1 0
19 20 1 0
12 21 2 0
21 22 1 0
22 23 2 0
23 24 1 0
24 25 1 0
25 26 2 0
8 27 1 0
27 28 2 0
28 5 1 0
26 10 1 0
20 15 1 0
25 21 1 0
1 29 1 0
1 30 1 0
6 31 1 0
7 32 1 0
9 33 1 0
14 34 1 0
14 35 1 0
15 36 1 0
16 37 1 0
16 38 1 0
17 39 1 0
17 40 1 0
18 41 1 0
18 42 1 0
19 43 1 0
19 44 1 0
20 45 1 0
20 46 1 0
23 47 1 0
24 48 1 0
27 49 1 0
28 50 1 0
M END
> <ligandCode> (3)
forcefield_14264
> <ligandName> (3)
1h1s
$$$$
forcefield_14265
RDKit 3D
50 53 0 0 0 0 0 0 0 0999 V2000
5.9560 25.0880 4.1850 N 0 0 0 0 0 0 0 0 0 0 0 0
5.6880 24.2300 5.6100 S 0 0 0 0 0 6 0 0 0 0 0 0
4.9010 23.0800 5.2270 O 0 0 0 0 0 0 0 0 0 0 0 0
6.9470 24.1160 6.3060 O 0 0 0 0 0 0 0 0 0 0 0 0
4.6500 25.3510 6.5050 C 0 0 0 0 0 0 0 0 0 0 0 0
4.3190 26.5790 5.9340 C 0 0 0 0 0 0 0 0 0 0 0 0
3.4970 27.4490 6.6410 C 0 0 0 0 0 0 0 0 0 0 0 0
3.0220 27.0950 7.9100 C 0 0 0 0 0 0 0 0 0 0 0 0
3.3680 25.8770 8.5100 C 0 0 0 0 0 0 0 0 0 0 0 0
2.9060 25.4620 9.7630 N 0 0 0 0 0 0 0 0 0 0 0 0
1.6520 25.1280 10.2760 C 0 0 0 0 0 0 0 0 0 0 0 0
0.6590 25.9320 9.8350 N 0 0 0 0 0 0 0 0 0 0 0 0
-0.6150 25.7270 10.2650 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.6410 26.5420 9.8330 O 0 0 0 0 0 0 0 0 0 0 0 0
-1.2650 27.5510 8.8940 C 0 0 0 0 0 0 0 0 0 0 0 0
-1.4150 28.9670 9.4830 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.0650 29.8960 8.4420 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.7130 31.1080 9.0990 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.8410 30.6980 10.0490 C 0 0 0 0 0 0 0 0 0 0 0 0
-3.6490 29.3080 10.6570 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.1750 28.9600 10.8270 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.8720 24.6910 11.1610 C 0 0 0 0 0 0 0 0 0 0 0 0
-2.0060 24.2140 11.7870 N 0 0 0 0 0 0 0 0 0 0 0 0
-1.5910 23.2030 12.5210 C 0 0 0 0 0 0 0 0 0 0 0 0
-0.2430 23.0050 12.3980 N 0 0 0 0 0 0 0 0 0 0 0 0
0.2320 23.9470 11.5330 C 0 0 0 0 0 0 0 0 0 0 0 0
1.5000 24.1080 11.1380 N 0 0 0 0 0 0 0 0 0 0 0 0
4.1890 25.0030 7.7800 C 0 0 0 0 0 0 0 0 0 0 0 0
6.4350 24.4790 3.5180 H 0 0 0 0 0 0 0 0 0 0 0 0
5.1080 25.4920 3.7910 H 0 0 0 0 0 0 0 0 0 0 0 0
4.7020 26.8549 4.9404 H 0 0 0 0 0 0 0 0 0 0 0 0
3.2187 28.4186 6.2023 H 0 0 0 0 0 0 0 0 0 0 0 0
2.3612 27.7900 8.4488 H 0 0 0 0 0 0 0 0 0 0 0 0
3.5640 25.4960 10.5280 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.2150 27.3950 8.6054 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.9312 27.4715 8.0223 H 0 0 0 0 0 0 0 0 0 0 0 0
-0.4134 29.3591 9.7135 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.2920 30.2413 7.7397 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.8482 29.3296 7.9168 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.9484 31.6568 9.6685 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.1423 31.7400 8.3076 H 0 0 0 0 0 0 0 0 0 0 0 0
-3.8921 31.4320 10.8667 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.7682 30.6682 9.4579 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.1367 29.2806 11.6426 H 0 0 0 0 0 0 0 0 0 0 0 0
-4.0960 28.5721 9.9724 H 0 0 0 0 0 0 0 0 0 0 0 0
-1.7122 29.6974 11.4994 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.1140 27.9439 11.2440 H 0 0 0 0 0 0 0 0 0 0 0 0
-2.2486 22.5915 13.1563 H 0 0 0 0 0 0 0 0 0 0 0 0
0.3120 22.2980 12.8550 H 0 0 0 0 0 0 0 0 0 0 0 0
4.4733 24.0335 8.2150 H 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0
2 3 2 0
2 4 2 0
2 5 1 0
5 6 2 0
6 7 1 0
7 8 2 0
8 9 1 0
9 10 1 0
10 11 1 0
11 12 2 0
12 13 1 0
13 14 1 0
14 15 1 0
15 16 1 0
16 17 1 0
17 18 1 0
18 19 1 0
19 20 1 0
20 21 1 0
13 22 2 0
22 23 1 0
23 24 2 0
24 25 1 0
25 26 1 0
26 27 2 0
9 28 2 0
28 5 1 0
27 11 1 0
21 16 1 0
26 22 1 0
1 29 1 0
1 30 1 0
6 31 1 0
7 32 1 0
8 33 1 0
10 34 1 0
15 35 1 0
15 36 1 0
16 37 1 0
17 38 1 0
17 39 1 0
18 40 1 0
18 41 1 0
19 42 1 0
19 43 1 0
20 44 1 0
20 45 1 0
21 46 1 0
21 47 1 0
24 48 1 0
25 49 1 0
28 50 1 0
M END
> <ligandCode> (4)
forcefield_14265
> <ligandName> (4)
1oiu
$$$$

File diff suppressed because it is too large Load Diff

45576
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "4.10.0",
"version": "4.14.1",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -10,6 +10,9 @@
"bugs": {
"url": "https://github.com/molstar/molstar/issues"
},
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"lint": "eslint .",
"lint-fix": "eslint . --fix",
@@ -32,6 +35,11 @@
"watch-webpack": "webpack -w --mode development --stats minimal",
"watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
"watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
"dev": "node build-dev.mjs",
"dev:all": "node build-dev.mjs -a -e",
"dev:viewer": "node build-dev.mjs -a viewer",
"dev:apps": "node build-dev.mjs -a",
"dev:examples": "node build-dev.mjs -e",
"serve": "http-server -p 1338 -g",
"model-server": "node lib/commonjs/servers/model/server.js",
"model-server-watch": "nodemon --watch lib lib/commonjs/servers/model/server.js",
@@ -114,7 +122,11 @@
"Eric E <etongfu@outlook.com>",
"Xavier Martinez <xavier.martinez.xm@gmail.com>",
"Alex Chan <smalldirkalex@gmail.com>",
"Simeon Borko <simeon.borko@gmail.com>"
"Simeon Borko <simeon.borko@gmail.com>",
"Ventura Rivera <venturaxrivera@gmail.com>",
"Andy Turner <agdturner@gmail.com>",
"Lukáš Polák <admin@lukaspolak.cz>",
"Chetan Mishra <chetan.s115@gmail.com>"
],
"license": "MIT",
"devDependencies": {
@@ -122,19 +134,21 @@
"@types/gl": "^6.0.5",
"@types/jest": "^29.5.14",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.16",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"benchmark": "^2.1.4",
"concurrently": "^9.1.0",
"concurrently": "^9.1.2",
"cpx2": "^8.0.0",
"crypto-browserify": "^3.12.1",
"css-loader": "^7.1.2",
"esbuild": "^0.25.0",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^8.57.1",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^6.2.0",
"fs-extra": "^11.2.0",
"fs-extra": "^11.3.0",
"http-server": "^14.1.1",
"jest": "^29.7.0",
"jpeg-js": "^0.4.4",
@@ -143,22 +157,22 @@
"raw-loader": "^4.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.83.0",
"sass": "^1.83.4",
"sass-loader": "^16.0.4",
"simple-git": "^3.27.0",
"stream-browserify": "^3.0.0",
"style-loader": "^4.0.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2",
"typescript": "^5.7.3",
"webpack": "^5.97.1",
"webpack-cli": "^5.1.4"
"webpack-cli": "^6.0.1"
},
"dependencies": {
"@types/argparse": "^2.0.17",
"@types/benchmark": "^2.1.5",
"@types/compression": "1.7.5",
"@types/express": "^5.0.0",
"@types/node": "^18.19.68",
"@types/node": "^18.19.74",
"@types/node-fetch": "^2.6.12",
"@types/swagger-ui-dist": "3.30.5",
"argparse": "^2.0.1",
@@ -170,11 +184,11 @@
"immutable": "^5.0.3",
"io-ts": "^2.2.22",
"node-fetch": "^2.7.0",
"react-markdown": "^9.0.1",
"react-markdown": "^9.0.3",
"rxjs": "^7.8.1",
"swagger-ui-dist": "^5.18.2",
"tslib": "^2.8.1",
"util.promisify": "^1.1.2",
"util.promisify": "^1.1.3",
"xhr2": "^0.2.1"
},
"peerDependencies": {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -9,7 +9,10 @@ const path = require('path');
const fs = require("fs");
const fse = require("fs-extra");
const VERSION = require(path.resolve(__dirname, '../package.json')).version;
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(buildDir, 'deploy/');
const localPath = path.resolve(deployDir, 'molstar.github.io/');
@@ -17,6 +20,12 @@ const localPath = path.resolve(deployDir, 'molstar.github.io/');
const analyticsTag = /<!-- __MOLSTAR_ANALYTICS__ -->/g;
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics --><iframe src="https://web3dsurvey.com/collector-iframe.html" style="width: 1px; height: 1px;"></iframe>`;
const manifestTag = /<!-- __MOLSTAR_MANIFEST__ -->/g;
const manifestCode = `<link rel="manifest" href="./manifest.webmanifest">`;
const pwaTag = /<!-- __MOLSTAR_PWA__ -->/g;
const pwaCode = `<script src='./pwa.js'></script>`;
function log(command, stdout, stderr) {
if (command) {
console.log('\n###', command);
@@ -31,17 +40,41 @@ function addAnalytics(path) {
fs.writeFileSync(path, result, 'utf8');
}
function addManifest(path) {
const data = fs.readFileSync(path, 'utf8');
const result = data.replace(manifestTag, manifestCode);
fs.writeFileSync(path, result, 'utf8');
}
function addPwa(path) {
const data = fs.readFileSync(path, 'utf8');
const result = data.replace(pwaTag, pwaCode);
fs.writeFileSync(path, result, 'utf8');
}
function addVersion(path) {
const data = fs.readFileSync(path, 'utf8');
const result = data.replace('__MOLSTAR_VERSION__', VERSION);
fs.writeFileSync(path, result, 'utf8');
}
function copyViewer() {
console.log('\n###', 'copy viewer files');
const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
const viewerBuildPath = path.resolve(buildDir, 'viewer/');
const viewerDeployPath = path.resolve(localPath, 'viewer/');
fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
addAnalytics(path.resolve(viewerDeployPath, 'index.html'));
addManifest(path.resolve(viewerDeployPath, 'index.html'));
addPwa(path.resolve(viewerDeployPath, 'index.html'));
const pwaDataPath = path.resolve(dataDir, 'pwa/');
fse.copySync(pwaDataPath, viewerDeployPath, { overwrite: true });
addVersion(path.resolve(viewerDeployPath, 'sw.js'));
}
function copyMe() {
console.log('\n###', 'copy me files');
const meBuildPath = path.resolve(buildDir, '../build/mesoscale-explorer/');
const meBuildPath = path.resolve(buildDir, 'mesoscale-explorer/');
const meDeployPath = path.resolve(localPath, 'me/viewer/');
fse.copySync(meBuildPath, meDeployPath, { overwrite: true });
addAnalytics(path.resolve(meDeployPath, 'index.html'));
@@ -49,12 +82,12 @@ function copyMe() {
function copyDemos() {
console.log('\n###', 'copy demos files');
const lightingBuildPath = path.resolve(buildDir, '../build/examples/lighting/');
const lightingBuildPath = path.resolve(buildDir, 'examples/lighting/');
const lightingDeployPath = path.resolve(localPath, 'demos/lighting/');
fse.copySync(lightingBuildPath, lightingDeployPath, { overwrite: true });
addAnalytics(path.resolve(lightingDeployPath, 'index.html'));
const orbitalsBuildPath = path.resolve(buildDir, '../build/examples/alpha-orbitals/');
const orbitalsBuildPath = path.resolve(buildDir, 'examples/alpha-orbitals/');
const orbitalsDeployPath = path.resolve(localPath, 'demos/alpha-orbitals/');
fse.copySync(orbitalsBuildPath, orbitalsDeployPath, { overwrite: true });
addAnalytics(path.resolve(orbitalsDeployPath, 'index.html'));

View File

@@ -27,7 +27,7 @@ import { ObjectKeys } from '../../mol-util/type-helpers';
import './index.html';
import { ShowButtons, StructurePreset, ViewportComponent } from './viewport';
require('mol-plugin-ui/skin/light.scss');
import '../../mol-plugin-ui/skin/light.scss';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setDebugMode, setProductionMode } from '../../mol-util/debug';

View File

@@ -25,7 +25,7 @@ import { MesoFocusLoci } from './behavior/camera';
import { GraphicsMode, MesoscaleState } from './data/state';
import { MesoSelectLoci } from './behavior/select';
import { Transparency } from '../../mol-gl/webgl/render-item';
import { LoadModel, loadExampleEntry, loadPdb, loadPdbDev, loadUrl, openState } from './ui/states';
import { LoadModel, loadExampleEntry, loadPdb, loadPdbIhm, loadUrl, openState } from './ui/states';
import { Asset } from '../../mol-util/assets';
import { AnimateCameraSpin } from '../../mol-plugin-state/animation/built-in/camera-spin';
import { AnimateCameraRock } from '../../mol-plugin-state/animation/built-in/camera-rock';
@@ -85,7 +85,7 @@ const DefaultMesoscaleExplorerOptions = {
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
viewportShowSelectionMode: false,
viewportShowSelectionMode: true,
viewportShowAnimation: false,
viewportShowTrajectoryControls: false,
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
@@ -120,8 +120,15 @@ export class MesoscaleExplorer {
await loadPdb(this.plugin, id);
}
/**
* @deprecated Scheduled for removal in v5. Use {@link loadPdbIhm | loadPdbIhm(id: string)} instead.
*/
async loadPdbDev(id: string) {
await loadPdbDev(this.plugin, id);
await this.loadPdbIhm(id);
}
async loadPdbIhm(id: string) {
await loadPdbIhm(this.plugin, id);
}
static async create(elementOrId: string | HTMLElement, options: Partial<MesoscaleExplorerOptions> = {}) {
@@ -143,7 +150,7 @@ export class MesoscaleExplorer {
PluginSpec.Behavior(MesoFocusLoci),
PluginSpec.Behavior(MesoSelectLoci),
PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
...o.extensions.map(e => Extensions[e]),
],
animations: [

View File

@@ -73,7 +73,7 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
this.ctx.managers.interactivity.lociSelects.deselectAll();
return;
}
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : 'chain');
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : this.ctx.managers.interactivity.props.granularity);
this.ctx.managers.interactivity.lociSelects.toggle({ loci }, false);
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
@@ -116,7 +116,7 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
if (modifiers.control) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
} else {
const loci = Loci.normalize(current.loci, 'chain');
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
}
}

View File

@@ -192,12 +192,13 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
const t = isBinary ? d : utf8Read(d, 0, d.length);
const file = Asset.File(new File([t], ent.file));
const color = ColorNames.skyblue;
const color = (ent.color) ? Color.fromRgb(ent.color[0], ent.color[1], ent.color[2]) : ColorNames.skyblue;
const sizeFactor = ent.sizeFactor || 1;
const tags = ent.groups.map(({ id, root }) => `${root}:${id}`);
const instances = ent.instances && getAssetInstances(ent.instances);
const description = ent.description;
const label = ent.label || ent.file.split('.')[0];
build = build
.toRoot()
@@ -300,6 +301,7 @@ type GenericEntity = {
file: string
label?: string
description?: string
color?: number[]
groups: {
/** reference to `${GenericGroup.id}` */
id: string,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -12,7 +12,7 @@ import { Color } from '../../../mol-util/color';
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
import { Clip } from '../../../mol-util/clip';
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
import { ParamMapping } from '../../../mol-util/param-mapping';
import { EntityNode } from '../ui/entities';
import { DistinctColorsProps, distinctColors } from '../../../mol-util/color/distinct';
@@ -211,7 +211,8 @@ export function getClipObjects(values: SimpleClipProps, boundingSphere: Sphere3D
invert: values.invert,
position,
scale,
rotation: values.rotation
rotation: values.rotation,
transform: Mat4.identity(),
}];
}

View File

@@ -93,9 +93,15 @@
return;
}
var pdbihm = getParam('pdbihm', '[^&]+').trim();
if (pdbihm) {
me.loadPdbIhm(pdbihm);
return;
}
// support for deprecated pdb-dev param
var pdbdev = getParam('pdbdev', '[^&]+').trim();
if (pdbdev) {
me.loadPdbDev(pdbdev);
me.loadPdbIhm(pdbdev);
return;
}
window.addEventListener('unload', () => {

View File

@@ -6,5 +6,5 @@
import './favicon.ico';
import './index.html';
require('./style.scss');
import './style.scss';
export * from './app';

View File

@@ -25,8 +25,8 @@ $logo-background: rgba(0,0,0,0.75);
@return color.adjust($color, $lightness: $amount, $space: hsl);
}
@import 'mol-plugin-ui/skin/base/base';
@import 'mol-plugin-ui/skin/base/variables';
@import '../../mol-plugin-ui/skin/base/base';
@import '../../mol-plugin-ui/skin/base/variables';
a {
color: $font-color;

View File

@@ -29,6 +29,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
import { MesoFocusLoci } from '../behavior/camera';
import Markdown from 'react-markdown';
import { combineLatest } from 'rxjs';
import { ColorLoaderControls } from './states';
function centerLoci(plugin: PluginContext, loci: Loci, durationMs = 250) {
const { canvas3d } = plugin;
@@ -619,6 +620,7 @@ class Node<P extends {}, S extends { isDisabled: boolean }> extends PluginUIComp
}
}
export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean, action?: 'color' | 'clip' | 'root', isDisabled: boolean }> {
state = {
isCollapsed: !!this.props.cell.state.isCollapsed,
@@ -881,12 +883,13 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
const root = (isRoot && this.allGroups.length > 1) && <IconButton svg={BrushSvg} toggleState={false} disabled={disabled} small onClick={this.toggleRoot} />;
const clip = <IconButton svg={ContentCutSvg} toggleState={false} disabled={disabled} small onClick={this.toggleClip} />;
const visibility = <IconButton svg={state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} disabled={disabled} small onClick={this.toggleVisible} />;
const loadColorButton = (depth === 0) && <ColorLoaderControls plugin={this.plugin} />;
return <>
<div className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${depth * 10 + 5}px` }}>
{expand}
{label}
{root || color}
{loadColorButton}
{clip}
{visibility}
</div>

View File

@@ -24,9 +24,10 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
import { createGenericHierarchy } from '../data/generic/preset';
import { createMmcifHierarchy } from '../data/mmcif/preset';
import { createPetworldHierarchy } from '../data/petworld/preset';
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
import { getAllEntities, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
import { isTimingMode } from '../../../mol-util/debug';
import { now } from '../../../mol-util/now';
import { readFromFile } from '../../../mol-util/data-source';
function adjustPluginProps(ctx: PluginContext) {
const customState = ctx.customState as MesoscaleExplorerState;
@@ -211,27 +212,54 @@ export async function loadPdb(ctx: PluginContext, id: string) {
await createHierarchy(ctx, data.ref);
}
export async function loadPdbDev(ctx: PluginContext, id: string) {
export async function loadPdbIhm(ctx: PluginContext, id: string) {
await reset(ctx);
let url: string;
// 4 character PDB id, TODO: support extended PDB ID
if (id.match(/^[1-9][A-Z0-9]{3}$/i) !== null) {
url = `https://pdb-dev.wwpdb.org/bcif/${id.toLowerCase()}.bcif`;
url = `https://pdb-ihm.org/bcif/${id.toLowerCase()}.bcif`;
} else {
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
url = `https://pdb-ihm.org/bcif/${nId.toUpperCase()}.bcif`;
}
const data = await ctx.builders.data.download({ url, isBinary: true });
await createHierarchy(ctx, data.ref);
}
async function loadColors(ctx: PluginContext, file: File) {
const data = await ctx.runTask(readFromFile(file, 'string'));
const colorData = JSON.parse(data);
const update = ctx.state.data.build();
const allEntities = getAllEntities(ctx);
for (const entityCell of allEntities) {
const label = getEntityLabel(ctx, entityCell);
const tags = entityCell.transform.tags;
const fullname = (tags?.[0].replace('comp:', '') ?? '') + '.' + label;
// test each tag, siwtch to uniform color
if (fullname in colorData) {
const { x, y, z } = colorData[fullname];
const color = Color.fromRgb(x, y, z);
update.to(entityCell).update(old => {
if (old.type) {
old.colorTheme = { name: 'uniform', params: { value: color, lightness: old.colorTheme.params.lightness } };
old.type.params.color = color;
} else if (old.coloring) {
old.coloring.params.color = color;
}
});
}
}
await update.commit();
}
//
export const LoadDatabase = StateAction.build({
display: { name: 'Database', description: 'Load from Database' },
params: (a, ctx: PluginContext) => {
return {
source: PD.Select('pdb', PD.objectToOptions({ pdb: 'PDB', pdbDev: 'PDB-Dev' })),
source: PD.Select('pdb', PD.objectToOptions({ pdb: 'PDB', pdbIhm: 'PDB-IHM' })),
entry: PD.Text(''),
};
},
@@ -239,8 +267,8 @@ export const LoadDatabase = StateAction.build({
})(({ params }, ctx: PluginContext) => Task.create('Loading from database...', async taskCtx => {
if (params.source === 'pdb') {
await loadPdb(ctx, params.entry);
} else if (params.source === 'pdbDev') {
await loadPdbDev(ctx, params.entry);
} else if (params.source === 'pdbIhm') {
await loadPdbIhm(ctx, params.entry);
}
}));
@@ -299,7 +327,6 @@ export const LoadModel = StateAction.build({
}
}));
//
export class DatabaseControls extends PluginUIComponent {
componentDidMount() {
@@ -337,6 +364,30 @@ export class ExampleControls extends PluginUIComponent {
}
}
export function ColorLoaderControls({ plugin }: { plugin: PluginContext }) {
const triggerLoadColors = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const input = e.target as HTMLInputElement;
if (!input.files || !input.files[0]) return;
const file = input.files[0];
await loadColors(plugin, new File([file], file.name));
};
input.click();
};
return (
<IconButton
svg={OpenInBrowserSvg}
title="Load Colors"
onClick={triggerLoadColors}
small
/>
);
}
export async function openState(ctx: PluginContext, file: File) {
const customState = ctx.customState as MesoscaleExplorerState;
delete customState.stateRef;
@@ -426,7 +477,7 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
driver.setSteps([
// Left panel
{ element: '#explorerinfo', popover: { title: 'Explorer Header Info', description: 'This section displays the explorer header with version information, documentation access, and tour navigation. Use the right and left arrow keys to navigate the tour.', side: 'left', align: 'start' } },
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-DEV databases.', side: 'bottom', align: 'start' } },
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-IHM databases.', side: 'bottom', align: 'start' } },
{ element: '#loader', popover: { title: 'Import from File', description: 'Load local files (.molx, .molj, .zip, .cif, .bcif) using this option.', side: 'bottom', align: 'start' } },
{ element: '#example', popover: { title: 'Example Models and Tours', description: 'Select from a range of example models and tours provided.', side: 'left', align: 'start' } },
{ element: '#session', popover: { title: 'Session Management', description: 'Download the current session in .molx format.', side: 'top', align: 'start' } },
@@ -486,7 +537,6 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
}
}
export class MesoQuickStylesControls extends CollapsableControls {
defaultState() {
return {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 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>
@@ -24,7 +24,6 @@ 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 { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { ZenodoImport } from '../../extensions/zenodo';
@@ -58,16 +57,16 @@ 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';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { consoleStats, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
export { consoleStats, setDebugMode, setProductionMode, setTimingMode, isProductionMode, isDebugMode, isTimingMode } from '../../mol-util/debug';
const CustomFormats = [
['g3d', G3dProvider] as const
];
export const ExtensionMap = {
'volseg': PluginSpec.Behavior(Volseg),
'backgrounds': PluginSpec.Behavior(Backgrounds),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
@@ -121,7 +120,6 @@ const DefaultViewerOptions = {
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
@@ -202,7 +200,6 @@ export class Viewer {
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
@@ -288,14 +285,21 @@ export class Viewer {
}));
}
/**
* @deprecated Scheduled for removal in v5. Use {@link loadPdbIhm | loadPdbIhm(pdbIhm: string)} instead.
*/
loadPdbDev(pdbDev: string) {
return this.loadPdbIhm(pdbDev);
}
loadPdbIhm(pdbIhm: string) {
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
source: {
name: 'pdb-dev' as const,
name: 'pdb-ihm' as const,
params: {
provider: {
id: pdbDev,
id: pdbIhm,
encoding: 'bcif',
},
options: params.source.params.options,
@@ -489,7 +493,8 @@ export class Viewer {
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
const provider = plugin.dataFormats.get(params.model.format);
model = await provider!.parse(plugin, data);
const parsed = await provider!.parse(plugin, data);
model = parsed.topology;
}
const data = params.coordinates.kind === 'coordinates-data'
@@ -553,6 +558,23 @@ export class Viewer {
}
}
loadFiles(files: File[]) {
const sessions = files.filter(f => {
const fn = f.name.toLowerCase();
return fn.endsWith('.molx') || fn.endsWith('.molj');
});
if (sessions.length > 0) {
return PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: sessions[0] });
} else {
return this.plugin.runTask(this.plugin.state.data.applyAction(OpenFiles, {
files: files.map(f => Asset.File(f)),
format: { name: 'auto', params: {} },
visuals: true
}));
}
}
handleResize() {
this.plugin.layout.events.updated.next(void 0);
}

View File

@@ -35,6 +35,7 @@
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
<!-- __MOLSTAR_MANIFEST__ -->
</head>
<body>
<div id="app"></div>
@@ -111,8 +112,11 @@
var pdb = getParam('pdb', '[^&]+').trim();
if (pdb) viewer.loadPdb(pdb);
var pdbIhm = getParam('pdb-ihm', '[^&]+').trim();
if (pdbIhm) viewer.loadPdbIhm(pdbIhm);
// support for deprecated pdb-dev param
var pdbDev = getParam('pdb-dev', '[^&]+').trim();
if (pdbDev) viewer.loadPdbDev(pdbDev);
if (pdbDev) viewer.loadPdbIhm(pdbDev);
var emdb = getParam('emdb', '[^&]+').trim();
if (emdb) viewer.loadEmdb(emdb);
@@ -127,8 +131,12 @@
// to aid GC
viewer.dispose();
});
const event = new CustomEvent("molstarViewerCreated", { detail: { viewer } });
window.dispatchEvent(event);
});
</script>
<!-- __MOLSTAR_PWA__ -->
<!-- __MOLSTAR_ANALYTICS__ -->
</body>
</html>

View File

@@ -8,5 +8,5 @@
import './embedded.html';
import './favicon.ico';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
import '../../mol-plugin-ui/skin/light.scss';
export * from './app';

View File

@@ -47,8 +47,8 @@
<div id="app"></div>
<div id='controls'></div>
<div id='sponsor'>
<a href='https://www.entos.ai/envision' target="_blank" rel="noopener">
<svg class="makeStyles-root-46" viewBox="0 0 190 36" xmlns="http://www.w3.org/2000/svg"><path d="M32.2591 28.6707C32.2591 32.3914 29.2421 35.407 25.5214 35.407C22.0752 35.407 19.2338 32.8206 18.8325 29.4831V29.4775C18.8143 29.3312 18.8018 29.1835 18.7934 29.0344C18.7934 29.0316 18.7921 29.0274 18.7921 29.0246V29.0177C18.7865 28.902 18.7837 28.7864 18.7837 28.6707C18.7837 26.2557 20.0532 24.1389 21.9609 22.9503C21.9623 22.9489 21.9651 22.9489 21.9665 22.9475C22.0933 22.8666 22.2243 22.7914 22.3581 22.7203C22.3581 22.7203 22.3595 22.7203 22.3595 22.7189C23.3029 22.2173 24.3787 21.933 25.5214 21.933C29.2421 21.933 32.2591 24.9486 32.2591 28.6707Z"></path><path d="M25.5214 14.0692C29.2421 14.0692 32.2591 11.0522 32.2591 7.33146C32.2591 3.61074 29.2421 0.59375 25.5214 0.59375C22.0529 0.59375 19.1962 3.21637 18.8255 6.58592C18.8185 6.67092 18.8116 6.75454 18.8018 6.83815C18.7893 7.00119 18.7837 7.16563 18.7837 7.33146C18.7837 9.73669 20.0434 11.8465 21.94 13.038C22.0891 13.116 22.2355 13.201 22.3776 13.2916C22.3783 13.2923 22.379 13.2926 22.3797 13.293C22.3804 13.2933 22.3811 13.2937 22.3818 13.2944C23.3196 13.7891 24.3871 14.0692 25.5214 14.0692Z"></path><path d="M19.3645 12.4113C20.2926 12.4113 21.1694 12.638 21.94 13.038C20.0434 11.8465 18.7837 9.73669 18.7837 7.33146C18.7837 7.16563 18.7893 7.00119 18.8018 6.83815C18.4688 9.76455 16.1385 12.0866 13.2065 12.4044C13.8545 13.1193 14.3785 13.9484 14.745 14.857C15.7497 13.3798 17.4443 12.4113 19.3645 12.4113Z"></path><path d="M14.7312 21.1249V21.1236C14.1279 20.2331 13.7767 19.1587 13.7767 18.0007V17.9728C13.7767 15.3084 12.2285 13.0035 9.9835 11.911C9.98141 11.9103 9.97967 11.9096 9.97793 11.9089C9.97619 11.9082 9.97444 11.9075 9.97235 11.9068C9.96817 11.904 9.96538 11.9026 9.9612 11.9012C9.95981 11.9012 9.95981 11.8998 9.95981 11.8998C9.9417 11.8915 9.92394 11.8831 9.90618 11.8747C9.8884 11.8664 9.87063 11.858 9.85251 11.8497C9.82046 11.8343 9.78701 11.819 9.75357 11.8051L9.74521 11.8009C8.91745 11.4372 8.0019 11.2351 7.03898 11.2351C3.31826 11.2351 0.30127 14.2521 0.30127 17.9728C0.30127 21.6935 3.31826 24.7105 7.03898 24.7105C7.98797 24.7105 8.89098 24.514 9.71037 24.1601C9.71246 24.1594 9.7142 24.1583 9.71594 24.1573C9.71768 24.1562 9.71943 24.1552 9.72152 24.1545C9.8107 24.1169 9.8985 24.0765 9.9849 24.0333L9.98629 24.0319C10.7625 23.6919 11.6181 23.5037 12.5197 23.5037C12.7524 23.5037 12.9824 23.5163 13.2081 23.54C13.2082 23.5399 13.2081 23.54 13.2081 23.54C15.0168 23.7365 16.5971 24.695 17.6185 26.0885C17.9195 25.1688 18.3752 24.3201 18.9563 23.5732C17.1964 23.4464 15.6635 22.5058 14.7312 21.1249Z"></path><g clip-path="url(#clip0)"><path d="M106.391 18.0021C106.391 11.3724 101.039 6 94.4389 6H88.4585C81.8581 6 76.5061 11.3724 76.5061 18.0021V30.0042H81.2845V18.0021C81.2845 14.0268 84.4941 10.8008 88.4585 10.8008H94.4347C98.395 10.8008 101.609 14.0226 101.609 18.0021V30.0042H106.391V18.0021Z"></path><path d="M149.432 6H142.258C135.653 6 130.301 11.3724 130.301 18.0021C130.301 24.6319 135.653 30.0042 142.258 30.0042H149.432C156.036 30.0042 161.388 24.6319 161.388 18.0021C161.388 11.3724 156.032 6 149.432 6ZM149.432 25.1992H142.258C138.297 25.1992 135.084 21.9774 135.084 17.9979C135.084 14.0183 138.293 10.7966 142.258 10.7966H149.432C153.392 10.7966 156.606 14.0183 156.606 17.9979C156.606 21.9774 153.392 25.1992 149.432 25.1992Z"></path><path d="M74.1151 25.1992H58.5736C55.4526 25.1992 52.804 23.1924 51.8171 20.3983H74.1151V17.9979C74.1151 17.1808 74.1868 16.3807 74.3175 15.5975H51.8171C52.804 12.8033 55.4526 10.7966 58.5736 10.7966H76.0383C77.1475 8.87458 78.6911 7.22773 80.5299 6H58.5736C51.969 6 46.6169 11.3724 46.6169 18.0021C46.6169 24.6276 51.969 30 58.5736 30H74.1151V25.1992Z"></path><path d="M120.74 6H115.958H102.369C104.212 7.22773 105.751 8.87458 106.861 10.8008H115.958V30H120.74V10.8008H129.838C130.947 8.87458 132.486 7.22773 134.329 6H120.74Z"></path><path d="M182.906 15.6017H169.756C168.436 15.6017 167.365 14.5264 167.365 13.2013C167.365 11.8762 168.436 10.8008 169.756 10.8008H188.882V6H169.756C165.796 6 162.582 9.22173 162.582 13.2013C162.582 17.1808 165.791 20.4025 169.756 20.4025H182.906C184.226 20.4025 185.297 21.4779 185.297 22.803C185.297 24.1281 184.226 25.2034 182.906 25.2034H161.852C160.743 27.1297 159.199 28.7765 157.361 30.0042H182.906C186.866 30.0042 190.08 26.7825 190.08 22.803C190.08 18.8234 186.866 15.6017 182.906 15.6017Z"></path></g><defs><clipPath id="clip0"><rect width="190" height="24" fill="white" transform="translate(0 6)"></rect></clipPath></defs></svg>
<a href='https://www.iambic-envision.com/' target="_blank" rel="noopener">
<svg viewBox="0 0 190 36" xmlns="http://www.w3.org/2000/svg"><path d="M32.2591 28.6707C32.2591 32.3914 29.2421 35.407 25.5214 35.407C22.0752 35.407 19.2338 32.8206 18.8325 29.4831V29.4775C18.8143 29.3312 18.8018 29.1835 18.7934 29.0344C18.7934 29.0316 18.7921 29.0274 18.7921 29.0246V29.0177C18.7865 28.902 18.7837 28.7864 18.7837 28.6707C18.7837 26.2557 20.0532 24.1389 21.9609 22.9503C21.9623 22.9489 21.9651 22.9489 21.9665 22.9475C22.0933 22.8666 22.2243 22.7914 22.3581 22.7203C22.3581 22.7203 22.3595 22.7203 22.3595 22.7189C23.3029 22.2173 24.3787 21.933 25.5214 21.933C29.2421 21.933 32.2591 24.9486 32.2591 28.6707Z"></path><path d="M25.5214 14.0692C29.2421 14.0692 32.2591 11.0522 32.2591 7.33146C32.2591 3.61074 29.2421 0.59375 25.5214 0.59375C22.0529 0.59375 19.1962 3.21637 18.8255 6.58592C18.8185 6.67092 18.8116 6.75454 18.8018 6.83815C18.7893 7.00119 18.7837 7.16563 18.7837 7.33146C18.7837 9.73669 20.0434 11.8465 21.94 13.038C22.0891 13.116 22.2355 13.201 22.3776 13.2916C22.3783 13.2923 22.379 13.2926 22.3797 13.293C22.3804 13.2933 22.3811 13.2937 22.3818 13.2944C23.3196 13.7891 24.3871 14.0692 25.5214 14.0692Z"></path><path d="M19.3645 12.4113C20.2926 12.4113 21.1694 12.638 21.94 13.038C20.0434 11.8465 18.7837 9.73669 18.7837 7.33146C18.7837 7.16563 18.7893 7.00119 18.8018 6.83815C18.4688 9.76455 16.1385 12.0866 13.2065 12.4044C13.8545 13.1193 14.3785 13.9484 14.745 14.857C15.7497 13.3798 17.4443 12.4113 19.3645 12.4113Z"></path><path d="M14.7312 21.1249V21.1236C14.1279 20.2331 13.7767 19.1587 13.7767 18.0007V17.9728C13.7767 15.3084 12.2285 13.0035 9.9835 11.911C9.98141 11.9103 9.97967 11.9096 9.97793 11.9089C9.97619 11.9082 9.97444 11.9075 9.97235 11.9068C9.96817 11.904 9.96538 11.9026 9.9612 11.9012C9.95981 11.9012 9.95981 11.8998 9.95981 11.8998C9.9417 11.8915 9.92394 11.8831 9.90618 11.8747C9.8884 11.8664 9.87063 11.858 9.85251 11.8497C9.82046 11.8343 9.78701 11.819 9.75357 11.8051L9.74521 11.8009C8.91745 11.4372 8.0019 11.2351 7.03898 11.2351C3.31826 11.2351 0.30127 14.2521 0.30127 17.9728C0.30127 21.6935 3.31826 24.7105 7.03898 24.7105C7.98797 24.7105 8.89098 24.514 9.71037 24.1601C9.71246 24.1594 9.7142 24.1583 9.71594 24.1573C9.71768 24.1562 9.71943 24.1552 9.72152 24.1545C9.8107 24.1169 9.8985 24.0765 9.9849 24.0333L9.98629 24.0319C10.7625 23.6919 11.6181 23.5037 12.5197 23.5037C12.7524 23.5037 12.9824 23.5163 13.2081 23.54C13.2082 23.5399 13.2081 23.54 13.2081 23.54C15.0168 23.7365 16.5971 24.695 17.6185 26.0885C17.9195 25.1688 18.3752 24.3201 18.9563 23.5732C17.1964 23.4464 15.6635 22.5058 14.7312 21.1249Z"></path><g clip-path="url(#clip0)"><path d="M106.391 18.0021C106.391 11.3724 101.039 6 94.4389 6H88.4585C81.8581 6 76.5061 11.3724 76.5061 18.0021V30.0042H81.2845V18.0021C81.2845 14.0268 84.4941 10.8008 88.4585 10.8008H94.4347C98.395 10.8008 101.609 14.0226 101.609 18.0021V30.0042H106.391V18.0021Z"></path><path d="M149.432 6H142.258C135.653 6 130.301 11.3724 130.301 18.0021C130.301 24.6319 135.653 30.0042 142.258 30.0042H149.432C156.036 30.0042 161.388 24.6319 161.388 18.0021C161.388 11.3724 156.032 6 149.432 6ZM149.432 25.1992H142.258C138.297 25.1992 135.084 21.9774 135.084 17.9979C135.084 14.0183 138.293 10.7966 142.258 10.7966H149.432C153.392 10.7966 156.606 14.0183 156.606 17.9979C156.606 21.9774 153.392 25.1992 149.432 25.1992Z"></path><path d="M74.1151 25.1992H58.5736C55.4526 25.1992 52.804 23.1924 51.8171 20.3983H74.1151V17.9979C74.1151 17.1808 74.1868 16.3807 74.3175 15.5975H51.8171C52.804 12.8033 55.4526 10.7966 58.5736 10.7966H76.0383C77.1475 8.87458 78.6911 7.22773 80.5299 6H58.5736C51.969 6 46.6169 11.3724 46.6169 18.0021C46.6169 24.6276 51.969 30 58.5736 30H74.1151V25.1992Z"></path><path d="M120.74 6H115.958H102.369C104.212 7.22773 105.751 8.87458 106.861 10.8008H115.958V30H120.74V10.8008H129.838C130.947 8.87458 132.486 7.22773 134.329 6H120.74Z"></path><path d="M182.906 15.6017H169.756C168.436 15.6017 167.365 14.5264 167.365 13.2013C167.365 11.8762 168.436 10.8008 169.756 10.8008H188.882V6H169.756C165.796 6 162.582 9.22173 162.582 13.2013C162.582 17.1808 165.791 20.4025 169.756 20.4025H182.906C184.226 20.4025 185.297 21.4779 185.297 22.803C185.297 24.1281 184.226 25.2034 182.906 25.2034H161.852C160.743 27.1297 159.199 28.7765 157.361 30.0042H182.906C186.866 30.0042 190.08 26.7825 190.08 22.803C190.08 18.8234 186.866 15.6017 182.906 15.6017Z"></path></g><defs><clipPath id="clip0"><rect width="190" height="24" fill="white" transform="translate(0 6)"></rect></clipPath></defs></svg>
<div>
Entos Envision
</div>

View File

@@ -24,7 +24,7 @@ import { ParamDefinition } from '../../mol-util/param-definition';
import { mountControls } from './controls';
import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
import '../../mol-plugin-ui/skin/light.scss';
import { setDebugMode, setTimingMode, consoleStats } from '../../mol-util/debug';

View File

@@ -11,7 +11,7 @@ import { MAPairwiseScorePlot } from '../../extensions/model-archive/quality-asse
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
import { Model, ResidueIndex } from '../../mol-model/structure';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
import '../../mol-plugin-ui/skin/light.scss';
export class AlphaFoldPAEExample {
viewer: Viewer;
@@ -46,7 +46,7 @@ export class AlphaFoldPAEExample {
const model = this.viewer.plugin.managers.structure.hierarchy.current.models[0]?.cell.obj?.data!;
const metric = pairwiseMetricFromAlphaFoldDbJson(model, json)!;
createRoot(document.getElementById(this.plotContainerId)!).render(
plotRoot.render(
<div className='msp-plugin' style={{ background: 'white' }}>
<MAPairwiseScorePlot plugin={this.viewer.plugin} pairwiseMetric={metric} model={model} />
</div>

View File

@@ -1,6 +1,7 @@
import { isPositionLocation } from '../../mol-geo/util/location-iterator';
import { Vec3 } from '../../mol-math/linear-algebra';
import { ColorTheme } from '../../mol-theme/color';
import { ColorThemeCategory } from '../../mol-theme/color/categories';
import { ThemeDataContext } from '../../mol-theme/theme';
import { Color } from '../../mol-util/color';
import { ColorNames } from '../../mol-util/color/names';
@@ -43,7 +44,7 @@ export function CustomColorTheme(
export const CustomColorThemeProvider: ColorTheme.Provider<{}, 'basic-wrapper-custom-color-theme'> = {
name: 'basic-wrapper-custom-color-theme',
label: 'Custom Color Theme',
category: ColorTheme.Category.Misc,
category: ColorThemeCategory.Misc,
factory: CustomColorTheme,
getParams: () => ({}),
defaultValues: { },

View File

@@ -22,7 +22,7 @@ import { CustomToastMessage } from './controls';
import { CustomColorThemeProvider } from './custom-theme';
import './index.html';
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData } from './superposition';
require('mol-plugin-ui/skin/light.scss');
import '../../mol-plugin-ui/skin/light.scss';
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }

View File

@@ -0,0 +1,49 @@
<!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>Mol* IHM Restraints Example</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: sans-serif;
}
#viewer {
position: absolute;
left: 20px;
top: 20px;
width: 640px;
height: 480px;
}
#links {
position: absolute;
bottom: 8px;
left: 8px;
font-family: sans-serif;
font-size: 0.8rem;
z-index: 2;
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
<script type="text/javascript" src="./index.js"></script>
</head>
<body>
<div id="viewer"></div>
<div id="links">
<a href="#" id="mvs-data" filename="ihm-restraints.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/ihm-restraints" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>
<script>
loadIHMRestraints(document.getElementById('viewer')).then(state => {
document.getElementById('mvs-data').addEventListener('click', (e) => {
e.preventDefault();
const data = JSON.stringify(state, null, 2);
molStarDownload(new Blob([data], { type: 'application/json' }), 'ihm-restraints.mvsj');
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,366 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { loadMVS } from '../../extensions/mvs/load';
import { MVSData_States, Snapshot } from '../../extensions/mvs/mvs-data';
import { createMVSBuilder } from '../../extensions/mvs/tree/mvs/mvs-builder';
import { parseCifText } from '../../mol-io/reader/cif/text/parser';
import { Vec3 } from '../../mol-math/linear-algebra';
import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
import { Model } from '../../mol-model/structure';
import { CoarseElementKey, CoarseElementReference } from '../../mol-model/structure/model/properties/coarse';
import { createPluginUI } from '../../mol-plugin-ui';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginContext } from '../../mol-plugin/context';
import { PluginSpec } from '../../mol-plugin/spec';
import { Task } from '../../mol-task';
import { ajaxGet } from '../../mol-util/data-source';
import { download } from '../../mol-util/download';
import './index.html';
import '../../mol-plugin-ui/skin/light.scss';
async function createViewer(root: HTMLElement) {
const spec = DefaultPluginUISpec();
const plugin = await createPluginUI({
target: root,
render: renderReact18,
spec: {
...spec,
layout: {
initial: {
isExpanded: true,
showControls: false
}
},
components: {
remoteState: 'none',
},
behaviors: [
...spec.behaviors,
PluginSpec.Behavior(MolViewSpec)
],
config: [
[PluginConfig.Viewport.ShowAnimation, false],
[PluginConfig.Viewport.ShowTrajectoryControls, false],
]
}
});
return plugin;
}
interface IHMRestraintInfo {
e1: CoarseElementKey & { label_comp_id: string },
e2: CoarseElementKey & { label_comp_id: string },
a: Vec3,
b: Vec3,
restraintType: 'harmonic' | 'upper bound' | 'lower bound',
threshold: number,
satisfied: boolean,
distance: number,
}
interface IHMStructureInfo {
entity_labels: [id: string | undefined, label: string | undefined][],
model_restraints: IHMRestraintInfo[][],
}
function getCoarseElementPosition(e: CoarseElementReference, model: Model, position: Vec3) {
if (!e.kind) Vec3.set(position, 0, 0, 0);
const { x, y, z } = model.coarseConformation[e.kind!];
const idx = e.index;
Vec3.set(position, x[idx], y[idx], z[idx]);
}
const _elementRef = CoarseElementReference();
function resolvePosition(model: Model, key: CoarseElementKey, position: Vec3) {
if (model.coarseHierarchy.index.findElement(key, _elementRef)) {
getCoarseElementPosition(_elementRef, model, position);
return true;
}
const rI = model.atomicHierarchy.index.findResidueLabel(key);
if (rI < 0) return false;
const atomStart = model.atomicHierarchy.residueAtomSegments.offsets[rI];
const atomEnd = model.atomicHierarchy.residueAtomSegments.offsets[rI + 1];
const atomId = model.atomicHierarchy.atoms.label_atom_id;
let aI = atomStart;
// Find CA otherwise use the first atom.
// Possible future improvement: use the atom closest to the center of mass of the residue.
for (; aI < atomEnd; aI++) {
if (atomId.value(aI) === 'CA') break;
}
if (aI === atomEnd) aI = atomStart;
const { x, y, z } = model.atomicConformation;
Vec3.set(position, x[aI], y[aI], z[aI]);
return true;
}
const HarmonicRestraintTolerance = 0.1;
async function parseInfo(plugin: PluginContext, url: string): Promise<IHMStructureInfo> {
const data = await plugin.runTask(ajaxGet(url)) as string;
const parsed = await plugin.runTask(parseCifText(data));
if (parsed.isError) {
console.error(parsed);
return { entity_labels: [], model_restraints: [] };
}
const trajectory = await plugin.runTask(trajectoryFromMmCIF(parsed.result.blocks[0], parsed.result));
const dataBlocks = parsed.result.blocks;
const ihm_cross_link_restraint = dataBlocks[0].categories['ihm_cross_link_restraint'];
const entity_id_1 = ihm_cross_link_restraint.getField('entity_id_1')!;
const asym_id_1 = ihm_cross_link_restraint.getField('asym_id_1')!;
const seq_id_1 = ihm_cross_link_restraint.getField('seq_id_1')!;
const comp_id_1 = ihm_cross_link_restraint.getField('comp_id_1')!;
const entity_id_2 = ihm_cross_link_restraint.getField('entity_id_2')!;
const asym_id_2 = ihm_cross_link_restraint.getField('asym_id_2')!;
const seq_id_2 = ihm_cross_link_restraint.getField('seq_id_2')!;
const comp_id_2 = ihm_cross_link_restraint.getField('comp_id_2')!;
const restraint_type = ihm_cross_link_restraint.getField('restraint_type')!;
const threshold = ihm_cross_link_restraint.getField('distance_threshold')!;
const e1key = CoarseElementKey();
const e2key = CoarseElementKey();
const a = Vec3.zero();
const b = Vec3.zero();
const entity_labels: IHMStructureInfo['entity_labels'] = [];
const entity = dataBlocks[0].categories['entity'];
const entity_id = entity.getField('id');
const pdbx_description = entity.getField('pdbx_description');
for (let i = 0; i < entity.rowCount; i++) {
entity_labels.push([entity_id?.str(i), pdbx_description?.str(i)]);
}
const model_restraints: IHMRestraintInfo[][] = [];
for (let modelIndex = 0; modelIndex < trajectory.frameCount; modelIndex++) {
const _model = trajectory.getFrameAtIndex(modelIndex);
const model = Task.is(_model) ? await plugin.runTask(_model) : _model;
const restraints: IHMRestraintInfo[] = [];
model_restraints.push(restraints);
for (let i = 0; i < ihm_cross_link_restraint.rowCount; i++) {
e1key.label_entity_id = entity_id_1.str(i);
e1key.label_asym_id = asym_id_1.str(i);
e1key.label_seq_id = seq_id_1.int(i);
e2key.label_entity_id = entity_id_2.str(i);
e2key.label_asym_id = asym_id_2.str(i);
e2key.label_seq_id = seq_id_2.int(i);
if (!resolvePosition(model, e1key, a) || !resolvePosition(model, e2key, b)) {
continue;
}
const restraintType: 'harmonic' | 'upper bound' | 'lower bound' = restraint_type.str(i)?.toLowerCase() as any;
const thresholdValue = threshold.float(i);
const distance = Vec3.distance(a, b);
let satisfied = true;
if (restraintType === 'harmonic') {
const thresholdValue = threshold.float(i);
satisfied = distance >= (1 - HarmonicRestraintTolerance) * thresholdValue && distance <= (1 + HarmonicRestraintTolerance) * thresholdValue;
} else if (restraintType === 'upper bound') {
satisfied = distance <= thresholdValue;
} else if (restraintType === 'lower bound') {
satisfied = distance >= thresholdValue;
}
restraints.push({
e1: { ...e1key, label_comp_id: comp_id_1.str(i) },
e2: { ...e2key, label_comp_id: comp_id_2.str(i) },
a: Vec3.clone(a),
b: Vec3.clone(b),
restraintType,
threshold: thresholdValue,
satisfied,
distance,
});
}
}
return { entity_labels, model_restraints };
}
function baseStructure(url: string, modelIndex: number, info: IHMStructureInfo, options?: { noEntityLabels?: boolean }) {
const builder = createMVSBuilder();
const structure = builder
.download({ url })
.parse({ format: 'mmcif' })
.modelStructure({ model_index: modelIndex });
structure
.component({ selector: 'coarse' })
.representation({ type: 'spacefill' })
.color({ custom: { molstar_use_default_coloring: true } })
.opacity({ opacity: 0.51 });
structure
.component({ selector: 'polymer' })
.representation({ type: 'cartoon' })
.color({ custom: { molstar_use_default_coloring: true } })
.opacity({ opacity: 0.51 });
if (!options?.noEntityLabels) {
const primitives = structure.primitives();
for (const [label_entity_id, text] of info.entity_labels) {
if (!text) continue;
primitives
.label({ position: { label_entity_id }, text, label_size: 16, label_color: '#cccccc' });
}
}
return [builder, structure] as const;
}
function drawConstraints([, structure]: ReturnType<typeof baseStructure>, restraints: IHMRestraintInfo[], options: {
filter: (r: IHMRestraintInfo) => boolean,
color: (r: IHMRestraintInfo) => any,
radius?: (r: IHMRestraintInfo) => number,
tooltip: (r: IHMRestraintInfo) => string | undefined,
}) {
const primitives = structure.primitives();
for (const r of restraints) {
if (!options.filter(r)) continue;
const radius = options.radius?.(r) ?? 1;
primitives.tube({
start: r.a as any,
end: r.b as any,
color: options.color(r) || 'white',
tooltip: options.tooltip(r),
radius: radius,
dash_length: radius,
});
}
}
function restraintTooltip(r: IHMRestraintInfo) {
return `
- Element 1: ${r.e1.label_entity_id} ${r.e1.label_asym_id} ${r.e1.label_seq_id} ${r.e1.label_comp_id}
- Element 2: ${r.e2.label_entity_id} ${r.e2.label_asym_id} ${r.e2.label_seq_id} ${r.e2.label_comp_id}
- Distance: ${r.distance.toFixed(2)} Å
- Threshold: ${r.threshold.toFixed(2)} Å
- Constraint: ${r.restraintType}
- Satisfied: ${r.satisfied ? 'Yes' : 'No'}
`;
}
export async function loadIHMRestraints(root: HTMLElement, url?: string) {
url ??= 'https://pdb-ihm.org/cif/8zz1.cif';
const plugin = await createViewer(root);
const info = await parseInfo(plugin, url);
const modelIndex = 0;
const restraints = info.model_restraints[modelIndex];
const nVialoted = restraints.filter(r => !r.satisfied).length;
const nSatisfied = restraints.length - nVialoted;
const snapshots: Snapshot[] = [];
let mvs = baseStructure(url, modelIndex, info);
drawConstraints(mvs, restraints, {
filter: r => true,
color: r => r.e1.label_entity_id === r.e2.label_entity_id && r.e1.label_asym_id === r.e2.label_asym_id ? 'yellow' : 'blue',
radius: r => 1,
tooltip: restraintTooltip,
});
snapshots.push(mvs[0].getSnapshot({
title: 'All Restraints',
linger_duration_ms: 5000,
description: `
### All Restraints
- Yellow: Intra-chain restraints
- Blue: Inter-chain restraints
`,
}));
mvs = baseStructure(url, modelIndex, info);
drawConstraints(mvs, restraints, {
filter: r => true,
color: r => r.satisfied ? 'green' : 'red',
radius: r => 1,
tooltip: restraintTooltip,
});
snapshots.push(mvs[0].getSnapshot({
title: 'Restraint Validation',
linger_duration_ms: 5000,
description: `
### Restraint Validation
- Red: ${nVialoted} Violated restraints
- Green: ${nSatisfied} Satisfied restraints
`,
}));
mvs = baseStructure(url, modelIndex, info);
drawConstraints(mvs, restraints, {
filter: r => !r.satisfied,
color: r => r.satisfied ? 'green' : 'red',
radius: r => 1,
tooltip: restraintTooltip,
});
snapshots.push(mvs[0].getSnapshot({
title: 'Violated Restraints',
linger_duration_ms: 5000,
description: `
### Violated Restraints
${nVialoted} restraints are violated.
`,
}));
mvs = baseStructure(url, modelIndex, info);
drawConstraints(mvs, restraints, {
filter: r => r.satisfied,
color: r => r.satisfied ? 'green' : 'red',
radius: r => 1,
tooltip: restraintTooltip,
});
snapshots.push(mvs[0].getSnapshot({
title: 'Satisfied Restraints',
linger_duration_ms: 5000,
description: `
### Satisfied Restraints
${nSatisfied} restraints are satisfied.
`,
}));
const data: MVSData_States = {
kind: 'multiple',
snapshots,
metadata: {
title: 'I/HM Restraints',
version: '1.0',
timestamp: new Date().toISOString(),
}
};
await loadMVS(plugin, data, { sanityChecks: true, replaceExisting: true, keepSnapshotCamera: true });
return data;
}
(window as any).loadIHMRestraints = loadIHMRestraints;
(window as any).molStarDownload = download;

View File

@@ -0,0 +1,35 @@
# I/HM Restraints Example
This example illustrates:
- Using Mol* to parse CIF files and extract custom data
- Using MolViewSpec to visualize I/HM structure along with annotated crosslink restraints
### Usage
- Clone Mol* GitHub repo and build it.
```bash
git clone https://github.com/molstar/molstar.git
cd molstar
npm install
npm build
```
- Get `molstar.css` and `index.js` from `build/examples/ihm-restraints` and include these to your HTML page in a similar fashion to [index.html](./index.html):
```html
<link rel="stylesheet" type="text/css" href="molstar.css" />
<script type="text/javascript" src="index.js"></script>
...
<div id="viewer"></div>
...
<script>
loadIHMRestraints(document.getElementById('viewer'))
</script>
```
- For interactive development build (for production use `npm run build`) of the example that immediately reflects changes use:
```bash
npm run dev -- -e ihm-restraints
```

View File

@@ -0,0 +1,46 @@
<!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>Mol* Interactions Example</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: sans-serif;
}
#viewer {
position: absolute;
left: 20px;
top: 20px;
width: 640px;
height: 480px;
}
#controls {
position: absolute;
left: 40%;
bottom: 8px;
font-family: sans-serif;
padding: 8px;
border: 1px dotted #aaa;
background: rgba(255, 255, 255, 0.5);
}
#controls select {
margin-left: 4px;
padding: 4px 8px;
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<body>
<div id="viewer"></div>
<div id="controls">
</div>
<script type="text/javascript" src="./index.js"></script>
<script>
initInteractionsExample('viewer', 'controls');
</script>
</body>
</html>

View File

@@ -0,0 +1,383 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { createRoot } from 'react-dom/client';
import { BehaviorSubject } from 'rxjs';
import { InteractionElementSchema, InteractionKind, StructureInteractionElement, StructureInteractions } from '../../extensions/interactions/model';
import { ComputeContacts, CustomInteractions, InteractionsShape } from '../../extensions/interactions/transforms';
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { ResidueIndex, Structure, StructureElement, StructureProperties, StructureQuery } from '../../mol-model/structure';
import { atoms } from '../../mol-model/structure/query/queries/generators';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { MultiStructureSelectionFromBundle, StructureSelectionFromBundle } from '../../mol-plugin-state/transforms/model';
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { createPluginUI } from '../../mol-plugin-ui';
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginContext } from '../../mol-plugin/context';
import { PluginSpec } from '../../mol-plugin/spec';
import '../../mol-plugin-ui/skin/light.scss';
import './index.html';
import { Task } from '../../mol-task';
import { computeContacts } from '../../extensions/interactions/compute';
async function createViewer(root: HTMLElement) {
const spec = DefaultPluginUISpec();
const plugin = await createPluginUI({
target: root,
render: renderReact18,
spec: {
...spec,
layout: {
initial: {
isExpanded: true,
showControls: false
}
},
components: {
remoteState: 'none',
},
behaviors: [
...spec.behaviors,
PluginSpec.Behavior(MolViewSpec)
],
config: [
[PluginConfig.Viewport.ShowAnimation, false],
]
}
});
return plugin;
}
async function createBindingSiteRepresentation(plugin: PluginContext, interactions: StructureInteractions[], receptors: Map<string, Structure>) {
const contactBundles = getBindingSiteBundles(interactions.flatMap(e => e.elements), receptors);
const update = plugin.build();
for (const [ref, bundle] of contactBundles) {
update.to(ref)
.apply(StructureSelectionFromBundle, { bundle, label: 'Binding Site' })
.apply(StructureRepresentation3D, {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.2 } },
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
});
}
await update.commit();
}
function getBindingSiteBundles(interactions: StructureInteractionElement[], receptors: Map<string, Structure>) {
const residueIndices = new Map<string, Set<ResidueIndex>>();
const loc = StructureElement.Location.create();
const add = (ref: string, loci: StructureElement.Loci) => {
if (!receptors.has(ref)) return;
let set: Set<ResidueIndex>;
if (residueIndices.has(ref)) {
set = residueIndices.get(ref)!;
} else {
set = new Set<ResidueIndex>();
residueIndices.set(ref, set);
}
StructureElement.Loci.forEachLocation(loci, l => {
set.add(StructureProperties.residue.key(l));
}, loc);
};
for (const e of interactions) {
add(e.aStructureRef!, e.a);
add(e.bStructureRef!, e.b);
}
const bundles: [ref: string, bundle: StructureElement.Bundle][] = [];
for (const [ref, indices] of Array.from(residueIndices.entries())) {
if (indices.size === 0) continue;
const loci = StructureQuery.loci(
atoms({
residueTest: e => indices.has(StructureProperties.residue.key(e.element))
}),
receptors.get(ref)!,
);
if (StructureElement.Loci.isEmpty(loci)) continue;
bundles.push([ref, StructureElement.Bundle.fromLoci(loci)]);
}
return bundles;
}
async function loadComputedExample(
plugin: PluginContext,
{ receptorUrl, ligandUrl }: { receptorUrl: [url: string, format: BuiltInTrajectoryFormat], ligandUrl: [url: string, format: BuiltInTrajectoryFormat] },
options: { receptor_label_asym_id: string | undefined, analyzeTrajectory?: boolean }
) {
await plugin.clear();
// Set up the receptor and ligand structures
const receptorData = await plugin.builders.data.download({ url: receptorUrl[0] });
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, receptorUrl[1]);
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default', { representationPreset: 'polymer-cartoon' });
const ligandData = await plugin.builders.data.download({ url: ligandUrl[0] });
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, ligandUrl[1]);
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
// Compute the interactions
const update = plugin.build();
const receptorRef = receptor?.structure.ref!;
const ligandRef = ligand?.structure.ref!;
const refs = [receptorRef, ligandRef];
const interactionsRef = update.toRoot()
.apply(MultiStructureSelectionFromBundle, {
selections: [
{ key: 'a', ref: receptorRef, bundle: StructureElement.Schema.toBundle(receptor?.structure.data!, { label_asym_id: options.receptor_label_asym_id }) },
{ key: 'b', ref: ligandRef, bundle: StructureElement.Schema.toBundle(ligand?.structure.data!, { }) },
],
isTransitive: true,
label: 'Label'
}, { dependsOn: refs })
.apply(ComputeContacts);
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
await update.commit();
if (!options.analyzeTrajectory) {
console.log('Interactions', interactionsRef.selector.data?.interactions);
// Create ball and stick representations for the binding site and focus on the ligand
await createBindingSiteRepresentation(
plugin,
[interactionsRef.selector.data?.interactions!],
new Map([[receptorRef, receptor?.structure.data!]])
);
} else {
const trajectoryInteractions: StructureInteractions[] = [];
const receptorLoci = StructureElement.Schema.toLoci(receptor?.structure.data!, { label_asym_id: options.receptor_label_asym_id });
for (let fI = 0; fI < ligandTrajectory.data!.frameCount; fI++) {
const model = await Task.resolveInContext(ligandTrajectory.data!.getFrameAtIndex(fI));
const structure = Structure.ofModel(model);
const currentInteractions = await plugin.runTask(Task.create('Compute Contacts', ctx => {
return computeContacts(ctx, [
{ structureRef: receptorRef, loci: receptorLoci },
{ structureRef: ligandRef, loci: Structure.toStructureElementLoci(structure) },
]);
}));
trajectoryInteractions.push(currentInteractions);
}
console.log('Interactions', trajectoryInteractions);
await createBindingSiteRepresentation(
plugin,
trajectoryInteractions,
new Map([[receptorRef, receptor?.structure.data!]])
);
}
PluginCommands.Camera.FocusObject(plugin, {
targets: [{
targetRef: ligand?.representation.representations.all.ref
}]
});
}
async function loadCustomExample(plugin: PluginContext) {
await plugin.clear();
// Set up the receptor and ligand structures
const receptorData = await plugin.builders.data.download({ url: '../../../examples/ace2.pdbqt' });
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, 'pdbqt');
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default');
const ligandData = await plugin.builders.data.download({ url: '../../../examples/ace2-hit.mol2' });
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, 'mol2');
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
// Compute the interactions
const update = plugin.build();
const receptorRef = receptor?.representation.components.polymer.ref!;
const ligandRef = ligand?.representation.components.all.ref!;
const refs = [receptorRef, ligandRef];
const interactionsRef = update.toRoot().apply(CustomInteractions, {
interactions: [
{
kind: 'hydrogen-bond',
aStructureRef: receptorRef,
a: { auth_seq_id: 353, auth_atom_id: 'N' },
bStructureRef: ligandRef,
b: { atom_index: 9 },
}
]
}, { dependsOn: refs });
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
await update.commit();
console.log('Interactions', interactionsRef.selector.data?.interactions);
// Create ball and stick representations for the binding site and focus on the ligand
await createBindingSiteRepresentation(
plugin,
[interactionsRef.selector.data?.interactions!],
new Map([[receptorRef, receptor?.representation.components.polymer.data]])
);
PluginCommands.Camera.FocusObject(plugin, {
targets: [{
targetRef: ligand?.representation.representations.all.ref
}]
});
}
async function loadTestAllExample(plugin: PluginContext) {
await plugin.clear();
// Set up the receptor and ligand structures
const receptorData = await plugin.builders.data.download({ url: '../../../examples/ace2.pdbqt' });
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, 'pdbqt');
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default');
const ligandData = await plugin.builders.data.download({ url: '../../../examples/ace2-hit.mol2' });
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, 'mol2');
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
// Compute the interactions
const update = plugin.build();
const receptorRef = receptor?.representation.components.polymer.ref!;
const ligandRef = ligand?.representation.components.all.ref!;
const refs = [receptorRef, ligandRef];
const basic = (kind: InteractionKind, atom_index: number | number[], description?: string): InteractionElementSchema => {
return {
kind,
aStructureRef: receptorRef,
a: { auth_seq_id: 354, auth_atom_id: 'N' },
bStructureRef: ligandRef,
b: Array.isArray(atom_index) ? { items: { atom_index } } : { atom_index },
description,
};
};
const covalent = (degree: number, atom_index: number): InteractionElementSchema => {
return {
kind: 'covalent',
degree: degree === -1 ? 'aromatic' : Math.abs(degree) as 1 | 2 | 3 | 4,
aStructureRef: receptorRef,
a: { auth_seq_id: 354, auth_atom_id: 'N' },
bStructureRef: ligandRef,
b: { atom_index }
};
};
const interactionsRef = update.toRoot().apply(CustomInteractions, {
interactions: [
basic('unknown', 1),
basic('ionic', 2),
basic('pi-stacking', 3),
basic('cation-pi', 4),
basic('halogen-bond', 5),
basic('hydrogen-bond', 6),
basic('weak-hydrogen-bond', 7),
basic('hydrophobic', 8),
basic('metal-coordination', 9),
basic('salt-bridge', 10),
covalent(1, 11),
covalent(2, 12),
covalent(3, 13),
covalent(-1, 14), // aromatic
basic('unknown', [0, 1, 2, 3, 13, 14], 'Testing centroid for atom set'),
]
}, { dependsOn: refs });
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
await update.commit();
console.log('Interactions', interactionsRef.selector.data?.interactions);
// Create ball and stick representations for the binding site and focus on the ligand
await createBindingSiteRepresentation(
plugin,
[interactionsRef.selector.data?.interactions!],
new Map([[receptorRef, receptor?.representation.components.polymer.data]])
);
PluginCommands.Camera.FocusObject(plugin, {
targets: [{
targetRef: ligand?.representation.representations.all.ref
}]
});
}
const Examples = {
'Computed (1iep)': (plugin: PluginContext) => loadComputedExample(plugin, {
receptorUrl: ['https://files.rcsb.org/download/1IEP.cif', 'mmcif'],
ligandUrl: ['https://models.rcsb.org/v1/1iep/atoms?label_asym_id=G&copy_all_categories=false', 'mmcif']
}, { receptor_label_asym_id: 'A' }),
'Computed (ACE2)': (plugin: PluginContext) => loadComputedExample(plugin, {
receptorUrl: ['../../../examples/ace2.pdbqt', 'pdbqt'],
ligandUrl: ['../../../examples/ace2-hit.mol2', 'mol2']
}, { receptor_label_asym_id: 'B' }),
'Computed (multiple)': (plugin: PluginContext) => loadComputedExample(plugin, {
receptorUrl: ['../../../examples/docking/receptor_1.pdb', 'pdb'],
ligandUrl: ['../../../examples/docking/ligands_1.sdf', 'sdf']
}, { receptor_label_asym_id: undefined, analyzeTrajectory: true }),
'Custom': loadCustomExample,
'Synthetic': loadTestAllExample
};
function SelectExampleUI({ state, load }: {
state: BehaviorSubject<{ name?: keyof typeof Examples, isLoading?: boolean }>,
load: (name: keyof typeof Examples) => void
}) {
const current = useBehavior(state);
return <div>
Select Example:{' '}
<select value={current.name} onChange={e => load(e.target.value as any)} disabled={current.isLoading}>
{Object.keys(Examples).map(k => <option key={k} value={k}>{k}</option>)}
</select>
</div>;
}
async function init(viewer: HTMLElement | string, controls: HTMLElement | string, defaultExample: keyof typeof Examples = 'Computed (1iep)') {
const root = typeof viewer === 'string' ? document.getElementById('viewer')! : viewer;
const plugin = await createViewer(root);
const state = new BehaviorSubject<{ name?: keyof typeof Examples, isLoading?: boolean }>({});
const loadExample = async (name: keyof typeof Examples) => {
state.next({ name, isLoading: true });
try {
await Examples[name](plugin);
state.next({ name });
} catch (e) {
console.error(e);
state.next({});
}
};
createRoot(
typeof controls === 'string' ? document.getElementById('controls')! : controls
).render(<SelectExampleUI state={state} load={loadExample} />);
loadExample(defaultExample);
return { plugin, loadExample };
}
(window as any).initInteractionsExample = init;

View File

@@ -0,0 +1,16 @@
# Interactions Example
An example showcasing features of the `interactions` extension, including:
- Computing interactions on demand using Mol*'s built-in interaction computation code
- Triggering interaction computation separately for multiple ligands stored in a single SDF file
- Specifying interaction manually from custom data source
- Synthetic example showing all supported interaction types
To run development build locally from the root `molstar` directory (after `npm install`):
```bash
npm run dev -- -e interactions
```
and navigate to `build/examples/interactions` in the hosted server linked in the script output.

View File

@@ -14,7 +14,7 @@ import { PluginCommands } from '../../mol-plugin/commands';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
import '../../mol-plugin-ui/skin/light.scss';
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { BehaviorSubject } from 'rxjs';
import { MVSData } from '../../extensions/mvs/mvs-data';
import type { MolComponentViewerModel } from './elements/viewer';
export type MolComponentCommand =
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData }
export class MolComponentContext {
commands = new BehaviorSubject<MolComponentCommand | undefined>(undefined);
behavior = {
viewers: new BehaviorSubject<{ name?: string, model: MolComponentViewerModel }[]>([]),
};
dispatch(command: MolComponentCommand) {
this.commands.next(command);
}
constructor(public name?: string) {
}
}
export function getMolComponentContext(options?: { name?: string, container?: object }) {
const container: any = options?.container ?? window;
container.componentContexts ??= {};
const name = options?.name ?? '<default>';
if (!container.componentContexts[name]) {
container.componentContexts[name] = new MolComponentContext(options?.name);
}
return container.componentContexts[name];
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { BehaviorSubject, distinctUntilChanged, map } from 'rxjs';
import { PluginComponent } from '../../../mol-plugin-state/component';
import { getMolComponentContext, MolComponentContext } from '../context';
import { MolComponentViewerModel } from './viewer';
import Markdown from 'react-markdown';
import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
import { createRoot } from 'react-dom/client';
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
import { MarkdownAnchor } from '../../../mol-plugin-ui/controls';
import { PluginReactContext } from '../../../mol-plugin-ui/base';
export class MolComponentSnapshotMarkdownModel extends PluginComponent {
readonly context: MolComponentContext;
root: HTMLElement | undefined = undefined;
state = new BehaviorSubject<{
entry?: PluginStateSnapshotManager.Entry,
index?: number,
all: PluginStateSnapshotManager.Entry[],
}>({ all: [] });
get viewer() {
return this.context.behavior.viewers.value?.find(v => this.options?.viewerName === v.name);
}
sync() {
const mng = this.viewer?.model.plugin?.managers.snapshot;
this.state.next({
entry: mng?.current,
index: mng?.current ? mng?.getIndex(mng.current) : undefined,
all: mng?.state.entries.toArray() ?? [],
});
}
async mount(root: HTMLElement) {
this.root = root;
createRoot(root).render(<MolComponentSnapshotMarkdownUI model={this} />);
let currentViewer: MolComponentViewerModel | undefined = undefined;
let sub: { unsubscribe: () => void } | undefined = undefined;
this.subscribe(this.context.behavior.viewers.pipe(
map(xs => xs.find(v => this.options?.viewerName === v.name)),
distinctUntilChanged((a, b) => a?.model === b?.model)
), viewer => {
if (currentViewer !== viewer) {
currentViewer = viewer?.model;
sub?.unsubscribe();
}
if (!viewer) return;
sub = this.subscribe(viewer.model.plugin?.managers.snapshot.events.changed, () => {
this.sync();
});
this.sync();
});
this.sync();
}
constructor(private options?: { context?: { name?: string, container?: object }, viewerName?: string }) {
super();
this.context = getMolComponentContext(options?.context);
}
}
export function MolComponentSnapshotMarkdownUI({ model }: { model: MolComponentSnapshotMarkdownModel }) {
const state = useBehavior(model.state);
if (state.all.length === 0) {
return <div>
<i>No snapshot loaded</i>
</div>;
}
return <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'row', width: '100%', gap: '8px' }} className='mc-snapshot-markdown-header'>
<span style={{ lineHeight: '38px', minWidth: 60, maxWidth: 60, flexShrink: 0 }}>{typeof state.index === 'number' ? state.index + 1 : '-'}/{state.all.length}</span>
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(-1)} style={{ flexGrow: 1, flexShrink: 0 }}>Prev</button>
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(1)} style={{ flexGrow: 1, flexShrink: 0 }}>Next</button>
</div>
<div style={{ flexGrow: 1, overflow: 'hidden', overflowY: 'auto', position: 'relative' }}>
<div style={{ position: 'absolute', inset: 0 }}>
<PluginReactContext.Provider value={model.viewer?.model.plugin as any}>
<Markdown skipHtml components={{ a: MarkdownAnchor }}>{state.entry?.description ?? 'Description not available'}</Markdown>
</PluginReactContext.Provider>
</div>
</div>
</div>;
}
export class MolComponentSnapshotMarkdownViewer extends HTMLElement {
private model: MolComponentSnapshotMarkdownModel | undefined = undefined;
async connectedCallback() {
this.model = new MolComponentSnapshotMarkdownModel({
context: { name: this.getAttribute('context-name') ?? undefined },
viewerName: this.getAttribute('viewer-name') ?? undefined,
});
await this.model.mount(this);
}
disconnectedCallback() {
this.model?.dispose();
this.model = undefined;
}
constructor() {
super();
}
}
window.customElements.define('mc-snapshot-markdown', MolComponentSnapshotMarkdownViewer);

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { MolViewSpec } from '../../../extensions/mvs/behavior';
import { loadMVS } from '../../../extensions/mvs/load';
import { MVSData } from '../../../extensions/mvs/mvs-data';
import { PluginComponent } from '../../../mol-plugin-state/component';
import { createPluginUI } from '../../../mol-plugin-ui';
import { renderReact18 } from '../../../mol-plugin-ui/react18';
import { DefaultPluginUISpec } from '../../../mol-plugin-ui/spec';
import { PluginConfig } from '../../../mol-plugin/config';
import { PluginContext } from '../../../mol-plugin/context';
import { PluginSpec } from '../../../mol-plugin/spec';
import { getMolComponentContext, MolComponentContext } from '../context';
export class MolComponentViewerModel extends PluginComponent {
readonly context: MolComponentContext;
plugin?: PluginContext = undefined;
async mount(root: HTMLElement) {
const spec = DefaultPluginUISpec();
this.plugin = await createPluginUI({
target: root,
render: renderReact18,
spec: {
...spec,
layout: {
initial: {
isExpanded: false,
showControls: false,
controlsDisplay: 'landscape',
},
},
components: {
remoteState: 'none',
viewport: {
snapshotDescription: EmptyDescription,
}
},
behaviors: [
...spec.behaviors,
PluginSpec.Behavior(MolViewSpec)
],
config: [
[PluginConfig.Viewport.ShowAnimation, false],
]
}
});
this.subscribe(this.context.commands, async (cmd) => {
if (!cmd) return;
if (cmd.kind === 'load-mvs') {
if (cmd.url) {
const data = await this.plugin!.runTask(this.plugin!.fetch({ url: cmd.url, type: 'string' }));
const mvsData = MVSData.fromMVSJ(data);
await loadMVS(this.plugin!, mvsData, { sanityChecks: true, sourceUrl: cmd.url, replaceExisting: true });
} else if (cmd.data) {
await loadMVS(this.plugin!, cmd.data, { sanityChecks: true, replaceExisting: true });
}
}
});
const viewers = this.context.behavior.viewers.value;
const next = [...viewers, { name: this.options?.name, model: this }];
this.context.behavior.viewers.next(next);
}
constructor(private options?: { context?: { name?: string, container?: object }, name?: string }) {
super();
this.context = getMolComponentContext(options?.context);
const viewers = this.context.behavior.viewers.value;
const index = viewers.findIndex(v => v.name === options?.name);
if (index >= 0) {
const next = [...viewers];
next[index].model.dispose();
next.splice(index, 0);
this.context.behavior.viewers.next(next);
}
}
}
function EmptyDescription() {
return <></>;
}
export class MolComponentViewer extends HTMLElement {
private model: MolComponentViewerModel | undefined = undefined;
async connectedCallback() {
this.model = new MolComponentViewerModel({
name: this.getAttribute('name') ?? undefined,
context: { name: this.getAttribute('context-name') ?? undefined },
});
await this.model.mount(this);
}
disconnectedCallback() {
this.model?.dispose();
this.model = undefined;
}
constructor() {
super();
}
}
window.customElements.define('mc-viewer', MolComponentViewer);

View File

@@ -0,0 +1,116 @@
<!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>Molecular Stories</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#viewer {
position: absolute;
left: 0;
top: 0;
right: 34%;
bottom: 0;
}
#controls {
position: absolute;
left: 66%;
top: 0;
right: 0;
bottom: 0;
padding: 16px;
padding-bottom: 20px;
border: 1px solid #ccc;
border-left: none;
background: #F6F5F3;
z-index: -2;
display: flex;
flex-direction: column;
gap: 16px;
}
#links {
position: absolute;
bottom: 4px;
right: 8px;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 0.6rem;
z-index: -1;
color: #666;
}
#links a {
color: #666;
text-decoration: none;
}
@media (orientation:portrait) {
#viewer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 40%;
}
#controls {
position: absolute;
left: 0;
top: 60%;
right: 0;
bottom: 0;
border-top: none;
}
.markdown-explanation {
font-size: 0.9rem !important;
}
.markdown-explanation h3 {
font-size: 1.5rem !important;
}
.msp-viewport-controls-buttons {
display: none;
}
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
<script type="text/javascript" src="./index.js"></script>
</head>
<body>
<div id="viewer">
<mc-viewer name="v1" />
</div>
<div id="controls">
<div id="select-story" class="select-story"></div>
<div class="markdown-explanation" style="flex-grow: 1;">
<mc-snapshot-markdown viewer-name="v1" />
</div>
</div>
<div id="links">
<a href="#" id="mvs-data" filename="kinase-story.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>
<script>
document.getElementById('mvs-data').addEventListener('click', (e) => {
e.preventDefault();
window.downloadStory();
});
setTimeout(() => {
window.initStories();
}, 0);
</script>
</body>
</html>

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { getMolComponentContext } from './context';
import './index.html';
import './elements/snapshot-markdown';
import './elements/viewer';
import '../../mol-plugin-ui/skin/light.scss';
import './styles.scss';
import { download } from '../../mol-util/download';
import { BehaviorSubject } from 'rxjs';
import { Stories } from './stories';
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
import { createRoot } from 'react-dom/client';
export class MolComponents {
getContext(name?: string) {
return getMolComponentContext({ name });
}
}
const MC = new MolComponents();
type Story = { kind: 'built-in', id: string } | { kind: 'url', url: string, format: 'mvsx' | 'mvsj' } | undefined;
const CurrentStory = new BehaviorSubject<Story>(undefined);
function SelectStoryUI({ subject }: { subject: BehaviorSubject<Story> }) {
const current = useBehavior(subject);
const selectedId = current?.kind === 'built-in' ? current.id : current?.kind === 'url' ? 'url' : '';
return <select onChange={e => {
const value = e.currentTarget.value;
const s = Stories.find(s => s.id === value);
if (!s) return;
subject.next({ kind: 'built-in', id: s.id });
}} value={selectedId}>
{!current && <option value=''>Select a story...</option>}
{Stories.map(s => <option key={s.name} value={s.id}>Story: {s.name}</option>)}
{current?.kind === 'url' && <option disabled>------------------</option>}
{current?.kind === 'url' && <option value='url'>{current.url}</option>}
</select>;
}
function init() {
CurrentStory.subscribe(story => {
if (!story) {
history.replaceState({}, '', '');
} else if (story.kind === 'url') {
history.replaceState({}, '', story ? `?story-url=${encodeURIComponent(story.url)}&data-format=${story.format}` : '');
MC.getContext().dispatch({
kind: 'load-mvs',
format: story.format,
url: story.url,
});
} else if (story.kind === 'built-in') {
history.replaceState({}, '', story ? `?story=${story.id}` : '');
const s = Stories.find(s => s.id === story.id);
if (s) {
MC.getContext().dispatch({
kind: 'load-mvs',
data: s.buildStory(),
});
} else {
console.warn('Story not found:', story.id);
CurrentStory.next({ kind: 'built-in', id: Stories[0].id });
}
}
});
const urlParams = new URLSearchParams(window.location.search);
const storyUrl = urlParams.get('story-url');
const dataFormat = urlParams.get('data-format') as 'mvsx' | 'mvsj' | null;
const storyId = urlParams.get('story');
if (storyUrl) {
CurrentStory.next({ kind: 'url', url: storyUrl, format: dataFormat ?? 'mvsj' });
} else if (storyId) {
CurrentStory.next({ kind: 'built-in', id: storyId });
} else {
CurrentStory.next({ kind: 'built-in', id: Stories[0].id });
}
createRoot(document.getElementById('select-story')!).render(<SelectStoryUI subject={CurrentStory} />);
}
(window as any).mc = MC;
(window as any).downloadStory = () => {
if (CurrentStory.value?.kind !== 'built-in') return;
const id = CurrentStory.value.id;
const story = Stories.find(s => s.id === id);
if (!story) return;
const data = JSON.stringify(story.buildStory(), null, 2);
download(new Blob([data], { type: 'application/json' }), 'story.mvsj');
};
(window as any).initStories = init;
(window as any).CurrentStory = CurrentStory;

View File

@@ -0,0 +1,57 @@
# MolViewSpec Stories Example
This example illustrates:
- Using MolViewSpec to tell a story
- A proof of concept for separating Mol* into a ready-to-use web component library.
- Ability to load MVS states
### Usage
- Clone Mol* GitHub repo and build it.
```bash
git clone https://github.com/molstar/molstar.git
cd molstar
npm install
npm build
```
- Get `molstar.css` and `index.js` from `build/examples/mvs-stories` and include these to your HTML page
```html
<link rel="stylesheet" type="text/css" href="molstar.css" />
<script type="text/javascript" src="index.js"></script>
```
- Plate the components in your page wrapper in `<div>` elements to set up positioning:
```html
<div class="viewer">
<mc-viewer name="v1" />
</div>
<div class="snapshot">
<mc-snapshot-markdown viewer-name="v1" />
</div>
```
- Load MolViewSpec state:
```html
<script>
window.mc.getContext().dispatch({
kind: 'load-mvs',
format: 'mvsj',
url: 'https://path/to/file.mvsj',
// or provide data directly
// data: mvsJSON
});
</script>
```
See [index.html](./index.html) for a full example.
- For interactive development build (for production use `npm run build`) of the example that immediately reflects changes use:
```bash
npm run dev -- -e mvs-stories
```

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { buildStory as kinase } from './kinase';
import { buildStory as tbp } from './tbp';
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 },
] as const;

View File

@@ -0,0 +1,756 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
import { createMVSBuilder, Structure as MVSStructure, Representation, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
import { ColorT, ComponentExpressionT, isPrimitiveComponentExpressions, PrimitivePositionT } from '../../../extensions/mvs/tree/mvs/param-types';
import { Mat3, Mat4, Vec3 } from '../../../mol-math/linear-algebra';
const Domains = {
ChainA: { auth_asym_id: 'A' },
SH2: { auth_asym_id: 'A', beg_auth_seq_id: 146, end_auth_seq_id: 247 },
SH3: { auth_asym_id: 'A', beg_auth_seq_id: 83, end_auth_seq_id: 145 },
P_loop: { auth_asym_id: 'A', beg_auth_seq_id: 246, end_auth_seq_id: 255 },
Activation_loop: { auth_asym_id: 'A', beg_auth_seq_id: 384, end_auth_seq_id: 402 },
};
const DomainColors = {
SH2: '#8ED1A4' as ColorT,
SH2_BCR: '#D03B4B' as ColorT,
SH3: '#64B9AA' as ColorT,
P_loop: 'pink' as ColorT,
Activation_loop: 'red' as ColorT,
DFG_motif: 'orange' as ColorT,
};
const Colors = {
'1opl': '#4577B2' as ColorT,
'2gqg': '#BC536D' as ColorT,
'2g2i': '#BC536D' as ColorT,
'1iep': '#B9E3A0' as ColorT,
'3ik3': '#F3774B' as ColorT,
'3oxz': '#7D7EA5' as ColorT,
'active-site': '#F3794C' as ColorT,
'binding-site': '#FEEB9F' as ColorT,
};
// Obtained using https://www.rcsb.org/alignment
// Aligned to 1iep
const Superpositions = {
'1opl': [-0.6321036327, 0.3450463255, 0.6938213248, 0, -0.6288677634, -0.7515716885, -0.1991615756, 0, 0.4527364948, -0.5622126202, 0.6920597055, 0, 36.3924122492, 118.2516908402, -26.4992054179, 1] as unknown as Mat4,
'3ik3': [-0.7767826245, -0.6295936551, 0.0148520572, 0, 0.6059737752, -0.7408035481, 0.2898376906, 0, -0.1714775143, 0.2341408391, 0.9569605684, 0, 21.0648276775, 53.0266628762, -0.3385906075, 1] as unknown as Mat4,
'2gqg': [0.0648740828, -0.7163272638, 0.6947421137, 0, 0.0160329972, -0.6953706204, -0.7184724374, 0, 0.9977646498, 0.0577490387, -0.0336266582, 0, -31.0690973964, 146.0940883054, 39.7107422531, 1] as unknown as Mat4,
'2g2i': [-0.5680242227, 0.6527660987, 0.5012433569, 0, -0.10067389, 0.5493518768, -0.8295042395, 0, -0.8168312251, -0.5216406194, -0.2463286704, 0, -8.1905690894, 75.7603329146, -6.1327389269, 1] as unknown as Mat4,
'3oxz': [0.7989033646, 0.5984398921, -0.0601922711, 0, -0.1303123126, 0.269921501, 0.9540236289, 0, 0.5871729857, -0.754328893, 0.2936252816, 0, -8.0697093741, 58.1709160658, 19.0363028443, 1] as unknown as Mat4,
};
const Steps = [
{
header: 'A Kinase Out of Control',
key: 'intro',
description: `
### The Structural Story of BCR-ABL: A Kinase Out of Control
BCR-ABL is a classic case of how structural biology can drive drug discovery. This story will help you understand:
- How the [ABL kinase is normally regulated](#regulated-kinase).
- How a small genetic fusion creates a [rogue kinase](#rogue-kinase).
- How ATP binding fuels [uncontrolled cancer growth](#unstoppable-signaling).
- How [Imatinib revolutionized treatment](#imatinib) by locking the kinase in an inactive state.
- How a [single mutation (T315I) enabled resistance](#mutation) and brought new challenges.
- How [Ponatinib](#ponatinib) and future inhibitors are being designed to keep up in this ongoing battle.
`,
state: (): Root => {
const builder = createMVSBuilder();
const _1opl = structure(builder, '1opl');
const [_1opl_poly,] = polymer(_1opl, { color: Colors['1opl'] });
_1opl_poly.label({ text: 'ABL Kinase' });
ligand(_1opl, {
selector: { label_asym_id: 'C' },
uniform_color: Colors['1opl'],
});
ligand(_1opl, {
selector: { label_asym_id: 'D' },
surface: true,
carbon_color: Colors['1opl'],
});
return builder;
},
camera: {
position: [103.72, 69.35, 20.52],
target: [0.36, 55.32, 21.8],
up: [-0.01, 0.01, -1],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'The ABL Kinase: A Well-Regulated Enzyme',
key: 'regulated-kinase',
description: `
### The ABL Kinase: A Well-Regulated Enzyme
Normally, the ABL kinase ([PDB ID 1OPL](${wwPDBLink('1opl')})) is a well-regulated enzyme, kept in check by its SH3 and SH2 domains which fold back onto the kinase domain like a safety lock.
`,
state: () => {
const builder = createMVSBuilder();
const _1opl = structure(builder, '1opl');
const [_1opl_poly, _1opl_poly_repr] = polymer(_1opl, { color: Colors['1opl'] });
ligand(_1opl, {
selector: { label_asym_id: 'C' },
uniform_color: Colors['1opl'],
});
ligand(_1opl, {
selector: { label_asym_id: 'D' },
surface: true,
carbon_color: Colors['1opl'],
});
domains(_1opl, _1opl_poly_repr, [
[Domains.SH2, DomainColors.SH2, 'SH2'],
[Domains.SH3, DomainColors.SH3, 'SH3'],
], { label_size: 9 });
return builder;
},
camera: {
position: [-18.33, -30.35, 48.2],
target: [-10.37, 49.7, 12.68],
up: [-0.27, -0.37, -0.89],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'The Birth of a Rogue Kinase',
key: 'rogue-kinase',
transition_duration_ms: 750,
description: `
### The Birth of a Rogue Kinase
But in BCR-ABL, this safety mechanism is gone. A reciprocal translocation between chromosomes 9 and 22 creates the Philadelphia chromosome (Ph),
fusing the ABL1 gene from chromosome 9 with the BCR gene on chromosome 22. This fusion produces the chimeric protein, BCR-ABL, which lacks the
regulation of the wildtype protein. Read more about this [here](https://www.cancer.gov/publications/dictionaries/cancer-terms/def/philadelphia-chromosome)
and [the history of its discovery](https://pmc.ncbi.nlm.nih.gov/articles/PMC1934591/).
Comparing the normal protein to the kinase domain alone ([PDB ID 2GQG](${wwPDBLink('2gqg')}), in light red), you can
see how the SH3 and SH2 domains (teal in normal ABL, red in BCR-ABL, with SH3 domain being unresolved in the crystal structure) are no longer positioned to restrain the kinase.
With this lock removed, BCR-ABL is stuck in an active conformation, like an accelerator pedal jammed to the floor. Without
its normal regulation, BCR-ABL will keep signaling, unchecked causing unregulated cell growth and cancer — [chronic myeloid leukemia (CML)](https://en.wikipedia.org/wiki/Chronic_myelogenous_leukemia).
`,
state: () => {
const builder = createMVSBuilder();
const _1opl = structure(builder, '1opl');
const [_1opl_poly, _1opl_poly_repr] = polymer(_1opl, { color: Colors['1opl'] });
ligand(_1opl, {
selector: { label_asym_id: 'C' },
uniform_color: Colors['1opl'],
});
ligand(_1opl, {
selector: { label_asym_id: 'D' },
surface: true,
carbon_color: Colors['1opl'],
});
domains(_1opl, _1opl_poly_repr, [
[Domains.SH2, DomainColors.SH2, 'SH2'],
[Domains.SH3, DomainColors.SH3, 'SH3'],
], { label_size: 9 });
const _2gqg = structure(builder, '2gqg');
const [, _2gqg_poly_repr] = polymer(_2gqg, { color: '#BF99A1' });
domains(_2gqg, _2gqg_poly_repr, [
[Domains.SH2, DomainColors['SH2_BCR'], 'SH2 (BCR)'],
], { label_size: 6 });
return builder;
},
camera: {
position: [30.7, -18.5, 13.47],
target: [3.99, 47.45, 0.08],
up: [-0.22, -0.28, -0.94],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'ATP Binding and Unstoppable Signaling [1/2]',
key: 'unstoppable-signaling',
description: `
### ATP Binding and Unstoppable Signaling
To function, every kinase needs [ATP](https://en.wikipedia.org/wiki/Kinase), and BCR-ABL is no exception. ATP donates a phosphate group that is transferred to a substrate
during phosphorylation — a key step in signaling pathways that control cell growth. However, ATP is chemically unstable under the conditions
used for crystallography. It often breaks down into ADP (adenosine diphosphate), losing one of its three phosphate
groups — the very group that would normally be transferred during catalysis.
Because of this instability, we don't have crystal structures of BCR-ABL bound to ATP itself. Instead, researchers have studied
the H396P mutant, which locks the kinase in a permanently active conformation, to understand how it binds nucleotides.
In this structure, [ADP](https://www.ebi.ac.uk/pdbe-srv/pdbechem/chemicalCompound/show/ADP) is clearly nestled in the kinase's active site.
Key catalytic residues — Lys271, Glu286, and Asp381 (in orange) — form a highly conserved network that helps position and stabilize the nucleotide.
Glu286, in particular, forms a salt bridge with Lys271, anchoring the active site in a catalytically competent conformation. This arrangement
supports efficient phosphate transfer, which is central to BCR-ABL's ability to activate downstream signaling pathways.
`,
state: () => {
const builder = createMVSBuilder();
const _2g2i = structure(builder, '2g2i');
const [, _2g2i_poly_repr] = polymer(_2g2i, { color: Colors['2g2i'] });
ligand(_2g2i, {
selector: { label_asym_id: 'E' },
surface: true,
label: 'ADP',
label_size: 2,
label_color: Colors['2g2i'],
});
domains(_2g2i, _2g2i_poly_repr, [
[Domains.SH2, DomainColors['SH2_BCR'], 'SH2'],
[Domains.P_loop, DomainColors['P_loop'], 'P Loop'],
[Domains.Activation_loop, DomainColors['Activation_loop'], 'Activation Loop (active)', { label_size: 3 }],
], { label_size: 3 });
drawInteractions(_2g2i, [
['Salt Bridge', { auth_asym_id: 'A', auth_seq_id: 271, auth_atom_id: 'NZ' }, { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE1' }, { skipResidue: true }],
]);
bindingSite(_2g2i, [
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
], { color: Colors['active-site'] });
return builder;
},
camera: {
position: [49.01, 78.47, 38.92],
target: [15.59, 54.81, 12.37],
up: [0.61, 0.03, -0.79],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'ATP Binding and Unstoppable Signaling [2/2]',
description: `
### ATP Binding and Unstoppable Signaling
Note the location of the activation loop (in red) which sits in its active conformation.
In normal cells, kinases like ABL are tightly regulated — ATP binding and activation only occur when needed. But in BCR-ABL, this regulation is lost.
ATP binds freely, phosphorylation proceeds unchecked, and the signaling pathways that drive leukemia remain constantly switched on.
`,
state: (): Root => {
return Steps.find((s: any) => s.key === 'unstoppable-signaling')?.state()!;
},
camera: {
position: [98.66, 82.23, 14.15],
target: [12.31, 54.23, 18.79],
up: [0.06, -0.35, -0.93],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'Imatinib: The Drug That Changed Everything [1/2]',
key: 'imatinib',
description: `
### Imatinib: The Drug That Changed Everything
For years, chronic myeloid leukemia (CML) was a death sentence. Then came Imatinib (Gleevec) — a small molecule designed to fit into the ATP-binding pocket of BCR-ABL and lock the kinase in an inactive conformation. It was the first targeted cancer therapy of its kind.
Take a look at the Imatinib-bound structure ([PDB ID 1IEP](${wwPDBLink('1iep')})), and you'll notice a key difference — this time, the kinase is frozen in its inactive form. The drug (shown in colour) nestles deep in the ATP-binding site and blocks ATP from binding.
Imatinib forms specific interactions with several important residues:
- A hydrogen bond with Thr315, the gatekeeper residue, which plays a major role in drug sensitivity and resistance.
- Asp381, part of the DFG motif, which helps coordinate catalytic magnesium ions and position the phosphate for transfer.
- Glu286, located in the αC-helix, normally forms a salt bridge with Lys271 in the active conformation — but here, it's flipped away.
- Ile360 and His361, part of the activation loop, help stabilize the inactive conformation that Imatinib prefers.
Together, these interactions stabilize the inactive kinase, shutting down its activity and halting the signaling cascade that drives leukemia.
`,
state: () => {
const builder = createMVSBuilder();
const _1iep = structure(builder, '1iep');
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
ligand(_1iep, {
selector: { label_asym_id: 'G' },
surface: true,
label: 'Imatinib',
label_size: 2,
label_color: Colors['1iep'],
});
drawInteractions(_1iep, [
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'G', label_atom_id: 'N21' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 315, auth_atom_id: 'OG1' }, { label_asym_id: 'G', label_atom_id: 'N13' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'O29' }, { skipResidue: true }],
]);
ligand(_1iep, {
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
label: 'Thr315',
label_size: 2,
carbon_color: 'red',
label_color: 'red',
});
bindingSite(_1iep, [
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
], { color: Colors['active-site'] });
bindingSite(_1iep, [
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
], { color: Colors['binding-site'] });
return builder;
},
camera: {
position: [40.32, 68.65, 13.5],
target: [16, 53.82, 14.88],
up: [0.26, -0.5, -0.83],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'Imatinib: The Drug That Changed Everything [2/2]',
description: `
### Imatinib: The Drug That Changed Everything
Notice how the P-loop, which normally cradles ATP in the active state, which was not visible in the active state, has shifted into a stabilised closed and collapsed conformation.
At the same time, the activation loop (in green), which needs to be extended and open for the kinase to catalyse phosphorylation, is now flipped into a closed, inactive position.
Imatinib doesn't just block ATP from binding — it locks BCR-ABL into an inactive conformation, one where the active site is misaligned and the kinase simply can't function.
By switching between the ADP-bound active structure and the Imatinib-bound inactive structure, you can clearly see the conformational changes. The shift is dramatic and decisive: the enzyme goes from a catalytically ready state to one that is completely switched off.
The change is decisive: BCR-ABL is finally silenced.
`,
state: () => {
const builder = createMVSBuilder();
const _1iep = structure(builder, '1iep');
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
ligand(_1iep, {
selector: { label_asym_id: 'G' },
surface: true,
label: 'Imatinib',
label_size: 2,
label_color: Colors['1iep'],
});
domains(_1iep, _1iep_poly_repr, [
[Domains.P_loop, DomainColors['P_loop'], 'P Loop'],
[Domains.Activation_loop, DomainColors['Activation_loop'], 'Activation Loop (inactive)', { label_size: 3 }],
], { label_size: 3 });
drawInteractions(_1iep, [
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'G', label_atom_id: 'N21' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 315, auth_atom_id: 'OG1' }, { label_asym_id: 'G', label_atom_id: 'N13' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'O29' }, { skipResidue: true }],
]);
ligand(_1iep, {
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
label: 'Thr315',
label_size: 2,
carbon_color: 'red',
label_color: 'red',
});
bindingSite(_1iep, [
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
], { color: Colors['active-site'] });
bindingSite(_1iep, [
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
], { color: Colors['binding-site'] });
return builder;
},
camera: {
position: [91.47, 73.63, 20.78],
target: [12.53, 54.2, 19.09],
up: [0.04, -0.07, -1],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'Resistance Strikes: The T315I Mutation [1/2]',
key: 'mutation',
description: `
### Resistance Strikes: The T315I Mutation
For a while, it seemed like leukemia had been beaten. But then, in some patients, the cancer returned. The culprit?
What was once a threonine (Thr) is now an isoleucine (Ile), a single mutation [T315I](https://doi.org/10.1016/j.ccr.2011.03.003), shown on [PDB ID 3IK3](${wwPDBLink('3ik3')}) in orange.
Forming a hydrogen bond with Imatinib, Thr315 was a crucial contact point. With bulkier and non-polar isoleucine in its place, the contact is lost and the drug won't bind.
`,
state: () => {
const builder = createMVSBuilder();
const _1iep = structure(builder, '1iep');
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
ligand(_1iep, {
selector: { label_asym_id: 'G' },
surface: true,
label: 'Imatinib',
label_size: 2,
label_color: Colors['1iep'],
});
ligand(_1iep, {
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
carbon_color: Colors['1iep'],
opacity: 0.51,
});
const _3ik3 = structure(builder, '3ik3');
const [, _3ik3_poly_repr] = polymer(_3ik3, { color: Colors['3ik3'] });
ligand(_3ik3, {
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
label: 'T315I',
label_size: 2,
carbon_color: 'red',
label_color: 'red',
});
return builder;
},
camera: {
position: [13.69, 72.8, 4.44],
target: [13.02, 54.12, 9.71],
up: [0.39, -0.26, -0.88],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'Resistance Strikes: The T315I Mutation [2/2]',
description: `
### Resistance Strikes: The T315I Mutation
This mutation prevents Imatinib binding but still allows ATP (here represented by the ADP) to nestle into the active site.
The result? Resistance. BCR-ABL is active again, and the leukemia returns, this time untouchable by Imatinib.
`,
state: () => {
const builder = createMVSBuilder();
const _2g2i = structure(builder, '2g2i');
const [, _2g2i_poly_repr] = polymer(_2g2i, { color: Colors['2g2i'] });
ligand(_2g2i, {
selector: { label_asym_id: 'E' },
surface: true,
label: 'ADP',
label_size: 2,
label_color: Colors['2g2i'],
});
drawInteractions(_2g2i, [
['Salt Bridge', { auth_asym_id: 'A', auth_seq_id: 271, auth_atom_id: 'NZ' }, { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE1' }, { skipResidue: true }],
]);
bindingSite(_2g2i, [
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
], { color: Colors['active-site'] });
const _3ik3 = structure(builder, '3ik3');
const [, _3ik3_poly_repr] = polymer(_3ik3, { color: Colors['3ik3'] });
ligand(_3ik3, {
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
label: 'T315I',
label_size: 2,
carbon_color: 'red',
label_color: 'red',
});
return builder;
},
camera: {
position: [-3.29, 89.29, 2.7],
target: [16.64, 55.48, 15.94],
up: [0.24, -0.23, -0.94],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'Fighting Back: Ponatinib and the Future of Kinase Inhibitors',
key: 'ponatinib',
description: `
### Fighting Back: Ponatinib and the Future of Kinase Inhibitors
The battle didn't end there. Scientists knew they needed a new inhibitor—one that could work even against T315I. Enter Ponatinib (shown in [PDB ID 3OXZ](${wwPDBLink('3oxz')})), a next-generation
drug designed to bypass this resistance. Viewing the Ponatinib-bound structure, you'll see how it differs from Imatinib. Instead of being blocked by T315I,
Ponatinib has a flexible triple-bond linker, allowing it to slip into the binding site without clashing with the mutation.
Look closely at the interactions—Ponatinib forms new hydrophobic contacts that compensate for the loss of the Thr315 interaction. This structure tells a story of rational drug design: scientists
used everything they learned about BCR-ABL's structure to engineer a molecule that could fit where others failed.
But the story isn't over. New mutations continue to arise, and leukemia is still finding ways to outmaneuver our drugs. The future may lie in allosteric
inhibitors that bind outside the ATP pocket, or even in protein degradation strategies that eliminate BCR-ABL entirely. Whatever the next breakthrough is,
it will start here—with a deep understanding of structure and function, and the power of visualization to reveal the molecular battles happening
inside every cancer cell.
`,
state: () => {
const builder = createMVSBuilder();
const _3oxz = structure(builder, '3oxz');
const [, _3oxz_poly_repr] = polymer(_3oxz, { color: Colors['3oxz'] });
ligand(_3oxz, {
selector: { label_asym_id: 'B' },
surface: true,
label: 'Ponatinib',
label_size: 2,
label_color: Colors['3oxz'],
});
ligand(_3oxz, {
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
label: 'T315I',
label_size: 2,
carbon_color: 'red',
label_color: 'red',
});
drawInteractions(_3oxz, [
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'B', label_atom_id: 'N4' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'B', label_atom_id: 'N4' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'B', label_atom_id: 'N2' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'B', label_atom_id: 'O1' }, { skipResidue: true }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'B', label_atom_id: 'N1' }, { skipResidue: true }],
]);
bindingSite(_3oxz, [
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
], { color: Colors['active-site'] });
return builder;
},
camera: {
position: [61.15, 66.58, 19.72],
target: [9.61, 50.49, 14.08],
up: [0.15, -0.15, -0.98],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'The End',
key: 'end',
description: `
### The End
That's all folks! We hope you enjoyed this interactive journey through the structural biology of BCR-ABL.
The next time you look at a macromolecular structure, remember: each atom tells a story, and each discovery shapes the future of medicine.
Read more [here](https://pmc.ncbi.nlm.nih.gov/articles/PMC3513788/).
`,
state: (): Root => {
return Steps[0].state();
},
camera: {
position: [103.72, 69.35, 20.52],
target: [0.36, 55.32, 21.8],
up: [-0.01, 0.01, -1],
} satisfies MVSNodeParams<'camera'>,
}
];
type Interaction = [label: string, polymer: PrimitivePositionT, ligand: PrimitivePositionT, options?: { skipResidue?: boolean }]
function drawInteractions(structure: MVSStructure, interactions: Interaction[]) {
const primitives = structure.primitives();
const interactingResidues: ComponentExpressionT[] = [];
const addedResidues = new Set<string>();
for (const [tooltip, a, b, options] of interactions) {
primitives.tube({ start: a, end: b, color: '#4289B5', tooltip, radius: 0.1, dash_length: 0.1 });
if (options?.skipResidue) continue;
const expressions = isPrimitiveComponentExpressions(a) ? a.expressions! : [a as ComponentExpressionT];
for (const _e of expressions) {
const e = { ..._e };
delete e.auth_atom_id;
delete e.label_atom_id;
const key = JSON.stringify(e);
if (addedResidues.has(key)) continue;
interactingResidues.push(e);
addedResidues.add(key);
}
}
if (interactingResidues.length === 0) return;
structure
.component({ selector: interactingResidues })
.representation({ type: 'ball_and_stick' })
.color({
custom: {
molstar_color_theme_name: 'element-symbol',
molstar_color_theme_params: { carbonColor: { name: 'element-symbol', params: {} } },
}
});
}
function transform(structure: MVSStructure, id: keyof typeof Superpositions) {
const rotation = Mat3.fromMat4(Mat3.zero(), Superpositions[id]);
const translation = Mat4.getTranslation(Vec3.zero(), Superpositions[id]) as any;
return structure.transform({ rotation, translation });
}
function wwPDBLink(id: string) {
return `https://doi.org/10.2210/pdb${id.toLowerCase()}/pdb`;
}
function structure(builder: Root, id: string): MVSStructure {
let ret = builder
.download({ url: pdbUrl(id) })
.parse({ format: 'bcif' })
.modelStructure();
if (id in Superpositions) {
ret = transform(ret, id as any);
}
return ret;
}
function domains(structure: MVSStructure, reprensentation: Representation, domains: [selector: ComponentExpressionT, color: ColorT, label?: string, options?: { label_size?: number }][], options?: { label_size?: number }) {
const hasLabels = domains.some(d => !!d[2]);
const primitives = hasLabels ? structure.primitives() : undefined;
for (const [selector, color, label, opts] of domains) {
reprensentation.color({ selector, color });
if (label) primitives!.label({ position: selector, text: label, label_color: color, label_size: opts?.label_size ?? options?.label_size ?? 1.5 });
}
}
function polymer(structure: MVSStructure, options: { color: ColorT }) {
const component = structure.component({ selector: { label_asym_id: 'A' } });
const reprensentation = component.representation({ type: 'cartoon' });
reprensentation.color({ color: options.color });
return [component, reprensentation] as const;
}
function ligand(structure: MVSStructure, options: {
selector: ComponentExpressionT | ComponentExpressionT[],
label?: string,
surface?: boolean,
carbon_color?: ColorT,
uniform_color?: ColorT,
label_color?: ColorT,
label_size?: number,
opacity?: number,
}) {
const comp = structure.component({ selector: options.selector });
const coloring = options.uniform_color
? { color: options.uniform_color }
: {
custom: {
molstar_color_theme_name: 'element-symbol',
molstar_color_theme_params: { carbonColor: options?.carbon_color ? { name: 'uniform', params: { value: decodeColor(options?.carbon_color) } } : { name: 'element-symbol', params: {} } }
}
};
if (options.surface) comp.representation({ type: 'surface' }).color(coloring).opacity({ opacity: 0.33 });
const repr = comp.representation({ type: 'ball_and_stick' }).color(coloring);
if (options.opacity) repr.opacity({ opacity: options.opacity });
const label_color: ColorT = options?.label_color ?? options.uniform_color ?? options.carbon_color ?? '#5B53A4';
if (options.label) {
structure.primitives().label({
position: Array.isArray(options.selector) ? { expressions: options.selector } : options.selector,
text: options.label,
label_color,
label_size: options?.label_size ?? 1.5
});
}
return comp;
}
function bindingSite(structure: MVSStructure, residues: [selector: ComponentExpressionT, label: string][], options: {
color?: ColorT,
label_size?: number,
}) {
const color: ColorT = options.color ?? '#5B53A4';
const coloring = {
custom: {
molstar_color_theme_name: 'element-symbol',
molstar_color_theme_params: { carbonColor: { name: 'uniform', params: { value: decodeColor(color) } } }
}
};
structure.component({ selector: residues.map(r => r[0]) }).representation({ type: 'ball_and_stick' }).color(coloring);
const primitives = structure.primitives();
for (const [selector, label] of residues) {
primitives.label({
position: selector,
text: label,
label_color: color,
label_size: options?.label_size ?? 1.5
});
}
}
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);
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: 5000,
transition_duration_ms: s.transition_duration_ms ?? 1500,
});
});
return {
kind: 'multiple',
snapshots,
metadata: {
title: 'The Structural Story of BCR-ABL: A Kinase Out of Control',
version: '1.0',
timestamp: new Date().toISOString(),
}
};
}

View File

@@ -0,0 +1,541 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
*/
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,
ComponentExpressionT,
isPrimitiveComponentExpressions,
PrimitivePositionT
} from '../../../extensions/mvs/tree/mvs/param-types';
import { Mat3, Mat4, Vec3 } from '../../../mol-math/linear-algebra';
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
const Colors = {
'1vok': '#4577B2' as ColorT,
'1cdw': '#BC536D' as ColorT,
'1cdw-2': '#c5a3af' as ColorT,
'1vtl': '#B9E3A0' as ColorT,
'7enc': '#0072B2' as ColorT,
'7enc-2': '#D55E00' as ColorT,
'7enc-3': '#009E73' as ColorT,
'7enc-4': '#56B4E9' as ColorT,
};
// Obtained using https://www.rcsb.org/alignment
const Superpositions = {
'1cdw': [-0.4665815186, 0.6063873444, -0.6438913535, 0, -0.581544075, -0.7588303199, -0.2932286385, 0, -0.6664144171, 0.2376361381, 0.7066971703, 0, 135.0863694935, 105.5007997009, 153.6890178993, 1] as unknown as Mat4,
'1vtl': [-0.4769460004, 0.7214347188, -0.5020502557, 0, -0.297882932, 0.4047204695, 0.8645617968, 0, 0.8269149119, 0.5619014932, 0.0218732801, 0, 65.6043682658, -3.7328402905, -16.8650755387, 1] as unknown as Mat4,
'7enc': [0.8975055044, -0.4316347566, -0.0904174009, 0, 0.247274877, 0.3227849997, 0.9136000105, 0, -0.3651561375, -0.8423189899, 0.3964337454, 0, -189.7572972798, 304.0841220076, -411.5005782853, 1] as unknown as Mat4,
};
const Steps = [
{
header: 'TATA-Binding Protein',
key: 'intro',
description: `
### TATA-Binding Protein Tells RNA Polymerase Where To Get Started on a Gene
Specialized DNA sequences next to genes, called promoters, define the proper start site and direction for transcription.
Promoters vary in sequence and location from organism to organism.
In eukaryotic cells, a complex promoter system that uses dozens of different proteins ensures that the proper RNA polymerase is targeted to each gene.
The TATA-binding protein (TBP) is the central element of this system, participating in transcription by all three RNA polymerases.
The crystal structure of TBP from *Arabidopsis thaliana* in its apo form is shown ([PDB ID 1VOK](${wwPDBLink('1vok')})).
The structure reveals a highly symmetric DNA-binding fold composed of two topologically identical domains derived from the two direct repeats in the phylogenetically conserved sequence.
The domains are related by an approximate 2-fold axis, and consist of a five-stranded, antiparallel beta sheet and two alpha helices.
It is thought that an ancient gene duplication created this protein by combining two copies of the same gene.
The intramolecular symmetry generates a saddle-shaped structure dominated by a curved antiparallel beta sheet, which forms the saddle's concave face and interacts with the DNA.
The convex face of the saddle would interact with other proteins during transcription initiation.
`,
state: (): Root => {
const builder = createMVSBuilder();
const _1vok = structure(builder, '1vok');
const _1vok_comp = _1vok.component({ selector: { label_asym_id: 'A' } });
_1vok_comp.representation({ type: 'cartoon' })
.color({
custom: {
molstar_color_theme_name: 'sequence-id',
molstar_color_theme_params: { carbonColor: { name: 'sequence-id', params: {} } },
}
});
_1vok.primitives().label({
position: { label_asym_id: 'A', label_seq_id: 88, label_atom_id: 'OD2' },
text: 'TATA-Binding Protein',
label_size: 5,
label_color: Colors['1vok']
});
return builder;
},
camera: {
position: [155.18, 118.49, 49.18],
target: [74.81, 66.8, 30.7],
up: [-0.55, 0.83, 0.1],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP: Highly Conserved in Eukaryotes [1/2]',
key: 'highly-conserved-1',
description: `
### TATA-Binding Protein Is Highly Conserved in Eukaryotes
TBP has a phylogenetically conserved, 180 amino-acid carboxy-terminal domain (ranging from 38 to 93% identity among eukaryotes and archaebacteria), containing two structural repeats flanking a highly basic segment known as the basic repeat.
The C-terminal or core portion of the protein binds the TATA consensus sequence with high affinity, interacting with the minor groove and promoting DNA bending.
Structural superposition of TBP bound to DNA in human ([PDB ID 1CDW](${wwPDBLink('1cdw')})) and *A. thaliana* ([PDB ID 1VTL](${wwPDBLink('1vtl')})) shows that their sequences are 83% identical and the RMSD between the structures is 0.43 Å.
`,
state: () => {
const builder = createMVSBuilder();
const _1cdw = structure(builder, '1cdw');
select(_1cdw, { color: Colors['1cdw'], selector: 'protein' });
select(_1cdw, { color: Colors['1cdw'], selector: 'nucleic', opacity: 0.5 })[0].label({ text: 'DNA' });
// uses a range to 'adjust' font size of label
label(_1cdw, { selector: { label_asym_id: 'C', beg_label_seq_id: 160, end_label_seq_id: 177 }, text: 'TBP (H. sapiens)' });
const _1vtl = structure(builder, '1vtl');
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'E' } });
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'A' }, opacity: 0.5 });
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'B' }, opacity: 0.5 });
// uses a range to 'adjust' font size of label
label(_1vtl, { selector: { label_asym_id: 'E', beg_label_seq_id: 75, end_label_seq_id: 92 }, text: 'TBP (A. thaliana)' });
return builder;
},
camera: {
position: [122.15, 104.37, 72.23],
target: [77.48, 59.61, 30.36],
up: [-0.73, 0.68, 0.06],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP: Minor Groove [2/2]',
key: 'highly-conserved-2',
description: `
### TATA-Binding Protein Is Highly Conserved in Eukaryotes
Eukaryotic protein-coding genes transcribed by RNA polymerase II (pol II) have a characteristic sequence of nucleotides, termed the TATA box, in front of the start site of transcription.
The typical sequence is something like T-A-T-A-a/t-A-a/t, where a/t refers to positions that can be either A or T.
TBP recognizes this TATA sequence and binds to it, creating a landmark that directs pol II to the transcription start site.
The structures of *A. thaliana* and human core TBP-TATA element co-crystal structures demonstrate a common induced-fit mechanism of protein-DNA recognition involving subtle conformation changes in the protein and an unprecedented DNA distortion.
`,
state: () => {
const builder = createMVSBuilder();
const _1cdw = structure(builder, '1cdw');
select(_1cdw, { color: Colors['1cdw'], selector: 'protein' });
select(_1cdw, { color: Colors['1cdw'], selector: 'nucleic', opacity: 0.5 });
// select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'A', beg_label_seq_id: 6, end_label_seq_id: 11 } })[0].label({ text: 'TATA box' });
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 5 }, text: 'T' });
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 6 }, text: 'A' });
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 7 }, text: 'T' });
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 8 }, text: 'A' });
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 9 }, text: 'A' });
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 10 }, text: 'A' });
const _1vtl = structure(builder, '1vtl');
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'E' } });
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'A' }, opacity: 0.5 });
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'B' }, opacity: 0.5 });
return builder;
},
camera: {
position: [108.48, 92.12, 4.1],
target: [80.16, 56.15, 28.96],
up: [-0.71, 0.68, 0.17],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP: Binding to TATA Box [1/5]',
key: 'tata-box-overview',
description: `
### TATA-Binding Protein Binds to the Minor Groove of the TATA Box
When the first structures of TBP-DNA complexes were determined, researchers discovered that TBP is not gentle when it binds to DNA.
Instead, it grabs the TATA sequence, bends and unwinds it to open up the minor groove, and kinks it sharply in two places (*e.g.*, [PDB ID 1VTL](${wwPDBLink('1vtl')})) and ([PDB ID 1CDW](${wwPDBLink('1cdw')})).
Interactions with the minor groove can be divided into different classes as seen in PDB ([PDB ID 1CDW](${wwPDBLink('1cdw')})).
The combination of interactions allows TATA-binding protein to recognize the proper DNA sequence.
`,
state: () => {
const builder = createMVSBuilder();
const _1cdw = structure(builder, '1cdw');
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
return builder;
},
camera: {
position: [122.15, 104.37, 72.23],
target: [77.48, 59.61, 30.36],
up: [-0.73, 0.68, 0.06],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP: Arginine [2/5]',
key: 'tata-box-1',
description: `
### Arginine Residues
A string of arginine amino acids interact with the phosphate groups of the DNA and glues the protein to the DNA: Arg192, Arg199, Arg204, and Arg290.
`,
state: () => {
const builder = createMVSBuilder();
const _1cdw = structure(builder, '1cdw');
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 38 }, text: 'Arg192' });
drawInteractions(_1cdw, [
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 7, label_atom_id: 'OP1' }],
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 6, label_atom_id: `O3'` }],
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH1' }, { label_asym_id: 'B', label_seq_id: 7, label_atom_id: 'OP1' }],
]);
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 45 }, text: 'Arg199' });
drawInteractions(_1cdw, [
['H-bond', { label_asym_id: 'C', label_seq_id: 45, label_atom_id: 'NE' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'OP1' }],
['H-bond', { label_asym_id: 'C', label_seq_id: 45, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'OP1' }],
]);
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 136 }, text: 'Arg290' });
drawInteractions(_1cdw, [
['H-bond', { label_asym_id: 'C', label_seq_id: 136, label_atom_id: 'NH1' }, { label_asym_id: 'A', label_seq_id: 8, label_atom_id: 'OP1' }],
]);
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 50 }, text: 'Arg204' });
drawInteractions(_1cdw, [
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'OP1' }, { label_asym_id: 'C', label_seq_id: 50, label_atom_id: 'NH1' }],
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'OP1' }, { label_asym_id: 'C', label_seq_id: 50, label_atom_id: 'NH2' }],
]);
return builder;
},
camera: {
position: [113.87, 71.89, 26.29],
target: [77.29, 61.61, 18.73],
up: [-0.28, 0.96, 0.04],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP: Phenylalanine [3/5]',
key: 'tata-box-2',
description: `
### Phenylalanine-Induced Kinks
At either end of the TATA element there are two pairs of phenylalanine side chains partially inserted between adjacent base pairs, producing the two dramatic kinks (Phe193, Phe210, Phe284, and Phe301). Between the two kinks the DNA is partially unwound.
`,
state: () => {
const builder = createMVSBuilder();
const _1cdw = structure(builder, '1cdw');
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
bindingSite(_1cdw, [
[{ label_asym_id: 'C', label_seq_id: 39 }, 'Phe193'],
[{ label_asym_id: 'C', label_seq_id: 56 }, 'Phe210'],
[{ label_asym_id: 'C', label_seq_id: 130 }, 'Phe284'],
[{ label_asym_id: 'C', label_seq_id: 147 }, 'Phe301'],
], { color: Colors['1cdw'] });
return builder;
},
camera: {
position: [111.01, 69.92, -3.14],
target: [85.52, 57.53, 19.15],
up: [-0.51, 0.85, -0.11],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP: H-Bonds in Minor Groove [4/5]',
key: 'tata-box-3',
description: `
### Hydrogen Bonds in the Minor Groove
Polar side chains make minor groove hydrogen bonds with acceptors of base pairs centred about the approximate 2-fold symmetry axis (Asn163, Asn253, and Thr309).
`,
state: () => {
const builder = createMVSBuilder();
const _1cdw = structure(builder, '1cdw');
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
// Asn: 9, 99, 155
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 9 }, text: 'Asn163' });
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 155 }, text: 'Thr309' });
drawInteractions(_1cdw, [
['H-bond', { label_asym_id: 'C', label_seq_id: 9, label_atom_id: 'ND2' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'O2' }],
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'O2' }, { label_asym_id: 'C', label_seq_id: 9, label_atom_id: 'ND2' }],
['H-bond', { label_asym_id: 'C', label_seq_id: 155, label_atom_id: 'OG1' }, { label_asym_id: 'A', label_seq_id: 8, label_atom_id: 'N3' }],
]);
bindingSite(_1cdw, [
[{ label_asym_id: 'C', label_seq_id: 99 }, 'Asn253'],
], { color: 'gray', label_color: Colors['1cdw'] });
return builder;
},
camera: {
position: [69.71, 56.65, 17.62],
target: [75.68, 63.48, 32.29],
up: [-0.9, 0.39, 0.18],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP: Non-Polar Interactions [5/5]',
key: 'tata-box-4',
description: `
### Hydrophobic and van der Waals Interactions
Several residues projecting from the concave surface of TBP make hydrophobic or van der Waals side-chain/base contacts (<4 Å between non-hydrogen atoms) with the TATA element.
`,
state: () => {
const builder = createMVSBuilder();
const _1cdw = structure(builder, '1cdw');
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
surface(_1cdw, {
selector: { label_asym_id: 'A' },
carbon_color: Colors['1cdw-2'],
});
surface(_1cdw, {
selector: { label_asym_id: 'B' },
carbon_color: Colors['1cdw-2'],
});
return builder;
},
camera: {
position: [122.15, 104.37, 72.23],
target: [77.48, 59.61, 30.36],
up: [-0.73, 0.68, 0.06],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'TBP and Transcription Pre-Initiation Complex',
key: 'pre-init-complex',
description: `
### TATA-Binding Protein and the Transcription Pre-Initiation Complex
The structure shown here, from [PDB ID 7ENC](${wwPDBLink('7enc')}), is that of a pol II “pre-initiation complex” (PIC) poised to start transcription.
This complex includes eukaryotic Mediator and pol II, along with a collection of general transcription factors that perform the necessary tasks of recognizing the transcription start site of genes, separating the two strands of the DNA double helix, and facilitating transcription initiation by RNA polymerase II.
TBP starts the process of assembling the PIC and works with TBP-associated factors (TAFs) as part of a large multi-component transcription factor, TFIID, that belongs to the collection of general transcription factors that cradle pol II with in the PIC.
After TBP binds to the promoter, it recruits additional transcription factors.
TFIIA and TFIIB interact with surrounding regions of the DNA and, along with TFIIF, assist with positioning of RNA polymerase II at the transcription start site.
TFIIE, TFIIH, and other TFIID subunits bring additional functionality to the complex.
In particular, part of TFIIH is a translocase that separates the two strands of DNA in preparation for transcription, and the CAK module of TFIIH adds phosphate groups to the long tail of RNA polymerase II, sending the signal that it is time to get started with mRNA synthesis.
TBP is a critical player that gets the transcription process started and is central to the pre-initiation complex.
Lying at the very heart of this complex macromolecular machine, it is highly conserved among eukaryotes and *archaea*.
`,
state: () => {
const builder = createMVSBuilder();
const _7enc = structure(builder, '7enc');
select(_7enc, { color: Colors['7enc'], selector: { label_asym_id: 'CB' } })[0].label({ text: 'TBP' });
select(_7enc, { color: Colors['7enc-2'], selector: { label_asym_id: 'GB' } });
select(_7enc, { color: Colors['7enc-2'], selector: { label_asym_id: 'HB' } });
select(_7enc, { color: Colors['7enc-3'], opacity: 0.5 });
_7enc.primitives()
.label({ position: { label_entity_id: '57' }, text: 'pol II', label_size: 20, label_color: Colors['7enc'] })
.label({ position: { label_entity_id: '53' }, text: 'DNA', label_size: 20, label_color: Colors['7enc-2'] })
.label({ position: { label_entity_id: '21' }, text: 'mediator', label_size: 20, label_color: Colors['7enc-3'] })
.label({ position: { label_entity_id: '42' }, text: 'TBP-associated factors (TAFs)', label_size: 20, label_color: Colors['7enc-4'] })
.label({ position: [100, 150, 0], text: 'PIC', label_size: 30, label_color: Colors['1vok'] })
;
return builder;
},
camera: {
position: [122.15, 104.37, 72.23],
target: [77.48, 59.61, 30.36],
up: [-0.73, 0.68, 0.06],
} satisfies MVSNodeParams<'camera'>,
}, {
header: 'The End',
key: 'end',
description: `
### The End
That's all folks! We hope you enjoyed this interactive journey through the structural biology of the TATA-binding protein.
The next time you look at a macromolecular structure, remember: each atom tells a story, and each discovery shapes the future of medicine.
Read more in the relevant publications on PDB IDs [1VOK](https://doi.org/10.1038/nsb0994-621), [1CDW](https://doi.org/10.1073/pnas.93.10.4862), [1VTL](https://doi.org/10.1038/365520a0), and [7ENC](https://doi.org/10.1126/science.abg0635) as well as the Molecule of the Month articles on the [TATA-binding protein](https://pdb101.rcsb.org/motm/67) and [Mediator](https://pdb101.rcsb.org/motm/289).
`,
state: (): Root => {
return Steps[Steps.length - 2].state();
},
camera: {
position: [461.95, 218.66, 140.67],
target: [87.56, -22.14, 54.59],
up: [-0.55, 0.83, 0.1],
} satisfies MVSNodeParams<'camera'>,
}
];
type Interaction = [label: string, res1: PrimitivePositionT, res2: PrimitivePositionT, options?: { skipResidue?: boolean }]
function drawInteractions(structure: MVSStructure, interactions: Interaction[]) {
const primitives = structure.primitives();
const interactingResidues: ComponentExpressionT[] = [];
const addedResidues = new Set<string>();
function drawResidue(a: PrimitivePositionT) {
const expressions = isPrimitiveComponentExpressions(a) ? a.expressions! : [a as ComponentExpressionT];
for (const _e of expressions) {
const e = { ..._e };
delete e.auth_atom_id;
delete e.label_atom_id;
const key = JSON.stringify(e);
if (addedResidues.has(key)) continue;
interactingResidues.push(e);
addedResidues.add(key);
}
}
for (const [tooltip, a, b, options] of interactions) {
primitives.tube({ start: a, end: b, color: '#4289B5', tooltip, radius: 0.1, dash_length: 0.1 });
if (options?.skipResidue) continue;
drawResidue(a);
drawResidue(b);
}
if (interactingResidues.length === 0) return;
structure
.component({ selector: interactingResidues })
.representation({ type: 'ball_and_stick' })
.color({
custom: {
molstar_color_theme_name: 'element-symbol',
molstar_color_theme_params: { carbonColor: { name: 'element-symbol', params: {} } },
}
});
}
function transform(structure: MVSStructure, id: keyof typeof Superpositions) {
const rotation = Mat3.fromMat4(Mat3.zero(), Superpositions[id]);
const translation = Mat4.getTranslation(Vec3.zero(), Superpositions[id]) as any;
return structure.transform({ rotation, translation });
}
function wwPDBLink(id: string) {
return `https://doi.org/10.2210/pdb${id.toLowerCase()}/pdb`;
}
function structure(builder: Root, id: string): MVSStructure {
let ret = builder
.download({ url: pdbUrl(id) })
.parse({ format: 'bcif' })
.modelStructure();
if (id in Superpositions) {
ret = transform(ret, id as any);
}
return ret;
}
function select(structure: MVSStructure, { color, opacity = 1.0, selector = 'polymer' }: { color: ColorT, opacity?: number, selector?: Partial<MVSNodeParams<'component'>>['selector'] }) {
const component = structure.component({ selector });
const representation = component.representation({ type: 'cartoon' });
representation.color({ color }).opacity({ opacity });
return [component, representation] as const;
}
function label(structure: MVSStructure, { selector, text }: { selector: Partial<MVSNodeParams<'component'>>['selector'], text: string }) {
structure.component({ selector })
.label({ text });
}
function surface(structure: MVSStructure, options: {
selector: ComponentExpressionT | ComponentExpressionT[],
carbon_color?: ColorT,
}) {
const comp = structure.component({ selector: options.selector });
const coloring = {
custom: {
molstar_color_theme_name: 'element-symbol',
molstar_color_theme_params: { carbonColor: options?.carbon_color ? { name: 'uniform', params: { value: decodeColor(options?.carbon_color) } } : { name: 'element-symbol', params: { } } }
}
};
comp.representation({ type: 'surface' }).color(coloring).opacity({ opacity: 0.33 });
return comp;
}
function bindingSite(structure: MVSStructure, residues: [selector: ComponentExpressionT, label: string][], options: {
color?: ColorT,
label_size?: number,
label_color?: ColorT,
}) {
const color: ColorT = options.color ?? '#5B53A4';
const coloring = {
custom: {
molstar_color_theme_name: 'element-symbol',
molstar_color_theme_params: { carbonColor: { name: 'uniform', params: { value: decodeColor(color) } } }
}
};
structure.component({ selector: residues.map(r => r[0]) }).representation({ type: 'ball_and_stick' }).color(coloring);
const primitives = structure.primitives();
for (const [selector, label] of residues) {
primitives.label({
position: selector,
text: label,
label_color: options?.label_color ?? color,
label_size: options?.label_size ?? 1.5
});
}
}
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);
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: 5000,
transition_duration_ms: 1500,
});
});
return {
kind: 'multiple',
snapshots,
metadata: {
title: 'The Structural Story of TATA-Binding Protein and its Role in Transcription Initiation',
version: '1.0',
timestamp: new Date().toISOString(),
}
};
}

View File

@@ -0,0 +1,182 @@
.select-story {
select {
width: 100%;
display: inline-block;
height: 38px;
padding: 0 8px;
color: #555;
line-height: 38px;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box;
}
}
.markdown-explanation {
// Adapted from skeleton.css, The MIT License (MIT), Copyright (c) 2011-2014 Dave Gamache
line-height: 1.4;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222;
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 0;
margin-bottom: 1rem;
font-weight: 300;
}
h1 {
font-size: 3rem;
line-height: 1.2;
letter-spacing: -.1rem;
}
h2 {
font-size: 2.6rem;
line-height: 1.25;
letter-spacing: -.1rem;
}
h3 {
font-size: 2rem;
line-height: 1.3;
letter-spacing: -.1rem;
}
h4 {
font-size: 1.7rem;
line-height: 1.35;
letter-spacing: -.08rem;
}
h5 {
font-size: 1.4rem;
line-height: 1.5;
letter-spacing: -.05rem;
}
h6 {
font-size: 1.1rem;
line-height: 1.6;
letter-spacing: 0;
}
button {
display: inline-block;
height: 38px;
padding: 0 24px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box;
}
ul {
list-style: circle inside;
}
ol {
list-style: decimal inside;
}
ol,
ul {
padding-left: 0;
margin-top: 0;
}
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%;
}
li {
margin-bottom: 0.2rem;
}
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px;
}
pre>code {
display: block;
padding: 1rem 1.5rem;
white-space: pre;
}
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1;
}
th:first-child,
td:first-child {
padding-left: 0;
}
th:last-child,
td:last-child {
padding-right: 0;
}
button,
.button {
margin-bottom: 1rem;
}
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem;
}
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 1rem;
}
hr {
margin-top: 1rem;
margin-bottom: 1rem;
border-width: 0;
border-top: 1px solid #E1E1E1;
}
}

View File

@@ -15,11 +15,11 @@ import { PluginContext } from '../../mol-plugin/context';
import { ColorNames } from '../../mol-util/color/names';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { StateTransformer } from '../../mol-state';
import { Theme } from '../../mol-theme/theme';
import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
import { AlphaOrbital, Basis, CubeGrid, CubeGridFormat, isCubeGridData } from './data-model';
import { createSphericalCollocationDensityGrid } from './density';
import { Tensor } from '../../mol-math/linear-algebra';
import { Theme } from '../../mol-theme/theme';
export class BasisAndOrbitals extends PluginStateObject.Create<{ basis: Basis, order: SphericalBasisOrder, orbitals: AlphaOrbital[] }>({ name: 'Basis', typeClass: 'Object' }) { }

View File

@@ -14,6 +14,7 @@ import { Location } from '../../mol-model/location';
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
import { getPalette, getPaletteParams } from '../../mol-util/color/palette';
import { CustomProperty } from '../../mol-model-props/common/custom-property';
import { ColorThemeCategory } from '../../mol-theme/color/categories';
const DefaultColor = Color(0xCCCCCC);
@@ -102,7 +103,7 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams, AssemblySymmetryData.Tag.Cluster> = {
name: AssemblySymmetryData.Tag.Cluster,
label: 'Assembly Symmetry Cluster',
category: ColorTheme.Category.Symmetry,
category: ColorThemeCategory.Symmetry,
factory: AssemblySymmetryClusterColorTheme,
getParams: getAssemblySymmetryClusterColorThemeParams,
defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),

View File

@@ -18,6 +18,7 @@ import { getColorMapParams } from '../../../mol-util/color/params';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TableLegend } from '../../../mol-util/legend';
import { ObjectKeys } from '../../../mol-util/type-helpers';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
const Description = 'Assigns colors to confal pyramids';
@@ -62,7 +63,7 @@ export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values
export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramidsColorThemeParams, 'confal-pyramids'> = {
name: 'confal-pyramids',
label: 'Confal Pyramids',
category: ColorTheme.Category.Residue,
category: ColorThemeCategory.Residue,
factory: ConfalPyramidsColorTheme,
getParams: getConfalPyramidsColorThemeParams,
defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),

View File

@@ -18,6 +18,7 @@ import { getColorMapParams } from '../../../mol-util/color/params';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TableLegend } from '../../../mol-util/legend';
import { ObjectKeys } from '../../../mol-util/type-helpers';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
const Description = 'Assigns colors to NtC Tube segments';
@@ -87,7 +88,7 @@ export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTub
export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = {
name: 'ntc-tube',
label: 'NtC Tube',
category: ColorTheme.Category.Residue,
category: ColorThemeCategory.Residue,
factory: NtCTubeColorTheme,
getParams: getNtCTubeColorThemeParams,
defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams),

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { InteractionsProps } from '../../mol-model-props/computed/interactions';
import { FeatureType } from '../../mol-model-props/computed/interactions/common';
import { computeInteractions, Interactions } from '../../mol-model-props/computed/interactions/interactions';
import { Structure, StructureElement, Unit } from '../../mol-model/structure';
import { RuntimeContext } from '../../mol-task';
import { AssetManager } from '../../mol-util/assets';
import { InteractionInfo, InteractionTypeToKind, StructureInteractions } from './model';
export interface ComputeInteractionsOptions {
interactions?: InteractionsProps
}
export async function computeContacts(
ctx: RuntimeContext,
selection: readonly { structureRef: string, loci: StructureElement.Loci }[],
options?: ComputeInteractionsOptions
): Promise<StructureInteractions> {
const unitIdToStructureRef = new Map<number, string>();
const unitIdToContactGroupId = new Map<number, number>();
const units: Unit[] = [];
let contactGroupId = 0;
const builder = Structure.Builder();
for (const { structureRef, loci } of selection) {
const s = StructureElement.Loci.toStructure(loci);
for (const unit of s.units) {
const newUnit = builder.copyUnit(unit, { propagateTransientCache: true });
units.push(newUnit);
unitIdToStructureRef.set(newUnit.id, structureRef);
unitIdToContactGroupId.set(newUnit.id, contactGroupId);
}
contactGroupId++;
}
const structure = builder.getStructure();
const interactions = await computeInteractions(
{ runtime: ctx, assetManager: new AssetManager() },
structure,
options?.interactions ?? {},
{
skipIntraContacts: true,
unitPairTest: (a, b) => unitIdToContactGroupId.get(a.id) !== unitIdToContactGroupId.get(b.id)
}
);
const { edges } = interactions.contacts;
const result: StructureInteractions = { kind: 'structure-interactions', elements: [] };
for (const e of edges) {
if (e.unitA > e.unitB) continue;
const [a, aType] = processFeature(structure, interactions, e.unitA, e.indexA);
const [b] = processFeature(structure, interactions, e.unitB, e.indexB);
const kind = InteractionTypeToKind[e.props.type] ?? 'unknown';
const info: InteractionInfo = { kind };
if (kind === 'hydrogen-bond' || kind === 'weak-hydrogen-bond') {
const isADonor = aType === FeatureType.HydrogenDonor || aType === FeatureType.WeakHydrogenDonor;
result.elements.push({
info,
aStructureRef: isADonor ? unitIdToStructureRef.get(e.unitA)! : unitIdToStructureRef.get(e.unitB)!,
bStructureRef: isADonor ? unitIdToStructureRef.get(e.unitB)! : unitIdToStructureRef.get(e.unitA)!,
a: isADonor ? a : b,
b: isADonor ? b : a,
});
} else {
result.elements.push({
info,
aStructureRef: unitIdToStructureRef.get(e.unitA)!,
bStructureRef: unitIdToStructureRef.get(e.unitB)!,
a,
b,
});
}
}
return result;
}
const _loc = StructureElement.Location.create();
function processFeature(structure: Structure, interactions: Interactions, unitId: number, featureIndex: number) {
_loc.structure = structure;
_loc.unit = structure.unitMap.get(unitId);
const xs = interactions.unitsFeatures.get(unitId)!;
let type: FeatureType = FeatureType.None;
const builder = structure.subsetBuilder(false);
builder.beginUnit(_loc.unit.id);
for (let o = xs.offsets[featureIndex], uIEnd = xs.offsets[featureIndex + 1]; o < uIEnd; o++) {
const unitIndex = xs.members[o];
_loc.element = _loc.unit.elements[unitIndex];
builder.addElement(_loc.element);
type = xs.types[o];
}
builder.commitUnit();
return [Structure.toStructureElementLoci(builder.getStructure()), type] as const;
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Structure, StructureElement } from '../../mol-model/structure';
import { InteractionElementSchema, InteractionInfo, StructureInteractionElement, StructureInteractions } from './model';
export function getCustomInteractionData(interactions: InteractionElementSchema[], structures: { [ref: string]: Structure }): StructureInteractions {
const elements: StructureInteractionElement[] = [];
for (const schema of interactions) {
let info: InteractionInfo;
if (schema.kind === 'covalent') {
info = { kind: schema.kind, degree: schema.degree };
} else {
info = { kind: schema.kind };
}
elements.push({
sourceSchema: schema,
info,
aStructureRef: schema.aStructureRef,
a: resolveLoci(structures[schema.aStructureRef!], schema.a),
bStructureRef: schema.bStructureRef,
b: resolveLoci(structures[schema.bStructureRef!], schema.b),
});
}
return { kind: 'structure-interactions', elements };
}
function resolveLoci(structure: Structure, schema: StructureElement.Schema) {
return StructureElement.Schema.toLoci(structure, schema);
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { InteractionType } from '../../mol-model-props/computed/interactions/common';
import { StructureElement } from '../../mol-model/structure';
interface InteractionElementSchemaBase {
aStructureRef?: string,
a: StructureElement.Schema,
bStructureRef?: string,
b: StructureElement.Schema,
description?: string,
}
export type InteractionElementSchema =
| { kind: 'unknown' } & InteractionElementSchemaBase
| { kind: 'ionic' } & InteractionElementSchemaBase
| { kind: 'pi-stacking' } & InteractionElementSchemaBase
| { kind: 'cation-pi' } & InteractionElementSchemaBase
| { kind: 'halogen-bond' } & InteractionElementSchemaBase
| { kind: 'hydrogen-bond' } & InteractionElementSchemaBase
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
| { kind: 'salt-bridge' } & InteractionElementSchemaBase
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
export type InteractionKind = InteractionElementSchema['kind']
export const InteractionKinds: InteractionKind[] = [
'unknown',
'ionic',
'pi-stacking',
'cation-pi',
'halogen-bond',
'hydrogen-bond',
'weak-hydrogen-bond',
'hydrophobic',
'metal-coordination',
'salt-bridge',
'covalent',
];
export type InteractionInfo =
| { kind: 'unknown' }
| { kind: 'ionic' }
| { kind: 'pi-stacking' }
| { kind: 'cation-pi' }
| { kind: 'halogen-bond' }
| { kind: 'hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
| { kind: 'hydrophobic' }
| { kind: 'metal-coordination' }
| { kind: 'salt-bridge' }
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
export interface StructureInteractionElement {
// Pass the schema when loading from custom data
sourceSchema?: InteractionElementSchema,
info: InteractionInfo,
aStructureRef?: string,
a: StructureElement.Loci,
bStructureRef?: string,
b: StructureElement.Loci,
}
export interface StructureInteractions {
kind: 'structure-interactions',
elements: StructureInteractionElement[],
}
export const InteractionTypeToKind = {
[InteractionType.Unknown]: 'unknown' as InteractionKind,
[InteractionType.Ionic]: 'ionic' as InteractionKind,
[InteractionType.CationPi]: 'cation-pi' as InteractionKind,
[InteractionType.PiStacking]: 'pi-stacking' as InteractionKind,
[InteractionType.HydrogenBond]: 'hydrogen-bond' as InteractionKind,
[InteractionType.HalogenBond]: 'halogen-bond' as InteractionKind,
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
};

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { InteractionsParams } from '../../mol-model-props/computed/interactions';
import { Structure } from '../../mol-model/structure';
import { PluginStateObject as SO } from '../../mol-plugin-state/objects';
import { StateTransformer } from '../../mol-state';
import { Task } from '../../mol-task';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { computeContacts } from './compute';
import { getCustomInteractionData } from './custom';
import { InteractionElementSchema, StructureInteractions } from './model';
import { buildInteractionsShape, InteractionVisualParams } from './visuals';
const Factory = StateTransformer.builderFactory('interactions-extension');
export class InteractionData extends SO.Create<{ interactions: StructureInteractions }>({ name: 'Interactions', typeClass: 'Data' }) { }
export const ComputeContacts = Factory({
name: 'compute-contacts',
display: 'Compute Contacts',
from: SO.Molecule.Structure.Selections,
to: InteractionData,
params: {
interactions: PD.Group(InteractionsParams),
},
})({
apply({ params, a }) {
return Task.create('Compute Contacts', async ctx => {
const interactions = await computeContacts(ctx, a.data, { interactions: params.interactions });
return new InteractionData({ interactions }, { label: 'Interactions' });
});
}
});
export const CustomInteractions = Factory({
name: 'custom-interactions',
display: 'Custom Interactions',
from: SO.Root,
to: InteractionData,
params: {
interactions: PD.Value<InteractionElementSchema[]>([], { isHidden: true }),
},
})({
apply({ params, dependencies }) {
return Task.create('Custom Interactions', async ctx => {
const structures: { [ref: string]: Structure } = {};
for (const [k, v] of Object.entries(dependencies ?? {})) {
structures[k] = v.data as Structure;
}
const interactions = getCustomInteractionData(params.interactions, structures);
return new InteractionData({ interactions }, { label: 'Custom Interactions' });
});
}
});
export const InteractionsShape = Factory({
name: 'interactions-shape',
display: { name: 'Interactions Shape' },
from: InteractionData,
to: SO.Shape.Provider,
params: InteractionVisualParams
})({
canAutoUpdate: () => true,
apply({ a, params }) {
return new SO.Shape.Provider({
label: 'Interactions Shape Provider',
data: { interactions: a.data.interactions, params },
params: PD.withDefaults(Mesh.Params, { }),
getShape: (_, data: { interactions: StructureInteractions, params: InteractionVisualParams }, __, prev: any) => buildInteractionsShape(data.interactions, data.params, prev?.geometry),
geometryUtils: Mesh.Utils,
}, { label: 'Interactions Shape Provider' });
}
});

View File

@@ -0,0 +1,199 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { addFixedCountDashedCylinder, addSimpleCylinder, BasicCylinderProps } from '../../mol-geo/geometry/mesh/builder/cylinder';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
import { Sphere3D } from '../../mol-math/geometry';
import { Vec3 } from '../../mol-math/linear-algebra';
import { Shape } from '../../mol-model/shape';
import { StructureElement } from '../../mol-model/structure';
import { addLinkCylinderMesh, AddLinkOptions, AddLinkParams, DefaultLinkCylinderProps, LinkStyle } from '../../mol-repr/structure/visual/util/link';
import { Color } from '../../mol-util/color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { stringToWords } from '../../mol-util/string';
import { InteractionKinds, StructureInteractions } from './model';
function visualParams({ color, style = 'dashed', radius = 0.04 }: { color: Color, style?: 'dashed' | 'solid', radius?: number }) {
return PD.Group({
color: PD.Color(color),
style: PD.Select<'dashed' | 'solid'>(style, [['dashed', 'Dashed'], ['solid', 'Solid']]),
radius: PD.Numeric(radius, { min: 0.01, max: 1, step: 0.01 }),
});
}
function hydrogenVisualParams({ color, style = 'dashed', radius = 0.04, showArrow = true, arrowOffset = 0.18 }: { color: Color, style?: 'dashed' | 'solid', radius?: number, showArrow?: boolean, arrowOffset?: number }) {
return PD.Group({
color: PD.Color(color),
style: PD.Select<'dashed' | 'solid'>(style, [['dashed', 'Dashed'], ['solid', 'Solid']]),
radius: PD.Numeric(radius, { min: 0.01, max: 1, step: 0.01 }),
showArrow: PD.Boolean(showArrow),
arrowOffset: PD.Numeric(arrowOffset, { min: 0, max: 1, step: 0.001 }),
});
}
export const InteractionVisualParams = {
kinds: PD.MultiSelect(InteractionKinds, InteractionKinds.map(k => [k, stringToWords(k)])),
styles: PD.Group({
'unknown': visualParams({ color: Color(0x0) }),
'ionic': visualParams({ color: Color(0xADD8E6) }),
'pi-stacking': visualParams({ color: Color(0x1E3F66) }),
'cation-pi': visualParams({ color: Color(0x06402B) }),
'halogen-bond': visualParams({ color: Color(0xFFDE21) }),
'hydrogen-bond': hydrogenVisualParams({ color: Color(0x0), style: 'solid' }),
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
'hydrophobic': visualParams({ color: Color(0x555555) }),
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
'salt-bridge': visualParams({ color: Color(0xF54029) }),
'covalent': PD.Group({
color: PD.Color(Color(0x999999)),
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),
}),
})
};
export type InteractionVisualParams = PD.Values<typeof InteractionVisualParams>;
export function buildInteractionsShape(interactions: StructureInteractions, params: InteractionVisualParams, prev?: Mesh): Shape<Mesh> {
const mesh = MeshBuilder.createState(interactions.elements.length * 128, 1024, prev);
mesh.currentGroup = -1;
const tooltips = new Map<number, string>();
const visible = new Set(params.kinds);
const kindsToWords = new Map(InteractionKinds.map(k => [k, stringToWords(k)]));
const colors = new Map<number, Color>();
const bA = { sphere: Sphere3D.zero() };
const bB = { sphere: Sphere3D.zero() };
const pA = Vec3();
const pB = Vec3();
const dir = Vec3();
const capPos = Vec3();
const addLinkOptions: AddLinkOptions = {
builderState: mesh,
props: { ...DefaultLinkCylinderProps },
};
const addLinkParams: AddLinkParams = {
a: pA,
b: pB,
group: 0,
linkStub: false,
linkStyle: LinkStyle.Solid,
linkRadius: 0,
};
for (const interaction of interactions.elements) {
mesh.currentGroup++;
if (!visible.has(interaction.info.kind)) continue;
let tooltip: string;
if (interaction.info.kind === 'covalent') {
if (interaction.info.degree === 'aromatic') tooltip = 'Aromatic';
else if (interaction.info.degree === 1) tooltip = 'Single';
else if (interaction.info.degree === 2) tooltip = 'Double';
else if (interaction.info.degree === 3) tooltip = 'Triple';
else if (interaction.info.degree === 4) tooltip = 'Quadruple';
else tooltip = 'Covalent';
} else {
tooltip = kindsToWords.get(interaction.info.kind) ?? interaction.info.kind;
}
if (interaction.sourceSchema?.description) {
tooltip += ` (${interaction.sourceSchema.description})`;
}
tooltips.set(mesh.currentGroup, tooltip);
const options = params.styles[interaction.info.kind];
let style: 'dashed' | 'solid' = 'solid';
if (interaction.info.kind !== 'covalent') {
style = params.styles[interaction.info.kind].style;
}
colors.set(mesh.currentGroup, params.styles[interaction.info.kind].color);
StructureElement.Loci.getBoundary(interaction.a, undefined, bA);
StructureElement.Loci.getBoundary(interaction.b, undefined, bB);
Vec3.sub(dir, bB.sphere.center, bA.sphere.center);
Vec3.normalize(dir, dir);
Vec3.copy(pA, bA.sphere.center);
Vec3.copy(pB, bB.sphere.center);
if (interaction.info.kind === 'hydrogen-bond' || interaction.info.kind === 'weak-hydrogen-bond') {
const hydrogenStyle = params.styles[interaction.info.kind];
if (hydrogenStyle.showArrow && hydrogenStyle.arrowOffset > 0) {
Vec3.scaleAndAdd(pB, pB, dir, -hydrogenStyle.arrowOffset);
}
if (hydrogenStyle.showArrow) {
const height = options.radius * 3;
Vec3.scaleAndAdd(capPos, pB, dir, -height);
cylinder(mesh, pA, capPos, options.radius, style);
addSimpleCylinder(
mesh,
capPos,
pB,
{ radiusTop: 0, radiusBottom: height, topCap: false, bottomCap: true }
);
} else {
cylinder(mesh, pA, pB, options.radius, style);
}
} else {
if (interaction.info.kind !== 'covalent') {
cylinder(mesh, pA, pB, options.radius, style);
} else {
addLinkParams.group = mesh.currentGroup;
addLinkParams.linkRadius = options.radius;
const degree = interaction.info.degree ?? 1;
if (degree === 'aromatic') addLinkParams.linkStyle = LinkStyle.Aromatic;
else if (degree === 2) addLinkParams.linkStyle = LinkStyle.Double;
else if (degree === 3) addLinkParams.linkStyle = LinkStyle.Triple;
else addLinkParams.linkStyle = LinkStyle.Solid;
addLinkParams.a = pA;
addLinkParams.b = pB;
addLinkCylinderMesh(addLinkOptions, addLinkParams);
addLinkParams.a = pB;
addLinkParams.b = pA;
addLinkCylinderMesh(addLinkOptions, addLinkParams);
}
}
}
return Shape.create(
'Interactions',
interactions,
MeshBuilder.getMesh(mesh),
(g) => colors.get(g) ?? 0 as Color,
(g) => 1,
(g) => tooltips.get(g) ?? ''
);
}
function cylinder(mesh: MeshBuilder.State, a: Vec3, b: Vec3, radius: number, style: 'dashed' | 'solid') {
const props: BasicCylinderProps = {
radiusBottom: radius,
radiusTop: radius,
topCap: true,
bottomCap: true,
};
if (style === 'dashed') {
const dist = Vec3.distance(a, b);
const count = Math.ceil(dist / (2 * radius));
addFixedCountDashedCylinder(mesh, a, b, 1.0, count, true, props);
} else {
if (style !== 'solid') {
console.warn(`Unknown style '${style}', using 'solid' instead.`);
}
addSimpleCylinder(mesh, a, b, props);
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-24 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-25 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>
@@ -38,10 +38,7 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
private labelProvider = {
label: (loci: Loci): string | undefined => {
if (!this.params.showTooltip) return;
return [
plddtLabel(loci),
qmeanLabel(loci),
].filter(l => !!l).join('</br>');
return metricLabels(loci)?.join('</br>');
}
};
@@ -106,59 +103,77 @@ function plddtCategory(score: number) {
return 'Very low';
}
function plddtLabel(loci: Loci): string | undefined {
return metricLabel(loci, 'pLDDT', (scoreAvg: number, countInfo: string) => `pLDDT Score ${countInfo}: ${scoreAvg.toFixed(2)} <small>(${plddtCategory(scoreAvg)})</small>`);
function buildLabel(metric: QualityAssessment.Local, scoreAvg: number, countInfo: string) {
let label = metric.name;
if (metric.type !== metric.name) label += ` (${metric.type})`;
if (countInfo) label += countInfo;
label += `: ${scoreAvg.toFixed(2)}`;
if (metric.kind === 'pLDDT') label += ` <small>(${plddtCategory(scoreAvg)})</small>`;
return label;
}
function qmeanLabel(loci: Loci): string | undefined {
return metricLabel(loci, 'qmean', (scoreAvg: number, countInfo: string) => `QMEAN Score ${countInfo}: ${scoreAvg.toFixed(2)}`);
interface MetricAggregate {
metric: QualityAssessment.Local,
scoreSum: number,
seenResidues: Set<number>,
}
function metricLabel(loci: Loci, name: 'qmean' | 'pLDDT', label: (scoreAvg: number, countInfo: string) => string): string | undefined {
if (loci.kind === 'element-loci') {
if (loci.elements.length === 0) return;
function metricLabels(loci: Loci): string[] | undefined {
if (loci.kind !== 'element-loci') return;
if (loci.elements.length === 0) return;
const seen = new Set<number>();
const scoreSeen = new Set<number>();
let scoreSum = 0;
const seenMetrics: MetricAggregate[] = [];
const aggregates = new Map<string, MetricAggregate>();
for (const { indices, unit } of loci.elements) {
const metric = QualityAssessmentProvider.get(unit.model).value?.[name];
if (!metric) continue;
for (const { indices, unit } of loci.elements) {
const metrics = QualityAssessmentProvider.get(unit.model).value?.local;
if (!metrics) continue;
const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index;
const { elements } = unit;
const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index;
const { elements } = unit;
for (const metric of metrics) {
const key = `${metric.name}-${metric.type}`;
let aggregate = aggregates.get(key);
if (!aggregate) {
aggregate = { metric, scoreSum: 0, seenResidues: new Set() };
aggregates.set(key, aggregate);
seenMetrics.push(aggregate);
}
const values = metric.values;
const { seenResidues } = aggregate;
OrderedSet.forEach(indices, idx => {
const eI = elements[idx];
const rI = residueIndex[eI];
const residueKey = cantorPairing(rI, unit.id);
if (!seen.has(residueKey)) {
const score = metric.get(residueIndex[eI]) ?? -1;
if (score !== -1) {
scoreSum += score;
scoreSeen.add(residueKey);
}
seen.add(residueKey);
}
if (seenResidues.has(residueKey)) return;
const score = values.get(residueIndex[eI]);
if (typeof score === 'undefined') return;
aggregate!.scoreSum += score;
aggregate!.seenResidues.add(residueKey);
});
}
if (seen.size === 0) return;
const summary: string[] = [];
if (scoreSeen.size) {
const countInfo = `<small>(${scoreSeen.size} ${scoreSeen.size > 1 ? 'Residues avg.' : 'Residue'})</small>`;
const scoreAvg = scoreSum / scoreSeen.size;
summary.push(label(scoreAvg, countInfo));
}
if (summary.length) {
return summary.join('</br>');
}
}
if (seenMetrics.length === 0) return;
const labels: string[] = [];
for (const { metric, scoreSum, seenResidues } of seenMetrics) {
let countInfo = '';
if (seenResidues.size > 1) {
countInfo = ` <small>(${seenResidues.size} Residues avg.)</small>`;
}
const scoreAvg = scoreSum / seenResidues.size;
const label = buildLabel(metric, scoreAvg, countInfo);
labels.push(label);
}
return labels;
}
//

View File

@@ -1,9 +1,10 @@
/**
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Mandar Deshpande <mandar@ebi.ac.uk>
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { QualityAssessment, QualityAssessmentProvider } from '../prop';
@@ -15,6 +16,7 @@ import { Color } from '../../../../mol-util/color';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
import { TableLegend } from '../../../../mol-util/legend';
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
const DefaultColor = Color(0xaaaaaa);
const ConfidenceColors = {
@@ -28,7 +30,9 @@ const ConfidenceColors = {
const ConfidenceColorLegend = TableLegend(Object.entries(ConfidenceColors));
export function getPLDDTConfidenceColorThemeParams(ctx: ThemeDataContext) {
return {};
return {
metricId: QualityAssessment.getLocalOptions(ctx.structure?.models[0], 'pLDDT'),
};
}
export type PLDDTConfidenceColorThemeParams = ReturnType<typeof getPLDDTConfidenceColorThemeParams>
@@ -43,7 +47,8 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
if (!Unit.isAtomic(unit)) return DefaultColor;
const qualityAssessment = QualityAssessmentProvider.get(unit.model).value;
let score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]);
const metric = qualityAssessment?.localMap.get(props.metricId!)?.values ?? qualityAssessment?.pLDDT;
let score = metric?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]);
if (typeof score !== 'number') {
score = unit.model.atomicConformation.B_iso_or_equiv.value(element);
}
@@ -87,7 +92,7 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
export const PLDDTConfidenceColorThemeProvider: ColorTheme.Provider<PLDDTConfidenceColorThemeParams, 'plddt-confidence'> = {
name: 'plddt-confidence',
label: 'pLDDT Confidence',
category: ColorTheme.Category.Validation,
category: ColorThemeCategory.Validation,
factory: PLDDTConfidenceColorTheme,
getParams: getPLDDTConfidenceColorThemeParams,
defaultValues: PD.getDefaultValues(getPLDDTConfidenceColorThemeParams({})),

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-25 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 { QualityAssessment, QualityAssessmentProvider } from '../prop';
@@ -12,11 +13,14 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
import { Color, ColorScale } from '../../../../mol-util/color';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
const DefaultColor = Color(0xaaaaaa);
export function getQmeanScoreColorThemeParams(ctx: ThemeDataContext) {
return {};
return {
metricId: QualityAssessment.getLocalOptions(ctx.structure?.models[0], 'qmean'),
};
}
export type QmeanScoreColorThemeParams = ReturnType<typeof getQmeanScoreColorThemeParams>
@@ -37,7 +41,8 @@ export function QmeanScoreColorTheme(ctx: ThemeDataContext, props: PD.Values<Qme
const { unit, element } = location;
if (!Unit.isAtomic(unit)) return DefaultColor;
const qualityAssessment = QualityAssessmentProvider.get(unit.model).value;
const score = qualityAssessment?.qmean?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1;
const metric = qualityAssessment?.localMap.get(props.metricId!)?.values ?? qualityAssessment?.qmean;
const score = metric?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1;
if (score < 0) {
return DefaultColor;
} else {
@@ -71,7 +76,7 @@ export function QmeanScoreColorTheme(ctx: ThemeDataContext, props: PD.Values<Qme
export const QmeanScoreColorThemeProvider: ColorTheme.Provider<QmeanScoreColorThemeParams, 'qmean-score'> = {
name: 'qmean-score',
label: 'QMEAN Score',
category: ColorTheme.Category.Validation,
category: ColorThemeCategory.Validation,
factory: QmeanScoreColorTheme,
getParams: getQmeanScoreColorThemeParams,
defaultValues: PD.getDefaultValues(getQmeanScoreColorThemeParams({})),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2024-25 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -29,6 +29,18 @@ function drawMetricPNG(model: Model, metric: QualityAssessment.Pairwise, colorRa
ctx.fillStyle = Color.toStyle(noDataColor);
ctx.fillRect(0, 0, canvas.width, canvas.height);
const colorCache = new Map<number, string>();
const getColor = (t: number) => {
const rounded = Math.round(t * 0xffff);
if (colorCache.has(rounded)) {
return colorCache.get(rounded)!;
}
const color = Color.interpolate(minColor, maxColor, rounded / 0xffff);
const style = Color.toStyle(color);
colorCache.set(rounded, style);
return style;
};
for (let rA = minResidueIndex; rA <= maxResidueIndex; rA++) {
const row = values[rA];
if (!row) continue;
@@ -40,11 +52,12 @@ function drawMetricPNG(model: Model, metric: QualityAssessment.Pairwise, colorRa
const x = rA - minResidueIndex;
const y = rB - minResidueIndex;
const t = (value - minMetric) / valueRange;
const color = Color.interpolate(minColor, maxColor, t);
ctx.fillStyle = Color.toStyle(color);
ctx.fillStyle = getColor(t);
ctx.fillRect(x, y, 1, 1);
ctx.fillRect(y, x, 1, 1);
if (typeof values[rB]?.[rA] !== 'number') {
ctx.fillRect(y, x, 1, 1);
}
}
}

View File

@@ -18,17 +18,39 @@ import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomi
import { CustomPropSymbol } from '../../../mol-script/language/symbol';
import { Type } from '../../../mol-script/language/type';
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { ParamDefinition, ParamDefinition as PD } from '../../../mol-util/param-definition';
export { QualityAssessment };
interface QualityAssessment {
localMetrics: Map<string, Map<ResidueIndex, number>>
local: QualityAssessment.Local[]
/** id -> metric info */
localMap: Map<number, QualityAssessment.Local>
/** default pLDDT metric */
pLDDT?: Map<ResidueIndex, number>
/** default qmean metric */
qmean?: Map<ResidueIndex, number>
/**
* @deprecated
* NOTE: Keeping this around in case someone is using it
* TODO: Remove in Mol* 5.0
*/
localMetrics: Map<string, Map<ResidueIndex, number>>
}
namespace QualityAssessment {
export interface Local {
id: number
kind?: 'pLDDT' | 'qmean';
type: mmCIF_Schema['ma_qa_metric']['type']['T']
name: string
domain?: [number, number]
valueRange: [number, number]
values: Map<ResidueIndex, number>
}
export interface Pairwise {
id: number
name: string
@@ -40,6 +62,8 @@ namespace QualityAssessment {
const Empty = {
value: {
local: [],
localMap: new Map<number, Local>(),
localMetrics: new Map(),
} satisfies QualityAssessment
};
@@ -52,9 +76,11 @@ namespace QualityAssessment {
db.ma_qa_metric_local.ordinal_id.isDefined
);
if (localMetricName && hasLocalMetric) {
const nameQuery = localMetricName.toLowerCase();
for (let i = 0, il = db.ma_qa_metric._rowCount; i < il; i++) {
if (db.ma_qa_metric.mode.value(i) !== 'local') continue;
if (localMetricName === db.ma_qa_metric.name.value(i)) return true;
const name = db.ma_qa_metric.name.value(i).toLowerCase();
if (name.includes(nameQuery)) return true;
}
return false;
} else {
@@ -62,27 +88,65 @@ namespace QualityAssessment {
}
}
export function getLocalOptions(model: Model | undefined, kind: 'pLDDT' | 'qmean') {
if (!model) return ParamDefinition.Select(undefined, [], { label: 'Metric', isHidden: true });
const local = QualityAssessmentProvider.get(model).value?.local;
if (!local) return ParamDefinition.Select(undefined, [], { label: 'Metric', isHidden: true });
const options = local.filter(m => m.kind === kind).map(m => [m.id, `${m.name} (${m.type})`] as [number, string]);
return ParamDefinition.Select(options[0]?.[0], options, { label: 'Metric ' });
}
export async function obtain(ctx: CustomProperty.Context, model: Model, props: QualityAssessmentProps): Promise<CustomProperty.Data<QualityAssessment>> {
if (!model || !MmcifFormat.is(model.sourceData)) return Empty;
const { ma_qa_metric, ma_qa_metric_local } = model.sourceData.data.db;
const { model_id, label_asym_id, label_seq_id, metric_id, metric_value } = ma_qa_metric_local;
const { index } = model.atomicHierarchy;
const locals: Local[] = [];
const localMetrics = new Map<number, Local>();
// for simplicity we assume names in ma_qa_metric for mode 'local' are unique
const localMetrics = new Map<string, Map<ResidueIndex, number>>();
const localMetricValues = new Map<string, Map<ResidueIndex, number>>();
const localNames = new Map<number, string>();
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
if (ma_qa_metric.mode.value(i) !== 'local') continue;
const id = ma_qa_metric.id.value(i);
const name = ma_qa_metric.name.value(i);
if (localMetrics.has(name)) {
const type = ma_qa_metric.type.value(i);
const values: Map<ResidueIndex, number> = new Map();
const ispPLDDType = type.toLowerCase().includes('plddt');
const has01Range = type.replace(/\s/g, '').includes('[0,1]');
let domain: [number, number] | undefined;
if (has01Range) {
domain = [0, 1];
} else if (ispPLDDType) {
domain = [0, 100];
}
let kind: 'pLDDT' | 'qmean' | undefined;
const nameLower = name.toLowerCase();
if (nameLower.includes('plddt')) kind = 'pLDDT';
else if (nameLower.includes('qmean')) kind = 'qmean';
if (!kind && ispPLDDType) kind = 'pLDDT';
if (!kind && has01Range) kind = 'qmean';
const metric: Local = { id, kind, type, name, domain, valueRange: [Number.MAX_VALUE, -Number.MAX_VALUE], values };
localMetrics.set(id, metric);
locals.push(metric);
if (localMetricValues.has(name)) {
console.warn(`local ma_qa_metric with name '${name}' already added`);
continue;
}
localMetrics.set(name, new Map());
localNames.set(ma_qa_metric.id.value(i), name);
localMetricValues.set(name, values);
localNames.set(id, name);
}
const residueKey: AtomicIndex.ResidueLabelKey = {
@@ -104,16 +168,25 @@ namespace QualityAssessment {
const rI = index.findResidueLabel(residueKey);
if (rI >= 0) {
const name = localNames.get(metric_id.value(i))!;
localMetrics.get(name)!.set(rI, metric_value.value(i));
const entry = localMetrics.get(metric_id.value(i));
if (!entry) continue;
const value = metric_value.value(i);
const range = entry.valueRange;
if (value < range[0]) range[0] = value;
if (value > range[1]) range[1] = value;
entry.values.set(rI, value);
}
}
return {
value: {
localMetrics,
pLDDT: localMetrics.get('pLDDT'),
qmean: localMetrics.get('qmean'),
local: Array.from(localMetrics.values()),
localMap: localMetrics,
pLDDT: locals.find(m => m.kind === 'pLDDT')?.values,
qmean: locals.find(m => m.kind === 'qmean')?.values,
localMetrics: localMetricValues,
}
};
}

View File

@@ -9,7 +9,7 @@ import { MVSData } from '../mvs-data';
describe('MVSData', () => {
it('MVSData functions work', async () => {
it.skip('MVSData functions work', async () => {
const data = fs.readFileSync('examples/mvs/1cbs.mvsj', { encoding: 'utf8' });
const mvsData = MVSData.fromMVSJ(data);
expect(mvsData).toBeTruthy();
@@ -26,7 +26,7 @@ describe('MVSData', () => {
expect(prettyString.length).toBeGreaterThan(0);
});
it('MVSData builder works', async () => {
it.skip('MVSData builder works', async () => {
const builder = MVSData.createBuilder();
expect(builder).toBeTruthy();

View File

@@ -6,8 +6,8 @@
import { Location } from '../../../mol-model/location';
import { Bond, StructureElement } from '../../../mol-model/structure';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { ThemeDataContext } from '../../../mol-theme/theme';
import type { ColorTheme, LocationColor } from '../../../mol-theme/color';
import type { ThemeDataContext } from '../../../mol-theme/theme';
import { ColorNames } from '../../../mol-util/color/names';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { decodeColor } from '../helpers/utils';
@@ -73,7 +73,7 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
export const MVSAnnotationColorThemeProvider: ColorTheme.Provider<MVSAnnotationColorThemeParams, 'mvs-annotation'> = {
name: 'mvs-annotation',
label: 'MVS Annotation',
category: ColorTheme.Category.Misc,
category: 'Miscellaneous', // ColorTheme.Category.Misc can cause webpack build error due to import ordering
factory: MVSAnnotationColorTheme,
getParams: ctx => MVSAnnotationColorThemeParams,
defaultValues: PD.getDefaultValues(MVSAnnotationColorThemeParams),

View File

@@ -7,6 +7,7 @@
import { Location } from '../../../mol-model/location';
import { Bond, Structure, StructureElement } from '../../../mol-model/structure';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
import { ThemeDataContext } from '../../../mol-theme/theme';
import { Color } from '../../../mol-util/color';
import { ColorNames } from '../../../mol-util/color/names';
@@ -139,7 +140,7 @@ export function makeMultilayerColorThemeProvider(colorThemeRegistry: ColorTheme.
return {
name: MultilayerColorThemeName,
label: 'MVS Multi-layer',
category: ColorTheme.Category.Misc,
category: ColorThemeCategory.Misc,
factory: (ctx, props) => makeMultilayerColorTheme(ctx, props, colorThemeRegistry),
getParams: (ctx: ThemeDataContext) => makeMultilayerColorThemeParams(colorThemeRegistry, ctx),
defaultValues: DefaultMultilayerColorThemeProps,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2024-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
@@ -8,12 +8,17 @@
import { Lines } from '../../../mol-geo/geometry/lines/lines';
import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
import { addFixedCountDashedCylinder, addSimpleCylinder, BasicCylinderProps } from '../../../mol-geo/geometry/mesh/builder/cylinder';
import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { Text } from '../../../mol-geo/geometry/text/text';
import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
import { Box, BoxCage } from '../../../mol-geo/primitive/box';
import { Circle } from '../../../mol-geo/primitive/circle';
import { Primitive } from '../../../mol-geo/primitive/primitive';
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
import { radToDeg } from '../../../mol-math/misc';
import { Shape } from '../../../mol-model/shape';
import { Structure, StructureElement, StructureSelection } from '../../../mol-model/structure';
import { StructureQueryHelper } from '../../../mol-plugin-state/helpers/structure-query';
@@ -278,6 +283,15 @@ const Builders: Record<PrimitiveParams['kind'], PrimitiveBuilder> = {
},
resolveRefs: resolveLineRefs,
},
arrow: {
builders: {
mesh: addArrowMesh,
},
resolveRefs: (params: PrimitiveParams<'arrow'>, refs: Set<string>) => {
addRef(params.start, refs);
if (params.end) addRef(params.end, refs);
},
},
label: {
builders: {
label: addPrimitiveLabel,
@@ -291,6 +305,45 @@ const Builders: Record<PrimitiveParams['kind'], PrimitiveBuilder> = {
},
resolveRefs: resolveLineRefs,
},
angle_measurement: {
builders: {
mesh: addAngleMesh,
label: addAngleLabel,
},
resolveRefs: (params: PrimitiveParams<'angle_measurement'>, refs: Set<string>) => {
addRef(params.a, refs);
addRef(params.b, refs);
addRef(params.c, refs);
},
},
ellipse: {
builders: {
mesh: addEllipseMesh,
},
resolveRefs: (params: PrimitiveParams<'ellipse'>, refs: Set<string>) => {
addRef(params.center, refs);
if (params.major_axis_endpoint) addRef(params.major_axis_endpoint, refs);
if (params.minor_axis_endpoint) addRef(params.minor_axis_endpoint, refs);
},
},
ellipsoid: {
builders: {
mesh: addEllipsoidMesh,
},
resolveRefs: (params: PrimitiveParams<'ellipsoid'>, refs: Set<string>) => {
addRef(params.center, refs);
if (params.major_axis_endpoint) addRef(params.major_axis_endpoint, refs);
if (params.minor_axis_endpoint) addRef(params.minor_axis_endpoint, refs);
},
},
box: {
builders: {
mesh: addBoxMesh,
},
resolveRefs: (params: PrimitiveParams<'box'>, refs: Set<string>) => {
addRef(params.center, refs);
},
}
};
@@ -602,6 +655,86 @@ function addTubeMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBui
}
}
const ArrowState = {
start: Vec3.zero(),
end: Vec3.zero(),
dir: Vec3.zero(),
startCap: Vec3.zero(),
endCap: Vec3.zero(),
};
function addArrowMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'arrow'>) {
resolveBasePosition(context, params.start, ArrowState.start);
if (params.end) {
resolveBasePosition(context, params.end, ArrowState.end);
}
if (params.direction) {
Vec3.add(ArrowState.end, ArrowState.start, params.direction as any as Vec3);
}
Vec3.sub(ArrowState.dir, ArrowState.end, ArrowState.start);
Vec3.normalize(ArrowState.dir, ArrowState.dir);
if (params.length) {
Vec3.scaleAndAdd(ArrowState.end, ArrowState.start, ArrowState.dir, params.length);
}
const length = Vec3.distance(ArrowState.start, ArrowState.end);
if (length < 1e-3) return;
const tubeRadius = params.tube_radius;
const tubeProps: BasicCylinderProps = {
radiusBottom: tubeRadius,
radiusTop: tubeRadius,
topCap: !params.show_end_cap,
bottomCap: !params.show_start_cap,
};
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, params.color);
groups.updateTooltip(mesh.currentGroup, params.tooltip);
const startRadius = params.start_cap_radius ?? tubeRadius;
if (params.show_start_cap) {
Vec3.scaleAndAdd(ArrowState.startCap, ArrowState.start, ArrowState.dir, startRadius);
addSimpleCylinder(mesh, ArrowState.startCap, ArrowState.start, {
radiusBottom: startRadius,
radiusTop: 0,
topCap: false,
bottomCap: true,
radialSegments: 12,
});
} else {
Vec3.copy(ArrowState.startCap, ArrowState.start);
}
const endRadius = params.end_cap_radius ?? tubeRadius;
if (params.show_end_cap) {
Vec3.scaleAndAdd(ArrowState.endCap, ArrowState.end, ArrowState.dir, -endRadius);
addSimpleCylinder(mesh, ArrowState.endCap, ArrowState.end, {
radiusBottom: endRadius,
radiusTop: 0,
topCap: false,
bottomCap: true,
radialSegments: 12,
});
} else {
Vec3.copy(ArrowState.endCap, ArrowState.end);
}
if (params.show_tube) {
if (params.tube_dash_length) {
const dist = Vec3.distance(ArrowState.startCap, ArrowState.endCap);
const count = Math.ceil(dist / (2 * params.tube_dash_length));
addFixedCountDashedCylinder(mesh, ArrowState.startCap, ArrowState.endCap, 1.0, count, true, tubeProps);
} else {
addSimpleCylinder(mesh, ArrowState.startCap, ArrowState.endCap, tubeProps);
}
}
}
function getDistanceLabel(context: PrimitiveBuilderContext, params: PrimitiveParams<'distance_measurement'>) {
resolveBasePosition(context, params.start, lStart);
resolveBasePosition(context, params.end, lEnd);
@@ -646,17 +779,332 @@ function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderS
labels.add(label, labelPos[0], labelPos[1], labelPos[2], 1.05 * (params.radius), 1, group);
}
const AngleState = {
a: Vec3(),
b: Vec3(),
c: Vec3(),
ba: Vec3(),
bc: Vec3(),
labelPos: Vec3(),
radius: 0,
};
function syncAngleState(context: PrimitiveBuilderContext, params: PrimitiveParams<'angle_measurement'>) {
resolveBasePosition(context, params.a, AngleState.a);
resolveBasePosition(context, params.b, AngleState.b);
resolveBasePosition(context, params.c, AngleState.c);
Vec3.sub(AngleState.ba, AngleState.a, AngleState.b);
Vec3.sub(AngleState.bc, AngleState.c, AngleState.b);
const value = radToDeg(Vec3.angle(AngleState.ba, AngleState.bc));
const angle = `${round(value, 2)}\u00B0`;
const label = typeof params.label_template === 'string' ? params.label_template.replace('{{angle}}', angle) : angle;
if (typeof params.section_radius === 'number') {
AngleState.radius = params.section_radius;
} else {
AngleState.radius = Math.min(Vec3.magnitude(AngleState.ba), Vec3.magnitude(AngleState.bc));
if (typeof params.section_radius_scale === 'number') {
AngleState.radius *= params.section_radius_scale;
}
}
return label;
}
function addAngleMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'angle_measurement'>) {
const label = syncAngleState(context, params);
const { groups, mesh } = state;
if (params.show_vector) {
const radius = 0.01;
const cylinderProps: BasicCylinderProps = {
radiusBottom: radius,
radiusTop: radius,
topCap: true,
bottomCap: true,
};
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, params.vector_color);
groups.updateTooltip(mesh.currentGroup, label);
let count = Math.ceil(Vec3.magnitude(AngleState.ba) / (2 * radius));
addFixedCountDashedCylinder(mesh, AngleState.a, AngleState.b, 1.0, count, true, cylinderProps);
count = Math.ceil(Vec3.magnitude(AngleState.bc) / (2 * radius));
addFixedCountDashedCylinder(mesh, AngleState.b, AngleState.c, 1.0, count, true, cylinderProps);
}
if (params.show_section) {
const angle = Vec3.angle(AngleState.ba, AngleState.bc);
Vec3.normalize(AngleState.ba, AngleState.ba);
Vec3.normalize(AngleState.bc, AngleState.bc);
Vec3.scale(AngleState.ba, AngleState.ba, AngleState.radius);
Vec3.scale(AngleState.bc, AngleState.bc, AngleState.radius);
addEllipseMesh(context, state, node, {
kind: 'ellipse',
as_circle: true,
center: AngleState.b as any,
major_axis_endpoint: null,
major_axis: AngleState.ba as any,
minor_axis_endpoint: null,
minor_axis: AngleState.bc as any,
radius_major: AngleState.radius,
radius_minor: AngleState.radius,
theta_start: 0,
theta_end: angle,
color: params.section_color,
tooltip: label,
});
}
}
function addAngleLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'angle_measurement'>) {
const { labels, groups } = state;
const label = syncAngleState(context, params);
Vec3.normalize(AngleState.ba, AngleState.ba);
Vec3.normalize(AngleState.bc, AngleState.bc);
Vec3.scale(AngleState.ba, AngleState.ba, AngleState.radius);
Vec3.scale(AngleState.bc, AngleState.bc, AngleState.radius);
let size: number | undefined;
if (typeof params.label_size === 'number') {
size = params.label_size;
} else {
size = Math.max(AngleState.radius * (params.label_auto_size_scale), params.label_auto_size_min);
}
Vec3.add(AngleState.labelPos, AngleState.ba, AngleState.bc);
Vec3.normalize(AngleState.labelPos, AngleState.labelPos);
Vec3.scale(AngleState.labelPos, AngleState.labelPos, AngleState.radius);
Vec3.add(AngleState.labelPos, AngleState.labelPos, AngleState.b);
const group = groups.allocateSingle(node);
groups.updateColor(group, params.label_color);
groups.updateSize(group, size);
labels.add(label, AngleState.labelPos[0], AngleState.labelPos[1], AngleState.labelPos[2], 1, 1, group);
}
function resolveLabelRefs(params: PrimitiveParams<'label'>, refs: Set<string>) {
addRef(params.position, refs);
}
const PrimitiveLabelState = {
position: Vec3.zero(),
sphere: Sphere3D.zero(),
};
function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'label'>) {
const { labels, groups } = state;
resolveBasePosition(context, params.position, labelPos);
resolvePosition(context, params.position, PrimitiveLabelState.position, PrimitiveLabelState.sphere, undefined);
const group = groups.allocateSingle(node);
groups.updateColor(group, params.label_color);
groups.updateSize(group, params.label_size);
labels.add(params.text, labelPos[0], labelPos[1], labelPos[2], params.label_offset, 1, group);
const offset = PrimitiveLabelState.sphere.radius + params.label_offset;
labels.add(params.text, PrimitiveLabelState.position[0], PrimitiveLabelState.position[1], PrimitiveLabelState.position[2], offset, 1, group);
}
const circleCache = new Map<string, Primitive>();
function getCircle(options: { thetaStart?: number, thetaEnd?: number }) {
const key = JSON.stringify(options);
if (circleCache.has(key)) return circleCache.get(key)!;
const thetaLength = (options.thetaEnd ?? 2 * Math.PI) - (options.thetaStart ?? 0);
if (Math.abs(thetaLength) < 1e-3) return null;
const circle = Circle({
radius: 1,
thetaStart: options.thetaStart ?? 0,
thetaLength,
segments: Math.ceil(2 * Math.PI / thetaLength * 64),
});
circleCache.set(key, circle);
return circle;
}
const EllipseState = {
centerPos: Vec3.zero(),
majorPos: Vec3.zero(),
minorPos: Vec3.zero(),
majorAxis: Vec3.zero(),
minorAxis: Vec3.zero(),
scale: Vec3.zero(),
normal: Vec3.zero(),
scaleXform: Mat4.identity(),
rotationXform: Mat4.identity(),
translationXform: Mat4.identity(),
xform: Mat4.identity(),
};
function addEllipseMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'ellipse'>) {
// Unit circle in the XZ plane (Y up)
// X = minor axis, Y = normal, Z = major axis
const circle = getCircle({ thetaStart: params.theta_start, thetaEnd: params.theta_end });
if (!circle) return;
resolvePosition(context, params.center, EllipseState.centerPos, undefined, undefined);
if (params.major_axis_endpoint) {
resolvePosition(context, params.major_axis_endpoint, EllipseState.majorPos, undefined, undefined);
Vec3.sub(EllipseState.majorAxis, EllipseState.majorPos, EllipseState.centerPos);
} else {
Vec3.copy(EllipseState.majorAxis, params.major_axis as any as Vec3);
}
if (params.minor_axis_endpoint) {
resolvePosition(context, params.minor_axis_endpoint, EllipseState.minorPos, undefined, undefined);
Vec3.sub(EllipseState.minorAxis, EllipseState.minorPos, EllipseState.centerPos);
} else {
Vec3.copy(EllipseState.minorAxis, params.minor_axis as any as Vec3);
}
const { mesh, groups } = state;
// Translation
Mat4.fromTranslation(EllipseState.translationXform, EllipseState.centerPos);
// Scale
if (params.as_circle) {
const r = params.radius_major ?? Vec3.magnitude(EllipseState.majorAxis);
Vec3.set(EllipseState.scale, r, 1, r);
} else {
const major = params.radius_major ?? Vec3.magnitude(EllipseState.majorAxis);
const minor = params.radius_minor ?? Vec3.magnitude(EllipseState.minorAxis);
Vec3.set(EllipseState.scale, minor, 1, major);
}
Mat4.fromScaling(EllipseState.scaleXform, EllipseState.scale);
// Rotation
Vec3.normalize(EllipseState.minorAxis, EllipseState.minorAxis);
Vec3.normalize(EllipseState.majorAxis, EllipseState.majorAxis);
Vec3.cross(EllipseState.normal, EllipseState.majorAxis, EllipseState.minorAxis);
Mat4.targetTo(EllipseState.rotationXform, Vec3.origin, EllipseState.majorAxis, EllipseState.normal);
Mat4.mul(EllipseState.rotationXform, EllipseState.rotationXform, Mat4.rotY180);
// Final xform
Mat4.mul3(EllipseState.xform, EllipseState.translationXform, EllipseState.rotationXform, EllipseState.scaleXform);
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, params.color);
groups.updateTooltip(mesh.currentGroup, params.tooltip);
MeshBuilder.addPrimitive(mesh, EllipseState.xform, circle);
MeshBuilder.addPrimitiveFlipped(mesh, EllipseState.xform, circle);
}
const EllipsoidState = {
centerPos: Vec3.zero(),
majorPos: Vec3.zero(),
minorPos: Vec3.zero(),
majorAxis: Vec3.zero(),
minorAxis: Vec3.zero(),
sphere: Sphere3D.zero(),
radius: Vec3.zero(),
extent: Vec3.zero(),
up: Vec3.zero(),
};
function addEllipsoidMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'ellipsoid'>) {
resolvePosition(context, params.center, EllipsoidState.centerPos, EllipsoidState.sphere, undefined);
if (params.major_axis_endpoint) {
resolvePosition(context, params.major_axis_endpoint, EllipsoidState.majorPos, undefined, undefined);
Vec3.sub(EllipsoidState.majorAxis, EllipsoidState.majorPos, EllipsoidState.centerPos);
} else if (params.major_axis) {
Vec3.copy(EllipsoidState.majorAxis, params.major_axis as any as Vec3);
} else {
Vec3.copy(EllipsoidState.majorAxis, Vec3.unitX);
}
if (params.minor_axis_endpoint) {
resolvePosition(context, params.minor_axis_endpoint, EllipsoidState.minorPos, undefined, undefined);
Vec3.sub(EllipsoidState.minorAxis, EllipsoidState.minorPos, EllipsoidState.centerPos);
} else if (params.minor_axis) {
Vec3.copy(EllipsoidState.minorAxis, params.minor_axis as any as Vec3);
} else {
Vec3.copy(EllipsoidState.minorAxis, Vec3.unitY);
}
if (typeof params.radius === 'number') {
Vec3.set(EllipsoidState.radius, params.radius, params.radius, params.radius);
} else if (params.radius) {
Vec3.copy(EllipsoidState.radius, params.radius as any as Vec3);
} else {
const r = EllipsoidState.sphere.radius;
Vec3.set(EllipsoidState.radius, r, r, r);
}
if (typeof params.radius_extent === 'number') {
Vec3.set(EllipsoidState.extent, params.radius_extent, params.radius_extent, params.radius_extent);
} else if (params.radius_extent) {
Vec3.copy(EllipsoidState.extent, params.radius_extent as any as Vec3);
} else {
Vec3.set(EllipsoidState.extent, 0, 0, 0);
}
Vec3.add(EllipsoidState.radius, EllipsoidState.radius, EllipsoidState.extent);
const { mesh, groups } = state;
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, params.color);
groups.updateTooltip(mesh.currentGroup, params.tooltip);
Vec3.normalize(EllipsoidState.majorAxis, EllipsoidState.majorAxis);
Vec3.normalize(EllipsoidState.minorAxis, EllipsoidState.minorAxis);
Vec3.cross(EllipsoidState.up, EllipsoidState.majorAxis, EllipsoidState.minorAxis);
addEllipsoid(mesh, EllipsoidState.centerPos, EllipsoidState.up, EllipsoidState.minorAxis, EllipsoidState.radius, 3);
}
const BoxState = {
center: Vec3.zero(),
boundary: Box3D.zero(),
size: Vec3.zero(),
cage: BoxCage(),
translationXform: Mat4.identity(),
scaleXform: Mat4.identity(),
xform: Mat4.identity(),
};
function addBoxMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'box'>) {
if (!params.show_edges && !params.show_faces) return;
resolvePosition(context, params.center, BoxState.center, undefined, BoxState.boundary);
if (params.extent) {
Box3D.expand(BoxState.boundary, BoxState.boundary, params.extent as unknown as Vec3);
}
if (Box3D.volume(BoxState.boundary) < 1e-3) return;
const { mesh, groups } = state;
Mat4.fromScaling(BoxState.scaleXform, Box3D.size(BoxState.size, BoxState.boundary));
Mat4.fromTranslation(BoxState.translationXform, BoxState.center);
Mat4.mul(BoxState.xform, BoxState.translationXform, BoxState.scaleXform);
if (params.show_faces) {
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, params.face_color);
groups.updateTooltip(mesh.currentGroup, params.tooltip);
MeshBuilder.addPrimitive(mesh, BoxState.xform, Box());
}
if (params.show_edges) {
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, params.edge_color);
groups.updateTooltip(mesh.currentGroup, params.tooltip);
MeshBuilder.addCage(mesh, BoxState.xform, BoxCage(), params.edge_radius, 2, 8);
}
}

View File

@@ -1,7 +1,8 @@
/**
* 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>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Column, Table } from '../../../mol-data/db';

View File

@@ -1,12 +1,12 @@
/**
* 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>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Column } from '../../../mol-data/db';
import { ChainIndex, ElementIndex, Model, ResidueIndex } from '../../../mol-model/structure';
import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
import { ChainIndex, ElementIndex, Model, ResidueIndex, StructureElement } from '../../../mol-model/structure';
import { Expression } from '../../../mol-script/language/expression';
import { arrayExtend, filterInPlace, range } from '../../../mol-util/array';
import { AtomRanges } from './atom-ranges';
@@ -256,71 +256,19 @@ function matchesRange<T>(requiredMin: T | undefined | null, requiredMax: T | und
return true;
}
/** Convert an annotation row into a MolScript expression */
export function rowToExpression(row: MVSAnnotationRow): Expression {
const { and } = MS.core.logic;
const { eq, gre: gte, lte } = MS.core.rel;
const { macromolecular } = MS.struct.atomProperty;
const propTests: Partial<Record<string, Expression>> = {};
if (isDefined(row.label_entity_id)) {
propTests['entity-test'] = eq([macromolecular.label_entity_id(), row.label_entity_id]);
}
const chainTests: Expression[] = [];
if (isDefined(row.label_asym_id)) chainTests.push(eq([macromolecular.label_asym_id(), row.label_asym_id]));
if (isDefined(row.auth_asym_id)) chainTests.push(eq([macromolecular.auth_asym_id(), row.auth_asym_id]));
if (chainTests.length === 1) {
propTests['chain-test'] = chainTests[0];
} else if (chainTests.length > 1) {
propTests['chain-test'] = and(chainTests);
}
const residueTests: Expression[] = [];
if (isDefined(row.label_seq_id)) residueTests.push(eq([macromolecular.label_seq_id(), row.label_seq_id]));
if (isDefined(row.auth_seq_id)) residueTests.push(eq([macromolecular.auth_seq_id(), row.auth_seq_id]));
if (isDefined(row.pdbx_PDB_ins_code)) residueTests.push(eq([macromolecular.pdbx_PDB_ins_code(), row.pdbx_PDB_ins_code]));
if (isDefined(row.beg_label_seq_id)) residueTests.push(gte([macromolecular.label_seq_id(), row.beg_label_seq_id]));
if (isDefined(row.end_label_seq_id)) residueTests.push(lte([macromolecular.label_seq_id(), row.end_label_seq_id]));
if (isDefined(row.beg_auth_seq_id)) residueTests.push(gte([macromolecular.auth_seq_id(), row.beg_auth_seq_id]));
if (isDefined(row.end_auth_seq_id)) residueTests.push(lte([macromolecular.auth_seq_id(), row.end_auth_seq_id]));
if (residueTests.length === 1) {
propTests['residue-test'] = residueTests[0];
} else if (residueTests.length > 1) {
propTests['residue-test'] = and(residueTests);
}
const atomTests: Expression[] = [];
if (isDefined(row.atom_id)) atomTests.push(eq([macromolecular.id(), row.atom_id]));
if (isDefined(row.atom_index)) atomTests.push(eq([MS.struct.atomProperty.core.sourceIndex(), row.atom_index]));
if (isDefined(row.label_atom_id)) atomTests.push(eq([macromolecular.label_atom_id(), row.label_atom_id]));
if (isDefined(row.auth_atom_id)) atomTests.push(eq([macromolecular.auth_atom_id(), row.auth_atom_id]));
if (isDefined(row.type_symbol)) atomTests.push(eq([MS.struct.atomProperty.core.elementSymbol(), row.type_symbol.toUpperCase()]));
if (atomTests.length === 1) {
propTests['atom-test'] = atomTests[0];
} else if (atomTests.length > 1) {
propTests['atom-test'] = and(atomTests);
}
return MS.struct.generator.atomGroups(propTests);
return StructureElement.Schema.toExpression(row);
}
/** Convert multiple annotation rows into a MolScript expression.
* (with union semantics, i.e. an atom qualifies if it qualifies for at least one of the rows) */
export function rowsToExpression(rows: readonly MVSAnnotationRow[]): Expression {
if (rows.length === 1) return rowToExpression(rows[0]);
return unionExpression(rows.map(rowToExpression));
return StructureElement.Schema.toExpression({
items: rows as StructureElement.SchemaItem[]
});
}
/** Create MolScript expression covering the set union of the given expressions */
function unionExpression(expressions: Expression[]): Expression {
return MS.struct.combinator.merge(expressions.map(e => MS.struct.modifier.union([e])));
}
/** Data structure for an array divided into contiguous groups */
interface GroupedArray<T> {
/** Number of groups */

View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { MolstarLoadingExtension } from '../load';
export const IsHiddenCustomStateExtension: MolstarLoadingExtension<{}> = {
id: 'ww-pdb/is-hidden-custom-state',
description: 'Allow updating initial visibility of nodes',
createExtensionContext: () => ({}),
action: (updateTarget, node) => {
if (!node.custom || !node.custom?.is_hidden) return;
updateTarget.update.to(updateTarget.selector).updateState({ isHidden: true });
},
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -125,13 +125,16 @@ export interface UpdateTarget {
readonly selector: StateObjectSelector,
readonly targetManager: TargetManager,
readonly mvsDependencyRefs: Set<string>,
readonly transformer?: StateTransformer,
readonly transformParams?: any,
}
export const UpdateTarget = {
/** Create a new update, with `selector` pointing to the root. */
create(plugin: PluginContext, replaceExisting: boolean): UpdateTarget {
const update = plugin.build();
const msTarget = update.toRoot().selector;
return { update, selector: msTarget, targetManager: new TargetManager(plugin, replaceExisting), mvsDependencyRefs: new Set() };
const msTarget = update.toRoot();
return { update, selector: msTarget.selector, targetManager: new TargetManager(plugin, replaceExisting), mvsDependencyRefs: new Set() };
},
/** Add a child node to `target.selector`, return a new `UpdateTarget` pointing to the new child. */
apply<A extends StateObject, B extends StateObject, P extends {}>(target: UpdateTarget, transformer: StateTransformer<A, B, P>, params?: Partial<P>, options?: Partial<StateTransform.Options>): UpdateTarget {
@@ -141,8 +144,8 @@ export const UpdateTarget = {
refSuffix += `:${reprType}`;
}
const ref = target.targetManager.getChildRef(target.selector, refSuffix);
const msResult = target.update.to(target.selector).apply(transformer, params, { ...options, ref }).selector;
const result: UpdateTarget = { ...target, selector: msResult, mvsDependencyRefs: new Set() };
const apply = target.update.to(target.selector).apply(transformer, params, { ...options, ref });
const result: UpdateTarget = { ...target, selector: apply.selector, mvsDependencyRefs: new Set(), transformer, transformParams: params };
target.targetManager.allTargets.push(result);
return result;
},

View File

@@ -1,13 +1,15 @@
/**
* 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 { Mat3, Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { Volume } from '../../mol-model/volume';
import { StructureComponentParams } from '../../mol-plugin-state/helpers/structure-component';
import { StructureFromModel, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { StateTransformer } from '../../mol-state';
import { arrayDistinct } from '../../mol-util/array';
import { canonicalJsonString } from '../../mol-util/json';
@@ -27,6 +29,7 @@ import { MolstarLoadingContext } from './load';
import { Subtree, getChildren } from './tree/generic/tree-schema';
import { dfs, formatObject } from './tree/generic/tree-utils';
import { MolstarKind, MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree } from './tree/molstar/molstar-tree';
import { DefaultColor } from './tree/mvs/mvs-tree';
export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri'] satisfies MolstarKind[]);
@@ -35,10 +38,6 @@ export type AnnotationFromUriKind = ElementOfSet<typeof AnnotationFromUriKinds>
export const AnnotationFromSourceKinds = new Set(['color_from_source', 'component_from_source', 'label_from_source', 'tooltip_from_source'] satisfies MolstarKind[]);
export type AnnotationFromSourceKind = ElementOfSet<typeof AnnotationFromSourceKinds>
/** Color to be used e.g. for representations without 'color' node */
export const DefaultColor = 'white';
/** Return a 4x4 matrix representing a rotation followed by a translation */
export function transformFromRotationTranslation(rotation: number[] | null | undefined, translation: number[] | null | undefined): Mat4 {
if (rotation && rotation.length !== 9) throw new Error(`'rotation' param for 'transform' node must be array of 9 elements, found ${rotation}`);
@@ -285,19 +284,30 @@ export function componentFromXProps(node: MolstarNode<'component_from_uri' | 'co
/** Create props for `StructureRepresentation3D` transformer from a representation node. */
export function representationProps(node: MolstarSubtree<'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
const alpha = alphaForNode(node);
switch (node.params.type) {
const params = node.params;
switch (params.type) {
case 'cartoon':
return {
type: { name: 'cartoon', params: { alpha } },
type: { name: 'cartoon', params: { alpha, tubularHelices: params.tubular_helices } },
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
};
case 'ball_and_stick':
return {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.5, sizeAspectRatio: 0.5, alpha } },
type: { name: 'ball-and-stick', params: { sizeFactor: (params.size_factor ?? 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
};
case 'spacefill':
return {
type: { name: 'spacefill', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
};
case 'carbohydrate':
return {
type: { name: 'carbohydrate', params: { alpha, sizeFactor: params.size_factor ?? 1 } },
};
case 'surface':
return {
type: { name: 'molecular-surface', params: { alpha } },
sizeTheme: { name: 'physical', params: { scale: 1 } },
type: { name: 'molecular-surface', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
};
default:
throw new Error('NotImplementedError');
@@ -305,7 +315,7 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
}
/** Create value for `type.params.alpha` prop for `StructureRepresentation3D` transformer from a representation node based on 'opacity' nodes in its subtree. */
export function alphaForNode(node: MolstarSubtree<'representation'>): number {
export function alphaForNode(node: MolstarSubtree<'representation' | 'volume_representation'>): number {
const children = getChildren(node).filter(c => c.kind === 'opacity');
if (children.length > 0) {
return children[children.length - 1].params.opacity;
@@ -313,8 +323,14 @@ export function alphaForNode(node: MolstarSubtree<'representation'>): number {
return 1;
}
}
function hasMolStarUseDefaultColoring(node: MolstarNode): boolean {
if (!node.custom) return false;
return 'molstar_use_default_coloring' in node.custom || 'molstar_color_theme_name' in node.custom;
}
/** 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'] {
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | 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) {
@@ -322,12 +338,23 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
name: 'uniform',
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 ?? {},
};
} else if (children.length === 1 && appliesColorToWholeRepr(children[0])) {
return colorThemeForNode(children[0], context);
} else {
const layers: MultilayerColorThemeProps['layers'] = children.map(
c => ({ theme: colorThemeForNode(c, context), selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) })
);
c => {
const theme = colorThemeForNode(c, context);
if (!theme) return undefined;
return { theme, selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) };
}
).filter(t => !!t);
return {
name: MultilayerColorThemeName,
params: { layers },
@@ -389,3 +416,36 @@ export function makeNearestReprMap(root: MolstarTree) {
});
return map;
}
/** Create props for `VolumeRepresentation3D` transformer from a representation node. */
export function volumeRepresentationProps(node: MolstarSubtree<'volume_representation'>): Partial<StateTransformer.Params<VolumeRepresentation3D>> {
const alpha = alphaForNode(node);
const params = node.params;
switch (params.type) {
case 'isosurface':
const isoValue = typeof params.absolute_isovalue === 'number' ? Volume.IsoValue.absolute(params.absolute_isovalue) : Volume.IsoValue.relative(params.relative_isovalue ?? 0);
const visuals: ('wireframe' | 'solid')[] = [];
if (params.show_wireframe) visuals.push('wireframe');
if (params.show_faces) visuals.push('solid');
return {
type: { name: 'isosurface', params: { alpha, isoValue, visuals } },
};
default:
throw new Error('NotImplementedError');
}
}
/** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */
export function volumeColorThemeForNode(node: MolstarSubtree<'volume_representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<VolumeRepresentation3D>['colorTheme'] | undefined {
if (node?.kind !== 'volume_representation') return undefined;
const children = getChildren(node).filter(c => c.kind === 'color') as MolstarNode<'color'>[];
if (children.length === 0) {
return {
name: 'uniform',
params: { value: decodeColor(DefaultColor) },
};
} if (children.length === 1) {
return colorThemeForNode(children[0], context);
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -7,9 +7,11 @@
*/
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
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, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { ShapeRepresentation3D, StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { VolumeFromCcp4, VolumeFromDensityServerCif } from '../../mol-plugin-state/transforms/volume';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector } from '../../mol-state';
@@ -22,9 +24,10 @@ 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 } from './components/primitives';
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
import { LoadingActions, LoadingExtension, loadTree, loadTreeVirtual, UpdateTarget } from './load-generic';
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps } from './load-helpers';
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
import { MVSData, SnapshotMetadata } from './mvs-data';
import { validateTree } from './tree/generic/tree-schema';
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
@@ -32,13 +35,24 @@ import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTre
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
export interface MVSLoadOptions {
replaceExisting?: boolean,
keepCamera?: boolean,
keepSnapshotCamera?: boolean,
extensions?: MolstarLoadingExtension<any>[],
sanityChecks?: boolean,
sourceUrl?: string,
doNotReportErrors?: boolean
}
/** Load a MolViewSpec (MVS) tree into the Mol* plugin.
* If `options.replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state.
* If `options.keepCamera`, ignore any camera positioning from the MVS state and keep the current camera position instead.
* If `options.keepSnapshotCamera`, ignore any camera positioning when generating snapshots.
* If `options.sanityChecks`, run some sanity checks and print potential issues to the console.
* If `options.extensions` is provided, apply specified set of MVS-loading extensions (not a part of standard MVS specification); default: apply all builtin extensions; use `extensions: []` to avoid applying builtin extensions.
* `options.sourceUrl` serves as the base for resolving relative URLs/URIs and may itself be relative to the window URL. */
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[], sanityChecks?: boolean, sourceUrl?: string, doNotReportErrors?: boolean } = {}) {
export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
plugin.errorContext.clear('mvs');
try {
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
@@ -113,13 +127,15 @@ async function loadMolstarTree(plugin: PluginContext, tree: MolstarTree, options
}
}
function molstarTreeToEntry(plugin: PluginContext, tree: MolstarTree, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }, options?: { replaceExisting?: boolean, keepCamera?: boolean }) {
function molstarTreeToEntry(plugin: PluginContext, tree: MolstarTree, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }, options?: { replaceExisting?: boolean, keepCamera?: boolean, keepSnapshotCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
const context = MolstarLoadingContext.create();
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, options);
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { ...options, extensions: options?.extensions ?? BuiltinLoadingExtensions });
snapshot.canvas3d = {
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas) : undefined,
};
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
if (!options?.keepSnapshotCamera) {
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
}
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
const entryParams: PluginStateSnapshotManager.EntryParams = {
@@ -172,6 +188,8 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
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;
@@ -272,6 +290,22 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
colorTheme: colorThemeForNode(node, context),
});
},
volume(updateParent: UpdateTarget, node: MolstarNode<'volume'>): UpdateTarget | undefined {
if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Ccp4)) {
return UpdateTarget.apply(updateParent, VolumeFromCcp4, {});
} else if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Cif)) {
return UpdateTarget.apply(updateParent, VolumeFromDensityServerCif, { blockHeader: node.params.channel_id || undefined });
} else {
console.error(`Unsupported volume format`);
return undefined;
}
},
volume_representation(updateParent: UpdateTarget, node: MolstarNode<'volume_representation'>, context: MolstarLoadingContext): UpdateTarget {
return UpdateTarget.apply(updateParent, VolumeRepresentation3D, {
...volumeRepresentationProps(node),
colorTheme: volumeColorThemeForNode(node, context),
});
},
color: undefined, // No action needed, already loaded in `representation`
color_from_uri: undefined, // No action needed, already loaded in `representation`
color_from_source: undefined, // No action needed, already loaded in `representation`
@@ -321,4 +355,5 @@ export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<Molsta
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
NonCovalentInteractionsExtension,
IsHiddenCustomStateExtension,
];

View File

@@ -1,7 +1,8 @@
/**
* 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 { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
@@ -17,6 +18,7 @@ export const ParseFormatMvsToMolstar = {
mmcif: { format: 'cif', is_binary: false },
bcif: { format: 'cif', is_binary: true },
pdb: { format: 'pdb', is_binary: false },
map: { format: 'map', is_binary: true },
} satisfies { [p in ParseFormatT]: { format: MolstarParseFormatT, is_binary: boolean } };
@@ -71,6 +73,7 @@ const StructureFormatExtensions: Record<ParseFormatT, (FileExtension | '*')[]> =
mmcif: ['.cif', '.mmif'],
bcif: ['.bcif'],
pdb: ['.pdb', '.ent'],
map: ['.map', '.ccp4', '.mrc', '.mrcs'],
};
/** Run some sanity check on a MVSTree. Return a list of potential problems (`undefined` if there are none) */

View File

@@ -2,17 +2,29 @@
* Copyright (c) 2023-2024 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 { MVSData } from '../../../mvs-data';
import { builderDemo } from '../mvs-builder';
import { builderDemo, createMVSBuilder } from '../mvs-builder';
describe('mvs-builder', () => {
it('mvs-builder demo works', async () => {
it('mvs-builder demo works', () => {
const mvsData = builderDemo();
expect(typeof mvsData.metadata.version).toEqual('string');
expect(typeof mvsData.metadata.timestamp).toEqual('string');
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
});
it('volume builder works', () => {
const builder = createMVSBuilder();
builder
.download({ url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/box/-22.367,-33.367,-21.634/-7.106,-10.042,-0.937?detail=3' })
.parse({ format: 'bcif' })
.volume({ channel_id: '2FO-FC' })
.representation({ type: 'isosurface', absolute_isovalue: 1, show_wireframe: true, show_faces: false });
const state = builder.getState();
expect(state).toBeDefined();
});
});

View File

@@ -1,7 +1,8 @@
/**
* 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 { deepClone, pickObjectKeys } from '../../../../mol-util/object';
@@ -151,6 +152,10 @@ export class Parse extends _Base<'parse'> {
ref: params.ref,
}));
}
/** Add a 'volume' node representing raw volume data */
volume(params: MVSNodeParams<'volume'> & CustomAndRef = {}): Volume {
return new Volume(this._root, this.addChild('volume', params));
}
}
@@ -248,10 +253,37 @@ export class Representation extends _Base<'representation'> {
}
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
export class Volume extends _Base<'volume'> implements FocusMixin {
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
representation(params: Partial<MVSNodeParams<'volume_representation'>> & CustomAndRef = {}): VolumeRepresentation {
const fullParams: MVSNodeParams<'volume_representation'> = { ...params, type: params.type ?? 'isosurface' };
return new VolumeRepresentation(this._root, this.addChild('volume_representation', fullParams));
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
/** MVS builder pointing to a 'volume_representation' node */
export class VolumeRepresentation extends _Base<'volume_representation'> implements FocusMixin {
/** Add a 'color' node and return builder pointing back to the representation node. 'color' node instructs to apply color to a visual representation. */
color(params: MVSNodeParams<'color'> & CustomAndRef): VolumeRepresentation {
this.addChild('color', params);
return this;
}
/** Add an 'opacity' node and return builder pointing back to the representation node. 'opacity' node instructs to customize opacity/transparency of a visual representation. */
opacity(params: MVSNodeParams<'opacity'> & CustomAndRef): VolumeRepresentation {
this.addChild('opacity', params);
return this;
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
type MVSPrimitiveSubparams<TKind extends MVSNodeParams<'primitive'>['kind']> = Omit<Extract<MVSNodeParams<'primitive'>, { kind: TKind }>, 'kind'>;
/** MVS builder pointing to a 'primitives' node */
class Primitives extends _Base<'primitives'> implements FocusMixin {
export class Primitives extends _Base<'primitives'> implements FocusMixin {
/** Construct custom meshes/shapes in a low-level fashion by providing vertices and indices. */
mesh(params: MVSPrimitiveSubparams<'mesh'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'mesh', ...params });
@@ -267,6 +299,11 @@ class Primitives extends _Base<'primitives'> implements FocusMixin {
this.addChild('primitive', { kind: 'tube', ...params });
return this;
}
/** Defines an arrow. */
arrow(params: MVSPrimitiveSubparams<'arrow'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'arrow', ...params });
return this;
}
/** Defines a tube, connecting a start and an end point, with label containing distance between start and end. */
distance(params: MVSPrimitiveSubparams<'distance_measurement'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'distance_measurement', ...params });
@@ -277,6 +314,21 @@ class Primitives extends _Base<'primitives'> implements FocusMixin {
this.addChild('primitive', { kind: 'label', ...params });
return this;
}
/** Defines an ellipse. */
ellipse(params: MVSPrimitiveSubparams<'ellipse'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'ellipse', ...params });
return this;
}
/** Defines an ellipsoid */
ellipsoid(params: MVSPrimitiveSubparams<'ellipsoid'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'ellipsoid', ...params });
return this;
}
/** Defines a box. */
box(params: MVSPrimitiveSubparams<'box'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'box', ...params });
return this;
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
}

View File

@@ -5,9 +5,9 @@
* @author Adam Midlik <midlik@gmail.com>
*/
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str } from '../generic/field-schema';
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str, union } from '../generic/field-schema';
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
import { ColorT, FloatList, IntList, PrimitivePositionT } from './param-types';
import { ColorT, FloatList, IntList, PrimitivePositionT, Vector3 } from './param-types';
const _TubeBase = {
@@ -75,6 +75,39 @@ const TubeParams = {
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
};
const ArrowParams = {
/** Start point of the tube. */
start: RequiredField(PrimitivePositionT, 'Start point of the arrow.'),
/** End point of the tube. */
end: OptionalField(nullable(PrimitivePositionT), null, 'End point of the arrow.'),
/** If specified, the endpoint is computed as start + direction. */
direction: OptionalField(nullable(Vector3), null, 'If specified, the endpoint is computed as start + direction.'),
/** Length of the arrow. If unset, the distance between start and end is used. */
length: OptionalField(nullable(float), null, 'Length of the arrow. If unset, the distance between start and end is used.'),
/** Draw a cap at the start of the arrow. */
show_start_cap: OptionalField(bool, false, 'Draw a cap at the start of the arrow.'),
/** Length of the start cap. */
start_cap_length: OptionalField(float, 0.1, 'Length of the start cap.'),
/** Radius of the start cap. */
start_cap_radius: OptionalField(float, 0.1, 'Radius of the start cap.'),
/** Draw an arrow at the end of the arrow. */
show_end_cap: OptionalField(bool, false, 'Draw a cap at the end of the arrow.'),
/** Height of the arrow at the end. */
end_cap_length: OptionalField(float, 0.1, 'Length of the end cap.'),
/** Radius of the arrow at the end. */
end_cap_radius: OptionalField(float, 0.1, 'Radius of the end cap.'),
/** Draw a tube connecting the start and end points. */
show_tube: OptionalField(bool, true, 'Draw a tube connecting the start and end points.'),
/** Tube radius (in Angstroms). */
tube_radius: OptionalField(float, 0.05, 'Tube radius (in Angstroms).'),
/** Length of each dash and gap between dashes. If not specified (null), draw full line. */
tube_dash_length: OptionalField(nullable(float), null, 'Length of each dash and gap between dashes. If not specified (null), draw full line.'),
/** Color of the tube. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the tube. If not specified, uses the parent primitives group `color`.'),
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the arrow. If not specified, uses the parent primitives group `tooltip`.'),
};
const DistanceMeasurementParams = {
..._TubeBase,
/** Template used to construct the label. Use {{distance}} as placeholder for the distance. */
@@ -89,6 +122,37 @@ const DistanceMeasurementParams = {
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
};
const AngleMeasurementParams = {
/** Point A. */
a: RequiredField(PrimitivePositionT, 'Point A.'),
/** Point B. */
b: RequiredField(PrimitivePositionT, 'Point B.'),
/** Point C. */
c: RequiredField(PrimitivePositionT, 'Point C.'),
/** Template used to construct the label. Use {{angle}} as placeholder for the angle in radians. */
label_template: OptionalField(str, '{{angle}}', 'Template used to construct the label. Use {{angle}} as placeholder for the angle in radians.'),
/** Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min). */
label_size: OptionalField(nullable(float), null, 'Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min).'),
/** Scaling factor for relative size. */
label_auto_size_scale: OptionalField(float, 0.33, 'Scaling factor for relative size.'),
/** Minimum size for relative size. */
label_auto_size_min: OptionalField(float, 0, 'Minimum size for relative size.'),
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
/** Draw vectors between (a, b) and (b, c). */
show_vector: OptionalField(bool, true, 'Draw vectors between (a, b) and (b, c).'),
/** Color of the vectors. */
vector_color: OptionalField(nullable(ColorT), null, 'Color of the vectors.'),
/** Draw a filled circle section representing the angle. */
show_section: OptionalField(bool, true, 'Draw a filled circle section representing the angle.'),
/** Color of the angle section. If not specified, the primitives group color is used. */
section_color: OptionalField(nullable(ColorT), null, 'Color of the angle section. If not specified, the primitives group color is used.'),
/** Radius of the angle section. In angstroms. */
section_radius: OptionalField(nullable(float), null, 'Radius of the angle section. In angstroms.'),
/** Factor to scale the radius of the angle section. Ignored if section_radius is set. */
section_radius_scale: OptionalField(float, 0.33, 'Factor to scale the radius of the angle section. Ignored if section_radius is set.'),
};
const PrimitiveLabelParams = {
/** Position of this label. */
position: RequiredField(PrimitivePositionT, 'Position of this label.'),
@@ -102,6 +166,73 @@ const PrimitiveLabelParams = {
label_offset: OptionalField(float, 0, 'Camera-facing offset to prevent overlap with geometry.'),
};
const EllipseParams = {
/** Color of the primitive. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the ellipse. If not specified, uses the parent primitives group `color`.'),
/** If true, ignores radius_minor/magnitude of the minor axis */
as_circle: OptionalField(bool, false, 'If true, ignores radius_minor/magnitude of the minor axis.'),
/** ellipse center. */
center: RequiredField(PrimitivePositionT, 'The center of the ellipse.'),
/** Major axis of this ellipse. */
major_axis: OptionalField(nullable(Vector3), null, 'Major axis of this ellipse.'),
/** Minor axis of this ellipse. */
minor_axis: OptionalField(nullable(Vector3), null, 'Minor axis of this ellipse.'),
/** Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center. */
major_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center.'),
/** Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center. */
minor_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center.'),
/** Radius of the major axis. If unset, the length of the major axis is used. */
radius_major: OptionalField(nullable(float), null, 'Radius of the major axis. If unset, the length of the major axis is used.'),
/** Radius of the minor axis. If unset, the length of the minor axis is used. */
radius_minor: OptionalField(nullable(float), null, 'Radius of the minor axis. If unset, the length of the minor axis is used.'),
/** Start of the arc. In radians */
theta_start: OptionalField(float, 0, 'Start of the arc. In radians'),
/** End of the arc. In radians */
theta_end: OptionalField(float, 2 * Math.PI, 'End of the arc. In radians'),
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
};
const EllipsoidParams = {
/** Color of the primitive. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the ellipsoid. If not specified, uses the parent primitives group `color`.'),
/** Ellipsoid center. */
center: RequiredField(PrimitivePositionT, 'The center of the ellipsoid.'),
/** Major axis of this ellipsoid. */
major_axis: OptionalField(nullable(Vector3), null, 'Major axis of this ellipsoid.'),
/** Minor axis of this ellipsoid. */
minor_axis: OptionalField(nullable(Vector3), null, 'Minor axis of this ellipsoid.'),
/** Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center. */
major_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Major axis endpoint. If specified, overrides major axis to be major_axis_endpoint - center.'),
/** Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center. */
minor_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center.'),
/** Radii of the ellipsoid along each axis. */
radius: OptionalField(nullable(union([Vector3, float])), null, 'Radii of the ellipsoid along each axis.'),
/** Added to the radii of the ellipsoid along each axis. */
radius_extent: OptionalField(nullable(union([Vector3, float])), null, 'Added to the radii of the ellipsoid along each axis.'),
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
};
const BoxParams = {
/** The center of the box. */
center: RequiredField(PrimitivePositionT, 'The center of the box.'),
/** The width, the height, and the depth of the box. Added to the bounding box determined by the center. */
extent: OptionalField(nullable(Vector3), null, 'The width, the height, and the depth of the box. Added to the bounding box determined by the center.'),
/** Determine whether to render the faces of the box. */
show_faces: OptionalField(bool, true, 'Determine whether to render the faces of the box.'),
/** Color of the box faces. */
face_color: OptionalField(nullable(ColorT), null, 'Color of the box faces.'),
/** Determine whether to render the edges of the box. */
show_edges: OptionalField(bool, false, 'Determine whether to render the edges of the box.'),
/** Radius of the box edges. In angstroms. */
edge_radius: OptionalField(float, 0.1, 'Radius of the box edges. In angstroms.'),
/** Color of the box edges. */
edge_color: OptionalField(nullable(ColorT), null, 'Color of the edges.'),
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
};
export const MVSPrimitiveParams = UnionParamsSchema(
'kind',
'Kind of geometrical primitive',
@@ -109,7 +240,12 @@ export const MVSPrimitiveParams = UnionParamsSchema(
'mesh': SimpleParamsSchema(MeshParams),
'lines': SimpleParamsSchema(LinesParams),
'tube': SimpleParamsSchema(TubeParams),
'arrow': SimpleParamsSchema(ArrowParams),
'distance_measurement': SimpleParamsSchema(DistanceMeasurementParams),
'angle_measurement': SimpleParamsSchema(AngleMeasurementParams),
'label': SimpleParamsSchema(PrimitiveLabelParams),
'ellipse': SimpleParamsSchema(EllipseParams),
'ellipsoid': SimpleParamsSchema(EllipsoidParams),
'box': SimpleParamsSchema(BoxParams),
},
);

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { bool, float, nullable, OptionalField } from '../generic/field-schema';
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
const Cartoon = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
/** Simplify corkscrew helices to tubes. */
tubular_helices: OptionalField(bool, false, 'Simplify corkscrew helices to tubes.'),
};
const BallAndStick = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
/** Controls whether hydrogen atoms are drawn. */
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
};
const Spacefill = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
/** Controls whether hydrogen atoms are drawn. */
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
};
const Carbohydrate = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
};
const Surface = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
/** Controls whether hydrogen atoms are drawn. */
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
};
export const MVSRepresentationParams = UnionParamsSchema(
'type',
'Representation type',
{
cartoon: SimpleParamsSchema(Cartoon),
ball_and_stick: SimpleParamsSchema(BallAndStick),
spacefill: SimpleParamsSchema(Spacefill),
carbohydrate: SimpleParamsSchema(Carbohydrate),
surface: SimpleParamsSchema(Surface),
},
);
const VolumeIsoSurface = {
/** Relative isovalue. */
relative_isovalue: OptionalField(nullable(float), null, 'Relative isovalue.'),
/** Absolute isovalue. Overrides `relative_isovalue`. */
absolute_isovalue: OptionalField(nullable(float), null, 'Absolute isovalue. Overrides `relative_isovalue`.'),
/** Show mesh wireframe. Defaults to false. */
show_wireframe: OptionalField(bool, false, 'Show mesh wireframe. Defaults to false.'),
/** Show mesh faces. Defaults to true. */
show_faces: OptionalField(bool, true, 'Show mesh faces. Defaults to true.'),
};
export const MVSVolumeRepresentationParams = UnionParamsSchema(
'type',
'Representation type',
{
'isosurface': SimpleParamsSchema(VolumeIsoSurface),
},
);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -8,8 +8,9 @@
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/field-schema';
import { SimpleParamsSchema } from '../generic/params-schema';
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
import { MVSRepresentationParams, MVSVolumeRepresentationParams } from './mvs-tree-representations';
import { MVSPrimitiveParams } from './mvs-tree-primitives';
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, RepresentationTypeT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
const _DataFromUriParams = {
@@ -42,6 +43,9 @@ const _DataFromSourceParams = {
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
};
/** Color to be used e.g. for representations without 'color' node */
export const DefaultColor = 'white';
/** Schema for `MVSTree` (MolViewSpec tree) */
export const MVSTreeSchema = TreeSchema({
rootKind: 'root',
@@ -142,18 +146,29 @@ export const MVSTreeSchema = TreeSchema({
representation: {
description: 'This node instructs to create a visual representation of a component.',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: MVSRepresentationParams,
},
/** This node instructs to create a volume from a parsed data resource. "Volume" refers to an internal representation of volumetric data without any visual representation. */
volume: {
description: 'This node instructs to create a volume from a parsed data resource. "Volume" refers to an internal representation of volumetric data without any visual representation.',
parent: ['parse'],
params: SimpleParamsSchema({
/** Method of visual representation of the component. */
type: RequiredField(RepresentationTypeT, 'Method of visual representation of the component.'),
channel_id: OptionalField(nullable(str), null, 'Channel identifier (only applies when the input data contain multiple channels).'),
}),
},
/** This node instructs to create a visual representation of a volume. */
volume_representation: {
description: 'This node instructs to create a visual representation of a volume.',
parent: ['volume'],
params: MVSVolumeRepresentationParams,
},
/** This node instructs to apply color to a visual representation. */
color: {
description: 'This node instructs to apply color to a visual representation.',
parent: ['representation'],
parent: ['representation', 'volume_representation'],
params: SimpleParamsSchema({
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
color: RequiredField(ColorT, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
color: OptionalField(ColorT, DefaultColor, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
/** Defines to what part of the representation this color should be applied. */
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'all', 'Defines to what part of the representation this color should be applied.'),
}),
@@ -181,7 +196,7 @@ export const MVSTreeSchema = TreeSchema({
/** This node instructs to apply opacity/transparency to a visual representation. */
opacity: {
description: 'This node instructs to apply opacity/transparency to a visual representation.',
parent: ['representation'],
parent: ['representation', 'volume_representation'],
params: SimpleParamsSchema({
/** Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque. */
opacity: RequiredField(float, 'Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque.'),
@@ -248,7 +263,7 @@ export const MVSTreeSchema = TreeSchema({
/** This node instructs to set the camera focus to a component (zoom in). */
focus: {
description: 'This node instructs to set the camera focus to a component (zoom in).',
parent: ['root', 'component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri'],
parent: ['root', 'component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri', 'volume', 'volume_representation'],
params: SimpleParamsSchema({
/** Vector describing the direction of the view (camera position -> focused target). */
direction: OptionalField(Vector3, [0, 0, -1], 'Vector describing the direction of the view (camera position -> focused target).'),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -12,18 +12,18 @@ import { ColorNames } from '../../../../mol-util/color/names';
/** `format` parameter values for `parse` node in MVS tree */
export const ParseFormatT = literal('mmcif', 'bcif', 'pdb');
export const ParseFormatT = literal('mmcif', 'bcif', 'pdb', 'map');
export type ParseFormatT = ValueFor<typeof ParseFormatT>
/** `format` parameter values for `parse` node in Molstar tree */
export const MolstarParseFormatT = literal('cif', 'pdb');
export const MolstarParseFormatT = literal('cif', 'pdb', 'map');
export type MolstarParseFormatT = ValueFor<typeof MolstarParseFormatT>
/** `kind` parameter values for `structure` node in MVS tree */
export const StructureTypeT = literal('model', 'assembly', 'symmetry', 'symmetry_mates');
/** `selector` parameter values for `component` node in MVS tree */
export const ComponentSelectorT = literal('all', 'polymer', 'protein', 'nucleic', 'branched', 'ligand', 'ion', 'water');
export const ComponentSelectorT = literal('all', 'polymer', 'protein', 'nucleic', 'branched', 'ligand', 'ion', 'water', 'coarse');
/** `selector` parameter values for `component` node in MVS tree */
export const ComponentExpressionT = iots.partial({
@@ -45,9 +45,6 @@ export const ComponentExpressionT = iots.partial({
});
export type ComponentExpressionT = ValueFor<typeof ComponentExpressionT>
/** `type` parameter values for `representation` node in MVS tree */
export const RepresentationTypeT = literal('ball_and_stick', 'cartoon', 'surface');
/** `schema` parameter values for `*_from_uri` and `*_from_source` nodes in MVS tree */
export const SchemaT = literal('whole_structure', 'entity', 'chain', 'auth_chain', 'residue', 'auth_residue', 'residue_range', 'auth_residue_range', 'atom', 'auth_atom', 'all_atomic');
@@ -64,7 +61,7 @@ export const Matrix = list(float);
/** Primitives-related types */
export const PrimitiveComponentExpressionT = iots.partial({ structure_ref: str, expression_schema: SchemaT, expressions: list(ComponentExpressionT) });
export type PrimitiveComponentExpressionT = ValueFor<typeof PrimitiveComponentExpressionT>
export const PrimitivePositionT = iots.union([Vector3, ComponentExpressionT, list(PrimitiveComponentExpressionT)]);
export const PrimitivePositionT = iots.union([Vector3, ComponentExpressionT, PrimitiveComponentExpressionT]);
export type PrimitivePositionT = ValueFor<typeof PrimitivePositionT>
export const FloatList = list(float);
@@ -93,6 +90,7 @@ export const ColorNamesT = literal(...Object.keys(ColorNames) as (keyof ColorNam
/** `color` parameter values for `color` node in MVS tree */
export const ColorT = union([ColorNameT, HexColorT]);
export type ColorT = ValueFor<typeof ColorT>
/** Type helpers */
export function isVector3(x: any): x is Vector3 {

View 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 { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector, StateTree } from '../../mol-state';
/**
* Queries all MolViewSpec references in the current state of the plugin.
*/
export function queryMVSRef(plugin: PluginContext, ref: string) {
return plugin.state.data.selectQ(q => q.root.subtree().withTag(`mvs-ref:${ref}`));
}
/**
* Creates a mapping of all MolViewSpec references in the current state of the plugin.
*/
export function createMVSRefMap(plugin: PluginContext) {
const tree = plugin.state.data.tree;
const mapping = new Map<string, StateObjectSelector[]>();
StateTree.doPreOrder(tree, tree.root, { mapping, plugin }, (n, _, s) => {
if (!n.tags) return;
for (const tag of n.tags) {
if (!tag.startsWith('mvs-ref:')) continue;
const mvsRef = tag.substring(8);
const selector = new StateObjectSelector(n.ref, s.plugin.state.data);
if (s.mapping.has(mvsRef)) s.mapping.get(mvsRef)!.push(selector);
else s.mapping.set(mvsRef, [selector]);
}
});
return mapping;
}

View File

@@ -13,6 +13,7 @@ import { Color } from '../../../mol-util/color';
import { TableLegend } from '../../../mol-util/legend';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
const ValidationColors = [
Color.fromRgb(170, 170, 170), // not applicable
@@ -90,7 +91,7 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params, 'pdbe-structure-quality-report'> = {
name: 'pdbe-structure-quality-report',
label: 'Structure Quality Report',
category: ColorTheme.Category.Validation,
category: ColorThemeCategory.Validation,
factory: StructureQualityReportColorTheme,
getParams: ctx => {
const issueTypes = StructureQualityReport.getIssueTypes(ctx.structure);

View File

@@ -12,6 +12,7 @@ import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-mod
import { Location } from '../../../../mol-model/location';
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
import { ValidationReportProvider, ValidationReport } from '../prop';
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
const DefaultColor = Color(0xCCCCCC);
@@ -70,7 +71,7 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
export const DensityFitColorThemeProvider: ColorTheme.Provider<{}, ValidationReport.Tag.DensityFit> = {
name: ValidationReport.Tag.DensityFit,
label: 'Density Fit',
category: ColorTheme.Category.Validation,
category: ColorThemeCategory.Validation,
factory: DensityFitColorTheme,
getParams: () => ({}),
defaultValues: PD.getDefaultValues({}),

View File

@@ -15,6 +15,7 @@ import { ValidationReportProvider, ValidationReport } from '../prop';
import { TableLegend } from '../../../../mol-util/legend';
import { PolymerType } from '../../../../mol-model/structure/model/types';
import { SetUtils } from '../../../../mol-util/set';
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
const DefaultColor = Color(0x909090);
@@ -108,7 +109,7 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQualityColorThemeParams, ValidationReport.Tag.GeometryQuality> = {
name: ValidationReport.Tag.GeometryQuality,
label: 'Geometry Quality',
category: ColorTheme.Category.Validation,
category: ColorThemeCategory.Validation,
factory: GeometryQualityColorTheme,
getParams: getGeometricQualityColorThemeParams,
defaultValues: PD.getDefaultValues(getGeometricQualityColorThemeParams({})),

View File

@@ -12,6 +12,7 @@ import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-mod
import { Location } from '../../../../mol-model/location';
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
import { ValidationReportProvider, ValidationReport } from '../prop';
import { ColorThemeCategory } from '../../../../mol-theme/color/categories';
const DefaultColor = Color(0xCCCCCC);
@@ -61,7 +62,7 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}, ValidationReport.Tag.RandomCoilIndex> = {
name: ValidationReport.Tag.RandomCoilIndex,
label: 'Random Coil Index',
category: ColorTheme.Category.Validation,
category: ColorThemeCategory.Validation,
factory: RandomCoilIndexColorTheme,
getParams: () => ({}),
defaultValues: PD.getDefaultValues({}),

View File

@@ -6,6 +6,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Location } from '../../../mol-model/location';
import { SbNcbrPartialChargesPropertyProvider } from './property';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
const Colors = {
Bond: Color(0xffffff),
@@ -133,7 +134,7 @@ PartialChargesThemeParams,
> = {
label: 'SB NCBR Partial Charges',
name: 'sb-ncbr-partial-charges',
category: ColorTheme.Category.Atom,
category: ColorThemeCategory.Atom,
factory: PartialChargesColorTheme,
getParams: getPartialChargesThemeParams,
defaultValues: PD.getDefaultValues(PartialChargesThemeParams),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 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 Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -1070,7 +1070,10 @@ namespace Canvas3D {
if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
if (props.pickPadding !== undefined) p.pickPadding = props.pickPadding;
if (props.pickPadding !== undefined) {
p.pickPadding = props.pickPadding;
pickHelper.setPickPadding(p.pickPadding);
}
if (props.userInteractionReleaseMs !== undefined) p.userInteractionReleaseMs = props.userInteractionReleaseMs;
if (props.viewport !== undefined) {
const doNotUpdate = p.viewport === props.viewport ||

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 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>
*/
@@ -247,6 +247,18 @@ export class PickHelper {
setViewport(x: number, y: number, width: number, height: number) {
Viewport.set(this.viewport, x, y, width, height);
this.update();
}
setPickPadding(pickPadding: number) {
if (this.pickPadding !== pickPadding) {
this.pickPadding = pickPadding;
this.update();
}
}
private update() {
const { x, y, width, height } = this.viewport;
this.pickRatio = this.pickPass.pickRatio;
this.pickX = Math.ceil(x * this.pickRatio);
@@ -263,7 +275,8 @@ export class PickHelper {
this.setupBuffers();
}
this.spiral = spiral2d(Math.round(this.pickRatio * this.pickPadding));
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
this.dirty = true;
}
private syncBuffers() {
@@ -324,6 +337,10 @@ export class PickHelper {
}
private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
if (this.pickRatio !== this.pickPass.pickRatio) {
this.update();
}
const { webgl, pickRatio } = this;
if (webgl.isContextLost) return;
@@ -392,7 +409,7 @@ export class PickHelper {
}
}
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, readonly pickPadding = 1) {
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, private pickPadding = 1) {
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -130,6 +130,7 @@ export namespace BaseGeometry {
uClipObjectPosition: ValueCell.create(clip.objects.position),
uClipObjectRotation: ValueCell.create(clip.objects.rotation),
uClipObjectScale: ValueCell.create(clip.objects.scale),
uClipObjectTransform: ValueCell.create(clip.objects.transform),
instanceGranularity: ValueCell.create(props.instanceGranularity),
uLod: ValueCell.create(Vec4.create(props.lod[0], props.lod[1], props.lod[2], 0)),
@@ -152,6 +153,7 @@ export namespace BaseGeometry {
ValueCell.update(values.uClipObjectPosition, clip.objects.position);
ValueCell.update(values.uClipObjectRotation, clip.objects.rotation);
ValueCell.update(values.uClipObjectScale, clip.objects.scale);
ValueCell.update(values.uClipObjectTransform, clip.objects.transform);
ValueCell.updateIfChanged(values.instanceGranularity, props.instanceGranularity);
ValueCell.update(values.uLod, Vec4.set(values.uLod.ref.value, props.lod[0], props.lod[1], props.lod[2], 0));

View File

@@ -1,5 +1,5 @@
/**
* 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 Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -24,6 +24,8 @@ export type ColorData = {
uColor: ValueCell<Vec3>,
tColor: ValueCell<TextureImage<Uint8Array>>,
tColorGrid: ValueCell<Texture>,
uPaletteDomain: ValueCell<Vec2>,
uPaletteDefault: ValueCell<Vec3>,
tPalette: ValueCell<TextureImage<Uint8Array>>,
uColorTexDim: ValueCell<Vec2>,
uColorGridDim: ValueCell<Vec3>,
@@ -36,6 +38,9 @@ export function createColors(locationIt: LocationIterator, positionIt: LocationI
const data = _createColors(locationIt, positionIt, colorTheme, colorData);
if (colorTheme.palette) {
ValueCell.updateIfChanged(data.dUsePalette, true);
const [min, max] = colorTheme.palette.domain || [0, 1];
ValueCell.update(data.uPaletteDomain, Vec2.set(data.uPaletteDomain.ref.value, min, max));
ValueCell.update(data.uPaletteDefault, Color.toVec3Normalized(data.uPaletteDefault.ref.value, colorTheme.palette.defaultColor ?? Color(0xCCCCCC)));
updatePaletteTexture(colorTheme.palette, data.tPalette);
} else {
ValueCell.updateIfChanged(data.dUsePalette, false);
@@ -103,6 +108,8 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
uColor: ValueCell.create(Color.toVec3Normalized(Vec3(), value)),
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
tColorGrid: ValueCell.create(createNullTexture()),
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
uPaletteDefault: ValueCell.create(Vec3()),
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
@@ -131,6 +138,8 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color
uColor: ValueCell.create(Vec3()),
tColor: ValueCell.create(colors),
tColorGrid: ValueCell.create(createNullTexture()),
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
uPaletteDefault: ValueCell.create(Vec3()),
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
uColorTexDim: ValueCell.create(Vec2.create(colors.width, colors.height)),
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
@@ -233,6 +242,8 @@ export function createGridColor(grid: ColorVolume, type: ColorType, colorData?:
uColor: ValueCell.create(Vec3()),
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
tColorGrid: ValueCell.create(colors),
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
uPaletteDefault: ValueCell.create(Vec3()),
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
uColorTexDim: ValueCell.create(Vec2.create(width, height)),
uColorGridDim: ValueCell.create(Vec3.clone(dimension)),
@@ -255,6 +266,8 @@ function createDirectColor(colorData?: ColorData): ColorData {
uColor: ValueCell.create(Vec3()),
tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
tColorGrid: ValueCell.create(createNullTexture()),
uPaletteDomain: ValueCell.create(Vec2.create(0, 1)),
uPaletteDefault: ValueCell.create(Vec3()),
tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -214,7 +214,7 @@ export namespace DirectVolume {
const { bboxSize, bboxMin, bboxMax, gridDimension, transform: gridTransform } = directVolume;
const { instanceCount, groupCount } = locationIt;
const positionIt = Utils.createPositionIterator(directVolume, transform);
const positionIt = createPositionIterator(directVolume, transform);
const color = createColors(locationIt, positionIt, theme.color);
const marker = props.instanceGranularity

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -23,6 +23,7 @@ import { RenderObjectValues } from '../../mol-gl/render-object';
import { TextureMesh } from './texture-mesh/texture-mesh';
import { Image } from './image/image';
import { Cylinders } from './cylinders/cylinders';
import { arrayMaxPackedIntToRGB } from '../../mol-util/number-packing';
export type GeometryKind = 'mesh' | 'points' | 'spheres' | 'cylinders' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
@@ -105,7 +106,7 @@ export namespace Geometry {
case 'direct-volume':
return 1;
case 'image':
return arrayMax(geometry.groupTexture.ref.value.array) + 1;
return arrayMaxPackedIntToRGB(geometry.groupTexture.ref.value.array, 4) + 1;
case 'texture-mesh':
return geometry.groupCount;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -9,7 +9,7 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator';
import { RenderableState } from '../../../mol-gl/renderable';
import { calculateTransformBoundingSphere, createTextureImage, TextureImage } from '../../../mol-gl/renderable/util';
import { Sphere3D } from '../../../mol-math/geometry';
import { Vec2, Vec4, Vec3 } from '../../../mol-math/linear-algebra';
import { Vec2, Vec4, Vec3, Quat, Mat4 } from '../../../mol-math/linear-algebra';
import { Theme } from '../../../mol-theme/theme';
import { ValueCell } from '../../../mol-util';
import { Color } from '../../../mol-util/color';
@@ -59,16 +59,37 @@ interface Image {
readonly imageTextureDim: ValueCell<Vec2>,
readonly cornerBuffer: ValueCell<Float32Array>,
readonly groupTexture: ValueCell<TextureImage<Uint8Array>>,
readonly valueTexture: ValueCell<TextureImage<Float32Array>>,
readonly trimType: ValueCell<number>,
readonly trimCenter: ValueCell<Vec3>,
readonly trimRotation: ValueCell<Quat>,
readonly trimScale: ValueCell<Vec3>,
readonly trimTransform: ValueCell<Mat4>,
readonly isoLevel: ValueCell<number>,
/** Bounding sphere of the image */
boundingSphere: Sphere3D
}
namespace Image {
export function create(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, image?: Image): Image {
export type Trim = {
type: 0 | 1 | 2 | 3 | 4 | 5,
center: Vec3,
rotation: Quat,
scale: Vec3,
transform: Mat4,
}
export function createEmptyTrim(): Trim {
return { type: 0, center: Vec3(), rotation: Quat(), scale: Vec3(), transform: Mat4() };
}
export function create(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, valueTexture: TextureImage<Float32Array>, trim: Trim, isoLevel: number, image?: Image): Image {
return image ?
update(imageTexture, corners, groupTexture, image) :
fromData(imageTexture, corners, groupTexture);
update(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image) :
fromData(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel);
}
function hashCode(image: Image) {
@@ -77,7 +98,7 @@ namespace Image {
]);
}
function fromData(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>): Image {
function fromData(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, valueTexture: TextureImage<Float32Array>, trim: Trim, isoLevel: number): Image {
const boundingSphere = Sphere3D();
let currentHash = -1;
@@ -90,6 +111,13 @@ namespace Image {
imageTextureDim: ValueCell.create(Vec2.create(width, height)),
cornerBuffer: ValueCell.create(corners),
groupTexture: ValueCell.create(groupTexture),
valueTexture: ValueCell.create(valueTexture),
trimType: ValueCell.create(trim.type),
trimCenter: ValueCell.create(trim.center),
trimRotation: ValueCell.create(trim.rotation),
trimScale: ValueCell.create(trim.scale),
trimTransform: ValueCell.create(trim.transform),
isoLevel: ValueCell.create(isoLevel),
get boundingSphere() {
const newHash = hashCode(image);
if (newHash !== currentHash) {
@@ -103,7 +131,7 @@ namespace Image {
return image;
}
function update(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, image: Image): Image {
function update(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, valueTexture: TextureImage<Float32Array>, trim: Trim, isoLevel: number, image: Image): Image {
const width = imageTexture.width;
const height = imageTexture.height;
@@ -111,6 +139,15 @@ namespace Image {
ValueCell.update(image.imageTextureDim, Vec2.set(image.imageTextureDim.ref.value, width, height));
ValueCell.update(image.cornerBuffer, corners);
ValueCell.update(image.groupTexture, groupTexture);
ValueCell.update(image.valueTexture, valueTexture);
ValueCell.updateIfChanged(image.trimType, trim.type);
ValueCell.update(image.trimCenter, Vec3.copy(image.trimCenter.ref.value, trim.center));
ValueCell.update(image.trimRotation, Quat.copy(image.trimRotation.ref.value, trim.rotation));
ValueCell.update(image.trimScale, Vec3.copy(image.trimScale.ref.value, trim.scale));
ValueCell.update(image.trimTransform, Mat4.copy(image.trimTransform.ref.value, trim.transform));
ValueCell.updateIfChanged(image.isoLevel, isoLevel);
return image;
}
@@ -118,7 +155,9 @@ namespace Image {
const imageTexture = createTextureImage(0, 4, Uint8Array);
const corners = image ? image.cornerBuffer.ref.value : new Float32Array(8 * 3);
const groupTexture = createTextureImage(0, 4, Uint8Array);
return create(imageTexture, corners, groupTexture, image);
const valueTexture = createTextureImage(0, 1, Float32Array);
const trim = createEmptyTrim();
return create(imageTexture, corners, groupTexture, valueTexture, trim, -1, image);
}
export const Params = {
@@ -136,12 +175,16 @@ namespace Image {
updateBoundingSphere,
createRenderableState,
updateRenderableState,
createPositionIterator: () => LocationIterator(1, 1, 1, () => NullLocation)
createPositionIterator
};
function createPositionIterator(_image: Image, _transform: TransformData): LocationIterator {
return LocationIterator(1, 1, 1, () => NullLocation);
}
function createValues(image: Image, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): ImageValues {
const { instanceCount, groupCount } = locationIt;
const positionIt = Utils.createPositionIterator(image, transform);
const positionIt = createPositionIterator(image, transform);
const color = createColors(locationIt, positionIt, theme.color);
const marker = props.instanceGranularity
@@ -186,6 +229,15 @@ namespace Image {
uImageTexDim: image.imageTextureDim,
tImageTex: image.imageTexture,
tGroupTex: image.groupTexture,
tValueTex: image.valueTexture,
uTrimType: image.trimType,
uTrimCenter: image.trimCenter,
uTrimRotation: image.trimRotation,
uTrimScale: image.trimScale,
uTrimTransform: image.trimTransform,
uIsoLevel: image.isoLevel,
};
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3, Mat4 } from '../../../../mol-math/linear-algebra';
import { SegmentedPlane } from '../../../primitive/plane';
import { MeshBuilder } from '../mesh-builder';
const tmpPlaneMat = Mat4.identity();
const tmpVec = Vec3();
function setPlaneMat(m: Mat4, center: Vec3, dirMajor: Vec3, dirMinor: Vec3, scale: Vec3) {
Vec3.add(tmpVec, center, dirMajor);
Mat4.targetTo(m, center, tmpVec, dirMinor);
Mat4.setTranslation(m, center);
Mat4.mul(m, m, Mat4.rotY90);
return Mat4.scale(m, m, scale);
}
export function addPlane(state: MeshBuilder.State, center: Vec3, dirMajor: Vec3, dirMinor: Vec3, scale: Vec3, widthSegments: number, heightSegments: number) {
const plane = SegmentedPlane(widthSegments, heightSegments);
MeshBuilder.addPrimitive(state, setPlaneMat(tmpPlaneMat, center, dirMajor, dirMinor, scale), plane);
}

View File

@@ -1,5 +1,5 @@
/**
* 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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -37,4 +37,53 @@ export function Plane(): Primitive {
export function PlaneCage(): Cage {
return planeCage;
}
}
//
export function SegmentedPlane(widthSegments: number, heightSegments: number): Primitive {
const widthSegments1 = widthSegments + 1;
const heightSegments1 = heightSegments + 1;
const segmentWidth = 1 / widthSegments;
const segmentHeight = 1 / heightSegments;
const vertices = new Float32Array(widthSegments1 * heightSegments1 * 3);
const normals = new Float32Array(widthSegments1 * heightSegments1 * 3);
const indices = new Uint32Array(widthSegments * heightSegments * 6);
let i = 0;
for (let iy = 0; iy < heightSegments1; ++iy) {
const y = iy * segmentHeight - 0.5;
for (let ix = 0; ix < widthSegments1; ++ix) {
const x = ix * segmentWidth - 0.5;
vertices[i] = x;
vertices[i + 1] = -y;
vertices[i + 2] = 0;
normals[i] = 0;
normals[i + 1] = 0;
normals[i + 2] = 1;
i += 3;
}
}
let j = 0;
for (let iy = 0; iy < heightSegments; ++iy) {
for (let ix = 0; ix < widthSegments; ++ix) {
const a = ix + widthSegments1 * iy;
const b = ix + widthSegments1 * (iy + 1);
const c = (ix + 1) + widthSegments1 * (iy + 1);
const d = (ix + 1) + widthSegments1 * iy;
indices[j] = a;
indices[j + 1] = b;
indices[j + 2] = d;
indices[j + 3] = b;
indices[j + 4] = c;
indices[j + 5] = d;
j += 6;
}
}
return { vertices, normals, indices };
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -23,6 +23,15 @@ export const ImageSchema = {
uImageTexDim: UniformSpec('v2'),
tImageTex: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
tGroupTex: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
tValueTex: TextureSpec('image-float32', 'alpha', 'float', 'linear'),
uTrimType: UniformSpec('i'),
uTrimCenter: UniformSpec('v3'),
uTrimRotation: UniformSpec('q'),
uTrimScale: UniformSpec('v3'),
uTrimTransform: UniformSpec('m4'),
uIsoLevel: UniformSpec('f'),
dInterpolation: DefineSpec('string', InterpolationTypeNames),
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 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 Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -203,6 +203,8 @@ export const ColorSchema = {
uColorTexDim: UniformSpec('v2'),
uColorGridDim: UniformSpec('v3'),
uColorGridTransform: UniformSpec('v4'),
uPaletteDomain: UniformSpec('v2'),
uPaletteDefault: UniformSpec('v3'),
tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
tPalette: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
tColorGrid: TextureSpec('texture', 'rgb', 'ubyte', 'linear'),
@@ -323,6 +325,7 @@ export const BaseSchema = {
uClipObjectPosition: UniformSpec('v3[]', 'material'),
uClipObjectRotation: UniformSpec('v4[]', 'material'),
uClipObjectScale: UniformSpec('v3[]', 'material'),
uClipObjectTransform: UniformSpec('m4[]', 'material'),
aInstance: AttributeSpec('float32', 1, 1),
/**

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