Compare commits

..

76 Commits

Author SHA1 Message Date
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
159 changed files with 7245 additions and 1644 deletions

View File

@@ -5,18 +5,75 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [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
- Fix plugin mouse interactions when CSS `scale` transform is applied
- 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
@@ -84,7 +141,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:
@@ -137,7 +194,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

2340
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.12.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -114,7 +114,8 @@
"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>"
],
"license": "MIT",
"devDependencies": {
@@ -122,19 +123,19 @@
"@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",
"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 +144,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 +171,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

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

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

@@ -211,15 +211,15 @@ 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);
@@ -231,7 +231,7 @@ 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 +239,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);
}
}));
@@ -426,7 +426,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' } },

View File

@@ -288,14 +288,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,

View File

@@ -111,8 +111,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);

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

@@ -0,0 +1,31 @@
<!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;
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
<script type="text/javascript" src="./index.js"></script>
</head>
<body>
<div id="viewer"></div>
<script>
loadIHMRestraints(document.getElementById('viewer'));
</script>
</body>
</html>

View File

@@ -0,0 +1,341 @@
/**
* 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 './index.html';
require('../../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,
}
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 parseRestraints(plugin: PluginContext, url: string) {
const data = await plugin.runTask(ajaxGet(url)) as string;
const parsed = await plugin.runTask(parseCifText(data));
if (parsed.isError) {
console.error(parsed);
return [];
}
const trajectory = await plugin.runTask(trajectoryFromMmCIF(parsed.result.blocks[0], parsed.result));
const dataBlocks = parsed.result.blocks;
const cat = dataBlocks[0].categories['ihm_cross_link_restraint'];
const entity_id_1 = cat.getField('entity_id_1')!;
const asym_id_1 = cat.getField('asym_id_1')!;
const seq_id_1 = cat.getField('seq_id_1')!;
const comp_id_1 = cat.getField('comp_id_1')!;
const entity_id_2 = cat.getField('entity_id_2')!;
const asym_id_2 = cat.getField('asym_id_2')!;
const seq_id_2 = cat.getField('seq_id_2')!;
const comp_id_2 = cat.getField('comp_id_2')!;
const restraint_type = cat.getField('restraint_type')!;
const threshold = cat.getField('distance_threshold')!;
const e1key = CoarseElementKey();
const e2key = CoarseElementKey();
const a = Vec3.zero();
const b = Vec3.zero();
const modelRestraints: 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[] = [];
modelRestraints.push(restraints);
for (let i = 0; i < cat.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 modelRestraints;
}
function baseStructure(url: string, modelIndex: number) {
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 });
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 modelRestraints = await parseRestraints(plugin, url);
const modelIndex = 0;
const restraints = modelRestraints[modelIndex];
const nVialoted = restraints.filter(r => !r.satisfied).length;
const nSatisfied = restraints.length - nVialoted;
const snapshots: Snapshot[] = [];
let mvs = baseStructure(url, modelIndex);
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);
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);
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);
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: `
### Violated Restraints
${nSatisfied} restraints are violated.
`,
}));
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 });
}
(window as any).loadIHMRestraints = loadIHMRestraints;

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,118 @@
/**
* 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();
}
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>
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(-1)} style={{ marginRight: 8 }}>Prev</button>
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(1)} style={{ marginRight: 8 }}>Next</button>
{typeof state.index === 'number' ? state.index + 1 : '-'}/{state.all.length}
</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,59 @@
<!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>The Kinase Story</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#viewer {
position: absolute;
left: 5px;
top: 5px;
right: 34%;
bottom: 5px;
}
#snapshot {
position: absolute;
left: 66%;
top: 5px;
right: 5px;
bottom: 5px;
padding: 16px;
border: 1px solid #ccc;
border-left: none;
background: #F6F5F3;
z-index: -1;
}
</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="snapshot" class="markdown-explanation ">
<mc-snapshot-markdown viewer-name="v1" />
</div>
<script>
setTimeout(() => {
const story = window.buildStory();
window.mc.getContext().dispatch({
kind: 'load-mvs',
data: story,
});
}, 0);
</script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
/**
* 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 { buildStory } from './kinase-story';
require('../../mol-plugin-ui/skin/light.scss');
require('./styles.scss');
export class MolComponents {
getContext(name?: string) {
return getMolComponentContext({ name });
}
}
(window as any).mc = new MolComponents();
(window as any).buildStory = buildStory;

View File

@@ -0,0 +1,731 @@
/**
* 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,
'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
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,
'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](https://www.ebi.ac.uk/pdbe/entry/pdb/1opl/index)) 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](https://www.ebi.ac.uk/pdbe/entry/pdb/2gqg/index), 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.
Here, you can see non-hydrolysable ATP analogue [(AMP-PNP)](https://www.ebi.ac.uk/pdbe-srv/pdbechem/chemicalCompound/show/ANP)
nestled in the active site. Look closely at the active site residues—Lys271, Glu286, and Asp381 (in orange).
They form a crucial network that stabilizes the AMP-PNP (and also the ATP) and in the ATP bound kinase catalyzes phosphorylation,
allowing BCR-ABL to continuously activate downstream signaling pathways.
`,
state: () => {
const builder = createMVSBuilder();
const _2gqg = structure(builder, '2gqg');
const [, _2gqg_poly_repr] = polymer(_2gqg, { color: Colors['2gqg'] });
ligand(_2gqg, {
selector: { label_asym_id: 'C' },
surface: true,
label: 'AMP-PNP ATP',
label_size: 2,
label_color: Colors['2gqg'],
});
domains(_2gqg, _2gqg_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(_2gqg, [
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'C', label_atom_id: 'N' }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'C', label_atom_id: 'N1' }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 315, auth_atom_id: 'OG1' }, { label_asym_id: 'C', label_atom_id: 'N2' }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 620, auth_atom_id: 'O' }, { label_asym_id: 'C', label_atom_id: 'N3' }],
]);
bindingSite(_2gqg, [
[{ 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: [38.76, 81.69, 5.8],
target: [13.01, 60.13, 11.63],
up: [0.19, -0.46, -0.87],
} 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 a normal kinase, ATP binding is a carefully controlled step. But in BCR-ABL, there's no regulation. ATP binds, reactions happen, and
the leukemia-driving signals keep firing.
`,
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 molecule designed to fit into the ATP-binding pocket
and lock BCR-ABL in an inactive conformation. The Imatinib-bound structure ([PDB ID 1IEP](https://www.ebi.ac.uk/pdbe/entry/pdb/1iep/index))
shows the difference: the kinase is frozen. The drug forms a key hydrogen bond with Thr315, known as the gatekeeper residue; as well as Met318, Asp381, Glu286, Ile360 and His361.
`,
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, has shifted into a closed conformation and the activation loop is also flipped into its closed
conformation. Imatinib doesn't just block ATP—it forces the kinase into a state where it can't function at all. The change is decisive: BCR-ABL is 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](https://www.ebi.ac.uk/pdbe/entry/pdb/3ik3/index) 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 ANP-PNP ATP 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 _2gqg = structure(builder, '2gqg');
const [, _2gqg_poly_repr] = polymer(_2gqg, { color: Colors['2gqg'] });
ligand(_2gqg, {
selector: { label_asym_id: 'C' },
surface: true,
label: 'ANP-PNP ATP',
label_size: 2,
label_color: Colors['2gqg'],
});
drawInteractions(_2gqg, [
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'C', label_atom_id: 'N' }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'C', label_atom_id: 'N1' }],
['H-bond', { auth_asym_id: 'A', auth_seq_id: 620, auth_atom_id: 'O' }, { label_asym_id: 'C', label_atom_id: 'N3' }],
]);
bindingSite(_2gqg, [
[{ 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: [19.42, 97.24, -0.29],
target: [13.02, 54.12, 9.71],
up: [0.37, -0.26, -0.89],
} 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](https://www.ebi.ac.uk/pdbe/entry/pdb/3oxz/index)), 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);
}
}
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 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,50 @@
# MolViewSpec Kinase Story 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.
### 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-kinase-story` 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 direcly
// data: mvsJSON
});
</script>
```
See [index.html](./index.html) for a full example.

View File

@@ -0,0 +1,164 @@
.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: 2rem;
font-weight: 300;
}
h1 {
font-size: 3.4rem;
line-height: 1.2;
letter-spacing: -.1rem;
}
h2 {
font-size: 3.0rem;
line-height: 1.25;
letter-spacing: -.1rem;
}
h3 {
font-size: 2.4rem;
line-height: 1.3;
letter-spacing: -.1rem;
}
h4 {
font-size: 2.1rem;
line-height: 1.35;
letter-spacing: -.08rem;
}
h5 {
font-size: 1.8rem;
line-height: 1.5;
letter-spacing: -.05rem;
}
h6 {
font-size: 1.5rem;
line-height: 1.6;
letter-spacing: 0;
}
button {
display: inline-block;
height: 38px;
padding: 0 30px;
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: 2rem;
}
hr {
margin-top: 2rem;
margin-bottom: 2rem;
border-width: 0;
border-top: 1px solid #E1E1E1;
}
}

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

@@ -15,6 +15,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 = {
@@ -87,7 +88,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

@@ -12,6 +12,7 @@ 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);
@@ -71,7 +72,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

@@ -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,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 } from '../../../mol-data/db';
@@ -262,7 +263,7 @@ function matchesRange<T>(requiredMin: T | undefined | null, requiredMax: T | und
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 { macromolecular, ihm } = MS.struct.atomProperty;
const propTests: Partial<Record<string, Expression>> = {};
if (isDefined(row.label_entity_id)) {
@@ -280,11 +281,16 @@ export function rowToExpression(row: MVSAnnotationRow): Expression {
}
const residueTests: Expression[] = [];
if (isDefined(row.label_seq_id)) residueTests.push(eq([macromolecular.label_seq_id(), row.label_seq_id]));
if (isDefined(row.label_seq_id)) {
residueTests.push(ihm.hasSeqId({ 0: 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_label_seq_id) || isDefined(row.end_label_seq_id)) {
residueTests.push(ihm.overlapsSeqIdRange({ beg: row.beg_label_seq_id, end: 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) {

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,6 +125,9 @@ 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. */
@@ -142,7 +145,7 @@ export const UpdateTarget = {
}
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 result: UpdateTarget = { ...target, selector: msResult, 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';
@@ -24,7 +26,7 @@ import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-p
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData } from './components/primitives';
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 +34,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 +126,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 }) {
const context = MolstarLoadingContext.create();
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, options);
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 +187,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 +289,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`

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

@@ -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 = {}): any {
return 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

@@ -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-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 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>
@@ -392,8 +392,6 @@ namespace Canvas3D {
let y = 0;
let width = 128;
let height = 128;
let canvasScaleRatioX = 1;
let canvasScaleRatioY = 1;
let forceNextRender = false;
let currentTime = 0;
@@ -647,7 +645,7 @@ namespace Canvas3D {
function identify(x: number, y: number): PickData | undefined {
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
return webgl.isContextLost ? undefined : pickHelper.identify(x / canvasScaleRatioX, y / canvasScaleRatioY, cam);
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
}
function commit(isSynchronous: boolean = false) {
@@ -1160,12 +1158,6 @@ namespace Canvas3D {
function updateViewport() {
const oldX = x, oldY = y, oldWidth = width, oldHeight = height;
const canvasRect = canvas?.getBoundingClientRect();
canvasScaleRatioX = (canvasRect?.width ?? gl.drawingBufferWidth) / gl.drawingBufferWidth;
if (!canvasScaleRatioX) canvasScaleRatioX = 1;
canvasScaleRatioY = (canvasRect?.height ?? gl.drawingBufferHeight) / gl.drawingBufferHeight;
if (!canvasScaleRatioY) canvasScaleRatioY = 1;
if (p.viewport.name === 'canvas') {
x = 0;
y = 0;
@@ -1192,7 +1184,7 @@ namespace Canvas3D {
pickHelper.setViewport(x, y, width, height);
renderer.setViewport(x, y, width, height);
Viewport.set(camera.viewport, x, y, width, height);
Viewport.set(controls.viewport, x, y, width * canvasScaleRatioX, height * canvasScaleRatioY);
Viewport.set(controls.viewport, x, y, width, height);
hiZ.setViewport(x, y, width, 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),
/**

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>
@@ -270,7 +270,6 @@ namespace Renderer {
return;
}
// TODO: check what happens if sphere surrounds frustum fully
if (!Frustum3D.intersectsSphere3D(frustum, r.values.boundingSphere.ref.value)) {
return;
}
@@ -283,8 +282,13 @@ namespace Renderer {
if (d - radius > maxDistance) return;
}
const hasInstanceGrid = r.values.instanceGrid.ref.value.cellSize > 1;
if (hasInstanceGrid || (hasInstanceGrid && r.values.lodLevels)) {
if (isOccluded !== null && isOccluded(r.values.boundingSphere.ref.value)) {
return;
}
const hasInstanceGrid = r.values.instanceGrid.ref.value.cellSize > 0;
const hasMultipleInstances = r.values.uInstanceCount.ref.value > 1;
if (hasInstanceGrid && (hasMultipleInstances || r.values.lodLevels)) {
r.cull(cameraPlane, frustum, isOccluded, ctx.stats);
} else {
r.uncull();

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>
@@ -252,7 +252,8 @@ namespace Scene {
const xray = (p.values.dXrayShaded?.ref.value === 'on' || p.values.dXrayShaded?.ref.value === 'inverted') ? 0.5 : 1;
const fuzzy = p.values.dPointStyle?.ref.value === 'fuzzy' ? 0.5 : 1;
const text = p.values.dGeometryType.ref.value === 'text' ? 0.5 : 1;
opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray * fuzzy * text;
const image = p.values.dGeometryType.ref.value === 'image' ? 0.5 : 1;
opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray * fuzzy * text * image;
count += 1;
}
return count > 0 ? opacityAverage / count : 0;
@@ -272,7 +273,8 @@ namespace Scene {
if (p.values.dXrayShaded?.ref.value === 'on' ||
p.values.dXrayShaded?.ref.value === 'inverted' ||
p.values.dPointStyle?.ref.value === 'fuzzy' ||
p.values.dGeometryType.ref.value === 'text'
p.values.dGeometryType.ref.value === 'text' ||
p.values.dGeometryType.ref.value === 'image'
) transparenyValues.push(0.5);
if (p.values.transparencyMin.ref.value > 0) transparenyValues.push(p.values.transparencyMin.ref.value);
transparencyMin = Math.min(transparencyMin, ...transparenyValues);

View File

@@ -29,9 +29,9 @@ export const assign_color_varying = `
vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + group, uColorTexDim).rgb;
#endif
#elif defined(dColorType_vertex)
vColor.rgb = readFromTexture(tColor, VertexID, uColorTexDim).rgb;
vColor.rgb = readFromTexture(tColor, vertexId, uColorTexDim).rgb;
#elif defined(dColorType_vertexInstance)
vColor.rgb = readFromTexture(tColor, int(aInstance) * uVertexCount + VertexID, uColorTexDim).rgb;
vColor.rgb = readFromTexture(tColor, int(aInstance) * uVertexCount + vertexId, uColorTexDim).rgb;
#elif defined(dColorType_volume)
vec3 cgridPos = (uColorGridTransform.w * (position - uColorGridTransform.xyz)) / uColorGridDim;
vColor.rgb = texture3dFrom2dLinear(tColorGrid, cgridPos, uColorGridDim, uColorTexDim).rgb;
@@ -41,7 +41,7 @@ export const assign_color_varying = `
#endif
#ifdef dUsePalette
vPaletteV = ((vColor.r * 256.0 * 256.0 * 255.0 + vColor.g * 256.0 * 255.0 + vColor.b * 255.0) - 1.0) / 16777215.0;
vPaletteV = ((vColor.r * 256.0 * 256.0 * 255.0 + vColor.g * 256.0 * 255.0 + vColor.b * 255.0) - 1.0) / PALETTE_SCALE;
#endif
#ifdef dOverpaint
@@ -50,7 +50,7 @@ export const assign_color_varying = `
#elif defined(dOverpaintType_groupInstance)
vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + group, uOverpaintTexDim);
#elif defined(dOverpaintType_vertexInstance)
vOverpaint = readFromTexture(tOverpaint, int(aInstance) * uVertexCount + VertexID, uOverpaintTexDim);
vOverpaint = readFromTexture(tOverpaint, int(aInstance) * uVertexCount + vertexId, uOverpaintTexDim);
#elif defined(dOverpaintType_volumeInstance)
vec3 ogridPos = (uOverpaintGridTransform.w * (vModelPosition - uOverpaintGridTransform.xyz)) / uOverpaintGridDim;
vOverpaint = texture3dFrom2dLinear(tOverpaintGrid, ogridPos, uOverpaintGridDim, uOverpaintTexDim);
@@ -71,7 +71,7 @@ export const assign_color_varying = `
#elif defined(dEmissiveType_groupInstance)
vEmissive = readFromTexture(tEmissive, aInstance * float(uGroupCount) + group, uEmissiveTexDim).a;
#elif defined(dEmissiveType_vertexInstance)
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + VertexID, uEmissiveTexDim).a;
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + vertexId, uEmissiveTexDim).a;
#elif defined(dEmissiveType_volumeInstance)
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
vEmissive = texture3dFrom2dLinear(tEmissiveGrid, egridPos, uEmissiveGridDim, uEmissiveTexDim).a;
@@ -85,7 +85,7 @@ export const assign_color_varying = `
#elif defined(dSubstanceType_groupInstance)
vSubstance = readFromTexture(tSubstance, aInstance * float(uGroupCount) + group, uSubstanceTexDim);
#elif defined(dSubstanceType_vertexInstance)
vSubstance = readFromTexture(tSubstance, int(aInstance) * uVertexCount + VertexID, uSubstanceTexDim);
vSubstance = readFromTexture(tSubstance, int(aInstance) * uVertexCount + vertexId, uSubstanceTexDim);
#elif defined(dSubstanceType_volumeInstance)
vec3 sgridPos = (uSubstanceGridTransform.w * (vModelPosition - uSubstanceGridTransform.xyz)) / uSubstanceGridDim;
vSubstance = texture3dFrom2dLinear(tSubstanceGrid, sgridPos, uSubstanceGridDim, uSubstanceTexDim);
@@ -102,7 +102,7 @@ export const assign_color_varying = `
#elif defined(dEmissiveType_groupInstance)
vEmissive = readFromTexture(tEmissive, aInstance * float(uGroupCount) + group, uEmissiveTexDim).a;
#elif defined(dEmissiveType_vertexInstance)
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + VertexID, uEmissiveTexDim).a;
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + vertexId, uEmissiveTexDim).a;
#elif defined(dEmissiveType_volumeInstance)
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
vEmissive = texture3dFrom2dLinear(tEmissiveGrid, egridPos, uEmissiveGridDim, uEmissiveTexDim).a;
@@ -131,7 +131,7 @@ export const assign_color_varying = `
#elif defined(dTransparencyType_groupInstance)
vTransparency = readFromTexture(tTransparency, aInstance * float(uGroupCount) + group, uTransparencyTexDim).a;
#elif defined(dTransparencyType_vertexInstance)
vTransparency = readFromTexture(tTransparency, int(aInstance) * uVertexCount + VertexID, uTransparencyTexDim).a;
vTransparency = readFromTexture(tTransparency, int(aInstance) * uVertexCount + vertexId, uTransparencyTexDim).a;
#elif defined(dTransparencyType_volumeInstance)
vec3 tgridPos = (uTransparencyGridTransform.w * (vModelPosition - uTransparencyGridTransform.xyz)) / uTransparencyGridDim;
vTransparency = texture3dFrom2dLinear(tTransparencyGrid, tgridPos, uTransparencyGridDim, uTransparencyTexDim).a;

View File

@@ -1,6 +1,6 @@
export const assign_group = `
#ifdef dGeometryType_textureMesh
float group = unpackRGBToInt(readFromTexture(tGroup, VertexID, uGeoTexDim).rgb);
float group = unpackRGBToInt(readFromTexture(tGroup, vertexId, uGeoTexDim).rgb);
#else
float group = aGroup;
#endif

View File

@@ -2,7 +2,7 @@ export const assign_position = `
mat4 model = uModel * aTransform;
mat4 modelView = uView * model;
#ifdef dGeometryType_textureMesh
vec3 position = readFromTexture(tPosition, VertexID, uGeoTexDim).xyz;
vec3 position = readFromTexture(tPosition, vertexId, uGeoTexDim).xyz;
#else
vec3 position = aPosition;
#endif

View File

@@ -1,8 +1,9 @@
export const clip_instance = `
#if defined(dClipVariant_instance) && dClipObjectCount != 0
vec4 mCenter = uModel * aTransform * vec4(uInvariantBoundingSphere.xyz, 1.0);
if (clipTest(vec4(mCenter.xyz, uInvariantBoundingSphere.w)))
vec3 mCenter = (uModel * aTransform * vec4(uInvariantBoundingSphere.xyz, 1.0)).xyz;
if (clipTest(mCenter)) {
// move out of [ -w, +w ] to 'discard' in vert shader
gl_Position.z = 2.0 * gl_Position.w;
}
#endif
`;

View File

@@ -1,6 +1,6 @@
export const clip_pixel = `
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
if (clipTest(vec4(vModelPosition, 0.0)))
if (clipTest(vModelPosition))
discard;
#endif
`;

View File

@@ -1,112 +1,110 @@
/**
* Copyright (c) 2020-2022 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 Ludovic Autin <autin@scripps.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
export const common_clip = `
#if dClipObjectCount != 0
vec3 quaternionTransform(const in vec4 q, const in vec3 v) {
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
vec3 quaternionTransform(const in vec4 q, const in vec3 v) {
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}
vec4 computePlane(const in vec3 normal, const in vec3 inPoint) {
return vec4(normalize(normal), -dot(normal, inPoint));
}
float planeSD(const in vec4 plane, const in vec3 center) {
return -dot(plane.xyz, center - plane.xyz * -plane.w);
}
float sphereSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
return (
length(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position) / size) - 1.0
) * min(min(size.x, size.y), size.z);
}
float cubeSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
vec3 d = abs(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position)) - size;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float cylinderSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
vec2 d = abs(vec2(length(t.xz), t.y)) - size.xy;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float infiniteConeSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
float q = length(t.xy);
return dot(size.xy, vec2(q, t.z));
}
float getSignedDistance(const in vec3 center, const in int type, const in vec3 position, const in vec4 rotation, const in vec3 scale, const in mat4 transform) {
vec3 c = (transform * vec4(center, 1.0)).xyz;
if (type == 1) {
vec3 normal = quaternionTransform(rotation, vec3(0.0, 1.0, 0.0));
vec4 plane = computePlane(normal, position);
return planeSD(plane, c);
} else if (type == 2) {
return sphereSD(position, rotation, scale * 0.5, c);
} else if (type == 3) {
return cubeSD(position, rotation, scale * 0.5, c);
} else if (type == 4) {
return cylinderSD(position, rotation, scale * 0.5, c);
} else if (type == 5) {
return infiniteConeSD(position, rotation, scale * 0.5, c);
} else {
return 0.1;
}
}
vec4 computePlane(const in vec3 normal, const in vec3 inPoint) {
return vec4(normalize(normal), -dot(normal, inPoint));
}
float planeSD(const in vec4 plane, const in vec3 center) {
return -dot(plane.xyz, center - plane.xyz * -plane.w);
}
float sphereSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
return (
length(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position) / size) - 1.0
) * min(min(size.x, size.y), size.z);
}
float cubeSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
vec3 d = abs(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position)) - size;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float cylinderSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
vec2 d = abs(vec2(length(t.xz), t.y)) - size.xy;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float infiniteConeSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
float q = length(t.xy);
return dot(size.xy, vec2(q, t.z));
}
float getSignedDistance(const in vec3 center, const in int type, const in vec3 position, const in vec4 rotation, const in vec3 scale) {
if (type == 1) {
vec3 normal = quaternionTransform(rotation, vec3(0.0, 1.0, 0.0));
vec4 plane = computePlane(normal, position);
return planeSD(plane, center);
} else if (type == 2) {
return sphereSD(position, rotation, scale * 0.5, center);
} else if (type == 3) {
return cubeSD(position, rotation, scale * 0.5, center);
} else if (type == 4) {
return cylinderSD(position, rotation, scale * 0.5, center);
} else if (type == 5) {
return infiniteConeSD(position, rotation, scale * 0.5, center);
} else {
return 0.1;
#if __VERSION__ == 100
// 8-bit
int bitwiseAnd(in int a, in int b) {
int d = 128;
int result = 0;
for (int i = 0; i < 8; ++i) {
if (d <= 0) break;
if (a >= d && b >= d) result += d;
if (a >= d) a -= d;
if (b >= d) b -= d;
d /= 2;
}
return result;
}
#if __VERSION__ == 100
// 8-bit
int bitwiseAnd(in int a, in int b) {
int d = 128;
int result = 0;
for (int i = 0; i < 8; ++i) {
if (d <= 0) break;
if (a >= d && b >= d) result += d;
if (a >= d) a -= d;
if (b >= d) b -= d;
d /= 2;
}
return result;
}
bool hasBit(const in int mask, const in int bit) {
return bitwiseAnd(mask, bit) == 0;
}
#else
bool hasBit(const in int mask, const in int bit) {
return (mask & bit) == 0;
}
#endif
bool clipTest(const in vec4 sphere) {
// flag is a bit-flag for clip-objects to ignore (note, object ids start at 1 not 0)
#if defined(dClipping)
int flag = int(floor(vClipping * 255.0 + 0.5));
#else
int flag = 0;
#endif
#pragma unroll_loop_start
for (int i = 0; i < dClipObjectCount; ++i) {
if (flag == 0 || hasBit(flag, UNROLLED_LOOP_INDEX + 1)) {
// TODO take sphere radius into account?
bool test = getSignedDistance(sphere.xyz, uClipObjectType[i], uClipObjectPosition[i], uClipObjectRotation[i], uClipObjectScale[i]) <= 0.0;
if ((!uClipObjectInvert[i] && test) || (uClipObjectInvert[i] && !test)) {
return true;
}
}
}
#pragma unroll_loop_end
return false;
bool hasBit(const in int mask, const in int bit) {
return bitwiseAnd(mask, bit) == 0;
}
#else
bool hasBit(const in int mask, const in int bit) {
return (mask & bit) == 0;
}
#endif
bool clipTest(const in vec3 center) {
// flag is a bit-flag for clip-objects to ignore (note, object ids start at 1 not 0)
#if defined(dClipping)
int flag = int(floor(vClipping * 255.0 + 0.5));
#else
int flag = 0;
#endif
#pragma unroll_loop_start
for (int i = 0; i < dClipObjectCount; ++i) {
if (flag == 0 || hasBit(flag, UNROLLED_LOOP_INDEX + 1)) {
bool test = getSignedDistance(center, uClipObjectType[i], uClipObjectPosition[i], uClipObjectRotation[i], uClipObjectScale[i], uClipObjectTransform[i]) <= 0.0;
if ((!uClipObjectInvert[i] && test) || (uClipObjectInvert[i] && !test)) {
return true;
}
}
}
#pragma unroll_loop_end
return false;
}
`;

View File

@@ -15,6 +15,7 @@ uniform vec4 uLod;
uniform vec3 uClipObjectPosition[dClipObjectCount];
uniform vec4 uClipObjectRotation[dClipObjectCount];
uniform vec3 uClipObjectScale[dClipObjectCount];
uniform mat4 uClipObjectTransform[dClipObjectCount];
#if defined(dClipping)
#if __VERSION__ == 100 || defined(dClippingType_instance) || !defined(dVaryingGroup)

View File

@@ -19,6 +19,7 @@ uniform int uPickType;
uniform vec3 uClipObjectPosition[dClipObjectCount];
uniform vec4 uClipObjectRotation[dClipObjectCount];
uniform vec3 uClipObjectScale[dClipObjectCount];
uniform mat4 uClipObjectTransform[dClipObjectCount];
#if defined(dClipping)
uniform vec2 uClippingTexDim;

View File

@@ -42,6 +42,8 @@ export const common = `
#define TWO_PI 6.2831853
#define HALF_PI 1.570796325
#define PALETTE_SCALE 16777214.0 // (1 << 24) - 2
#define saturate(a) clamp(a, 0.0, 1.0)
#if __VERSION__ == 100

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>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -84,7 +84,7 @@ bool CylinderImpostor(
viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
fragmentDepth = calcDepth(viewPosition);
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
if (clipTest(vec4(modelPosition, 0.0))) {
if (clipTest(modelPosition)) {
objectClipped = true;
fragmentDepth = -1.0;
#ifdef dSolidInterior
@@ -108,7 +108,7 @@ bool CylinderImpostor(
viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
fragmentDepth = calcDepth(viewPosition);
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
if (clipTest(vec4(modelPosition, 0.0))) {
if (clipTest(modelPosition)) {
objectClipped = true;
fragmentDepth = -1.0;
#ifdef dSolidInterior
@@ -138,7 +138,7 @@ bool CylinderImpostor(
viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
fragmentDepth = calcDepth(viewPosition);
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
if (clipTest(vec4(modelPosition, 0.0))) {
if (clipTest(modelPosition)) {
objectClipped = true;
fragmentDepth = -1.0;
#ifdef dSolidInterior

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>
*/
@@ -38,6 +38,8 @@ uniform float uIsOrtho;
uniform vec3 uCameraDir;
void main() {
int vertexId = VertexID;
#include assign_group
#include assign_color_varying
#include assign_marker_varying

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -18,6 +18,7 @@ precision highp int;
uniform vec3 uClipObjectPosition[dClipObjectCount];
uniform vec4 uClipObjectRotation[dClipObjectCount];
uniform vec3 uClipObjectScale[dClipObjectCount];
uniform mat4 uClipObjectTransform[dClipObjectCount];
#endif
#include common_clip
@@ -122,6 +123,7 @@ uniform mat4 uCartnToUnit;
#endif
#ifdef dUsePalette
uniform vec2 uPaletteDomain;
uniform sampler2D tPalette;
#endif
@@ -240,7 +242,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
vec3 vModelPosition = v3m4(unitPos * uGridDim, modelTransform);
if (clipTest(vec4(vModelPosition, 0.0))) {
if (clipTest(modelPosition)) {
prevValue = value;
pos += step;
continue;
@@ -271,7 +273,8 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
#endif
#if defined(dColorType_direct) && defined(dUsePalette)
material.rgb = texture2D(tPalette, vec2(value, 0.0)).rgb;
float paletteValue = (value - uPaletteDomain[0]) / (uPaletteDomain[1] - uPaletteDomain[0]);
material.rgb = texture2D(tPalette, vec2(clamp(paletteValue, 0.0, 1.0), 0.0)).rgb;
#elif defined(dColorType_uniform)
material.rgb = uColor;
#elif defined(dColorType_instance)

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>
*/
@@ -12,12 +12,46 @@ precision highp int;
#include common_frag_params
#include common_clip
uniform float uEmissive;
// Density value to estimate object thickness
uniform float uDensity;
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#ifdef dOverpaint
#if defined(dOverpaintType_instance) || defined(dOverpaintType_groupInstance)
varying vec4 vOverpaint;
uniform vec2 uOverpaintTexDim;
uniform sampler2D tOverpaint;
#endif
uniform float uOverpaintStrength;
#endif
#endif
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing) || defined(dRenderVariant_emissive)
#ifdef dEmissive
#if defined(dEmissiveType_instance) || defined(dEmissiveType_groupInstance)
varying float vEmissive;
uniform vec2 uEmissiveTexDim;
uniform sampler2D tEmissive;
#endif
uniform float uEmissiveStrength;
#endif
#endif
#ifdef dTransparency
#if defined(dTransparencyType_instance) || defined(dTransparencyType_groupInstance)
varying float vTransparency;
uniform vec2 uTransparencyTexDim;
uniform sampler2D tTransparency;
#endif
uniform float uTransparencyStrength;
#endif
uniform vec2 uImageTexDim;
uniform sampler2D tImageTex;
uniform sampler2D tGroupTex;
uniform sampler2D tValueTex;
uniform vec2 uMarkerTexDim;
uniform sampler2D tMarker;
@@ -25,6 +59,19 @@ uniform sampler2D tMarker;
varying vec2 vUv;
varying float vInstance;
#ifdef dUsePalette
uniform sampler2D tPalette;
uniform vec3 uPaletteDefault;
#endif
uniform int uTrimType;
uniform vec3 uTrimCenter;
uniform vec4 uTrimRotation;
uniform vec3 uTrimScale;
uniform mat4 uTrimTransform;
uniform float uIsoLevel;
#if defined(dInterpolation_catmulrom) || defined(dInterpolation_mitchell) || defined(dInterpolation_bspline)
#define dInterpolation_cubic
#endif
@@ -91,36 +138,102 @@ varying float vInstance;
#endif
void main() {
if (uTrimType != 0 && getSignedDistance(vModelPosition, uTrimType, uTrimCenter, uTrimRotation, uTrimScale, uTrimTransform) > 0.0) discard;
#include fade_lod
#include clip_pixel
#if defined(dInterpolation_cubic)
vec4 imageData = biCubic(tImageTex, vUv);
#ifdef dUsePalette
vec4 material = texture2D(tImageTex, vUv);
if (material.rgb != vec3(1.0)) {
material = biCubic(tImageTex, vUv);
}
#else
vec4 material = biCubic(tImageTex, vUv);
#endif
#else
vec4 imageData = texture2D(tImageTex, vUv);
vec4 material = texture2D(tImageTex, vUv);
#endif
imageData.a = clamp(imageData.a, 0.0, 1.0);
if (imageData.a > 0.9) imageData.a = 1.0;
imageData.a *= uAlpha;
if (imageData.a < 0.05)
discard;
if (uIsoLevel >= 0.0) {
if (texture2D(tValueTex, vUv).r < uIsoLevel) discard;
material.a = uAlpha;
} else {
if (material.a == 0.0) discard;
material.a *= uAlpha;
}
float fragmentDepth = gl_FragCoord.z;
if ((uRenderMask == MaskOpaque && imageData.a < 1.0) ||
(uRenderMask == MaskTransparent && imageData.a == 1.0)
vec3 packedGroup = texture2D(tGroupTex, vUv).rgb;
float group = packedGroup == vec3(0.0) ? -1.0 : unpackRGBToInt(packedGroup);
// apply per-group transparency
#if defined(dTransparency) && (defined(dRenderVariant_pick) || defined(dRenderVariant_color) || defined(dRenderVariant_emissive) || defined(dRenderVariant_tracing))
float transparency = 0.0;
#if defined(dTransparencyType_instance)
transparency = readFromTexture(tTransparency, vInstance, uTransparencyTexDim).a;
#elif defined(dTransparencyType_groupInstance)
transparency = readFromTexture(tTransparency, vInstance * float(uGroupCount) + group, uTransparencyTexDim).a;
#endif
transparency *= uTransparencyStrength;
float ta = 1.0 - transparency;
if (transparency < 0.09) ta = 1.0; // hard cutoff looks better
#if defined(dRenderVariant_pick)
if (ta * uAlpha < uPickingAlphaThreshold)
discard; // ignore so the element below can be picked
#elif defined(dRenderVariant_emissive)
if (ta < 1.0)
discard; // emissive not supported with transparency
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
material.a *= ta;
#endif
#endif
if ((uRenderMask == MaskOpaque && material.a < 1.0) ||
(uRenderMask == MaskTransparent && material.a == 1.0)
) {
discard;
}
#if defined(dNeedsMarker)
float marker = uMarker;
if (group == -1.0) {
marker = 0.0;
} else if (uMarker == -1.0) {
marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
}
#endif
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing) || defined(dRenderVariant_emissive)
float emissive = uEmissive;
if (group == -1.0) {
emissive = 0.0;
} else {
#ifdef dEmissive
#if defined(dEmissiveType_instance)
emissive += readFromTexture(tEmissive, vInstance, uEmissiveTexDim).a * uEmissiveStrength;
#elif defined(dEmissiveType_groupInstance)
emissive += readFromTexture(tEmissive, vInstance * float(uGroupCount) + group, uEmissiveTexDim).a * uEmissiveStrength;
#endif
#endif
}
#endif
#if defined(dRenderVariant_pick)
if (imageData.a < 0.3)
discard;
if (group == -1.0) discard;
#include check_picking_alpha
#ifdef requiredDrawBuffers
gl_FragColor = vec4(packIntToRGB(float(uObjectId)), 1.0);
gl_FragData[1] = vec4(packIntToRGB(vInstance), 1.0);
gl_FragData[2] = vec4(texture2D(tGroupTex, vUv).rgb, 1.0);
gl_FragData[2] = vec4(packIntToRGB(group), 1.0);
gl_FragData[3] = packDepthToRGBA(fragmentDepth);
#else
gl_FragColor = vColor;
@@ -129,50 +242,61 @@ void main() {
} else if (uPickType == 2) {
gl_FragColor = vec4(packIntToRGB(vInstance), 1.0);
} else {
gl_FragColor = vec4(texture2D(tGroupTex, vUv).rgb, 1.0);
gl_FragColor = vec4(packIntToRGB(group), 1.0);
}
#endif
#elif defined(dRenderVariant_depth)
if (imageData.a < 0.05)
discard;
if (uRenderMask == MaskOpaque) {
gl_FragColor = packDepthToRGBA(fragmentDepth);
} else if (uRenderMask == MaskTransparent) {
gl_FragColor = packDepthWithAlphaToRGBA(fragmentDepth, imageData.a);
gl_FragColor = packDepthWithAlphaToRGBA(fragmentDepth, material.a);
}
#elif defined(dRenderVariant_marking)
float marker = uMarker;
if (uMarker == -1.0) {
float group = unpackRGBToInt(texture2D(tGroupTex, vUv).rgb);
marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
}
if (uMarkingType == 1) {
if (marker > 0.0 || imageData.a < 0.05)
if (marker > 0.0)
discard;
gl_FragColor = packDepthToRGBA(fragmentDepth);
} else {
if (marker == 0.0 || imageData.a < 0.05)
if (marker == 0.0)
discard;
float depthTest = 1.0;
if (uMarkingDepthTest) {
depthTest = (fragmentDepth >= getDepthPacked(gl_FragCoord.xy / uDrawingBufferSize)) ? 1.0 : 0.0;
}
bool isHighlight = intMod(marker, 2.0) > 0.1;
gl_FragColor = vec4(0.0, depthTest, isHighlight ? 1.0 : 0.0, 1.0);
float viewZ = depthToViewZ(uIsOrtho, fragmentDepth, uNear, uFar);
float fogFactor = smoothstep(uFogNear, uFogFar, abs(viewZ));
if (fogFactor == 1.0)
discard;
gl_FragColor = vec4(0.0, depthTest, isHighlight ? 1.0 : 0.0, 1.0 - fogFactor);
}
#elif defined(dRenderVariant_emissive)
gl_FragColor = vec4(0.0);
gl_FragColor = vec4(emissive);
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
gl_FragColor = imageData;
#ifdef dUsePalette
if (material.rgb == vec3(1.0)) {
material.rgb = uPaletteDefault;
} else {
float v = ((material.r * 256.0 * 256.0 * 255.0 + material.g * 256.0 * 255.0 + material.b * 255.0) - 1.0) / PALETTE_SCALE;
material.rgb = texture2D(tPalette, vec2(v, 0.0)).rgb;
}
#endif
float marker = uMarker;
if (uMarker == -1.0) {
float group = unpackRGBToInt(texture2D(tGroupTex, vUv).rgb);
marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
}
// mix material with overpaint
#if defined(dOverpaint)
vec4 overpaint = vec4(0.0);
if (group != -1.0) {
#if defined(dOverpaintType_instance)
overpaint = readFromTexture(tOverpaint, vInstance, uOverpaintTexDim);
#elif defined(dOverpaintType_groupInstance)
overpaint = readFromTexture(tOverpaint, vInstance * float(uGroupCount) + group, uOverpaintTexDim);
#endif
overpaint *= uOverpaintStrength;
}
material.rgb = mix(material.rgb, overpaint.rgb, overpaint.a);
#endif
gl_FragColor = material;
#include apply_marker_color
#if defined(dRenderVariant_color)
@@ -180,8 +304,8 @@ void main() {
#include wboit_write
#include dpoit_write
#elif defined(dRenderVariant_tracing)
gl_FragData[1] = vec4(normalize(vViewPosition), 0.0);
gl_FragData[2] = vec4(imageData.rgb, uDensity);
gl_FragData[1] = vec4(normalize(vViewPosition), emissive);
gl_FragData[2] = vec4(material.rgb, uDensity);
#endif
#endif
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020 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>
*/
@@ -19,6 +19,8 @@ varying vec2 vUv;
varying float vInstance;
void main() {
int vertexId = VertexID;
#include assign_position
vUv = aUv;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 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>
*
@@ -40,6 +40,7 @@ void trimSegment(const in vec4 start, inout vec4 end) {
void main(){
float aspect = uViewport.z / uViewport.w;
int vertexId = VertexID;
#include assign_group
#include assign_color_varying

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>
*/
@@ -32,6 +32,8 @@ attribute float aInstance;
varying vec3 vNormal;
void main(){
int vertexId = VertexID;
#include assign_group
#include assign_marker_varying
#include assign_clipping_varying
@@ -40,7 +42,7 @@ void main(){
#include clip_instance
#ifdef dGeometryType_textureMesh
vec3 normal = readFromTexture(tNormal, VertexID, uGeoTexDim).xyz;
vec3 normal = readFromTexture(tNormal, vertexId, uGeoTexDim).xyz;
#else
vec3 normal = aNormal;
#endif

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 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>
*/
@@ -24,6 +24,8 @@ attribute float aInstance;
attribute float aGroup;
void main(){
int vertexId = VertexID;
#include assign_group
#include assign_color_varying
#include assign_marker_varying

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>
*/
@@ -52,7 +52,7 @@ bool SphereImpostor(out vec3 modelPos, out vec3 cameraPos, out vec3 cameraNormal
bool objectClipped = false;
#if !defined(dClipPrimitive) && defined(dClipVariant_pixel) && dClipObjectCount != 0
if (clipTest(vec4(modelPos, 0.0))) {
if (clipTest(modelPos)) {
objectClipped = true;
fragmentDepth = -1.0;
}

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>
*/
@@ -70,7 +70,9 @@ void main(void){
mapping = vec2(1.0, -1.0);
}
vec4 positionGroup = readFromTexture(tPositionGroup, VertexID / 6, uTexDim);
int vertexId = VertexID / 6;
vec4 positionGroup = readFromTexture(tPositionGroup, vertexId, uTexDim);
vec3 position = positionGroup.rgb;
float group = positionGroup.a;
@@ -128,7 +130,7 @@ void main(void){
}
#if defined(dClipPrimitive) && !defined(dClipVariant_instance) && dClipObjectCount != 0
if (clipTest(vec4(vModelPosition.xyz, 0.0))) {
if (clipTest(vModelPosition)) {
// move out of [ -w, +w ] to 'discard' in vert shader
gl_Position.z = 2.0 * gl_Position.w;
}

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>
*/
@@ -38,6 +38,8 @@ varying vec2 vTexCoord;
#include matrix_scale
void main(void){
int vertexId = VertexID;
#include assign_group
#include assign_color_varying
#include assign_marker_varying

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>
@@ -21,6 +21,10 @@ import { fillSerial } from '../../mol-util/array';
import { deepClone } from '../../mol-util/object';
import { cloneUniformValues, UniformsList } from './uniform';
// Handle Firefox's preference [webgl.max-vert-ids-per-draw] which defaults to 30_000_000
// since FF119, see https://bugzilla.mozilla.org/show_bug.cgi?id=1849433
const MaxDrawCount = 30_000_000;
const getNextRenderItemId = idFactory();
export type DrawMode = 'points' | 'lines' | 'line-strip' | 'line-loop' | 'triangles' | 'triangle-strip' | 'triangle-fan'
@@ -311,10 +315,16 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
}
}
} else {
if (elementsBuffer) {
instancedArrays.drawElementsInstanced(glDrawMode, drawCount, elementsBuffer._dataType, 0, instanceCount);
} else {
instancedArrays.drawArraysInstanced(glDrawMode, 0, drawCount, instanceCount);
let offset = 0;
while (true) {
const count = Math.min(drawCount - offset, MaxDrawCount);
if (elementsBuffer) {
instancedArrays.drawElementsInstanced(glDrawMode, count, elementsBuffer._dataType, offset * elementsBuffer._bpe, instanceCount);
} else {
instancedArrays.drawArraysInstanced(glDrawMode, offset, count, instanceCount);
}
offset += count;
if (offset >= drawCount) break;
}
if (isTimingMode) {
stats.calls.drawInstanced += 1;

View File

@@ -1,10 +1,10 @@
/**
* 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>
*/
import { Mat3, Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { Mat3, Mat4, Quat, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { ValueCell } from '../../mol-util';
import { GLRenderingContext } from './compat';
import { RenderableSchema } from '../../mol-gl/renderable/schema';
@@ -18,6 +18,7 @@ export type UniformKindValue = {
'v2': Vec2; 'v2[]': number[]
'v3': Vec3; 'v3[]': number[]
'v4': Vec4; 'v4[]': number[]
'q': Quat; 'q[]': number[]
'iv2': Vec2; 'iv2[]': number[]
'iv3': Vec3; 'iv3[]': number[]
'iv4': Vec4; 'iv4[]': number[]
@@ -39,6 +40,7 @@ export function getUniformType(gl: GLRenderingContext, kind: UniformKind) {
case 'v2': case 'v2[]': return gl.FLOAT_VEC2;
case 'v3': case 'v3[]': return gl.FLOAT_VEC3;
case 'v4': case 'v4[]': return gl.FLOAT_VEC4;
case 'q': case 'q[]': return gl.FLOAT_VEC4;
case 'iv2': case 'iv2[]': return gl.INT_VEC2;
case 'iv3': case 'iv3[]': return gl.INT_VEC3;
case 'iv4': case 'iv4[]': return gl.INT_VEC4;
@@ -77,6 +79,7 @@ function getUniformSetter(kind: UniformKind): UniformSetter {
case 'v2': case 'v2[]': return uniform2fv;
case 'v3': case 'v3[]': return uniform3fv;
case 'v4': case 'v4[]': return uniform4fv;
case 'q': case 'q[]': return uniform4fv;
case 'iv2': case 'iv2[]': return uniform2iv;
case 'iv3': case 'iv3[]': return uniform3iv;
case 'iv4': case 'iv4[]': return uniform4iv;
@@ -107,6 +110,7 @@ export function getUniformGlslType(kind: UniformKind): string {
case 'v2': return 'vec2';
case 'v3': return 'vec3';
case 'v4': return 'vec4';
case 'q': return 'vec4';
case 'm3': return 'mat3';
case 'm4': return 'mat4';
}

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.399, IHM 1.27, MA 1.4.6.
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.401, IHM 1.27, MA 1.4.7.
*
* @author molstar/ciftools package
*/

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.399, IHM 1.27, MA 1.4.6.
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.401, IHM 1.27, MA 1.4.7.
*
* @author molstar/ciftools package
*/

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.399, IHM 1.27, MA 1.4.6.
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.401, IHM 1.27, MA 1.4.7.
*
* @author molstar/ciftools package
*/
@@ -705,7 +705,7 @@ export const mmCIF_Schema = {
/**
* An abbreviation that identifies the database.
*/
database_id: Aliased<'alphafolddb' | 'cas' | 'csd' | 'emdb' | 'icsd' | 'modelarchive' | 'mdf' | 'modbase' | 'ndb' | 'nbs' | 'pdb' | 'pdf' | 'rcsb' | 'swiss-model_repository' | 'ebi' | 'pdbe' | 'bmrb' | 'wwpdb' | 'pdb_acc'>(lstr),
database_id: Aliased<'alphafolddb' | 'cas' | 'csd' | 'emdb' | 'icsd' | 'modelarchive' | 'mdf' | 'modbase' | 'ndb' | 'nbs' | 'pdb' | 'pdb-dev' | 'pdf' | 'rcsb' | 'swiss-model_repository' | 'ebi' | 'pdbe' | 'bmrb' | 'wwpdb' | 'pdb_acc'>(lstr),
/**
* The code assigned by the database identified in
* _database_2.database_id.
@@ -884,7 +884,7 @@ export const mmCIF_Schema = {
* (DT) for Thymidine-5'-monophosphate
* (MSE) for Selenomethionine
* (SEP) for Phosphoserine
* (PTO) for Phosphothreonine
* (TPO) for Phosphothreonine
* (PTR) for Phosphotyrosine
* (PCA) for Pyroglutamic acid
* (UNK) for Unknown amino acid
@@ -2098,7 +2098,7 @@ export const mmCIF_Schema = {
/**
* The name of the database containing the related entry.
*/
db_name: str,
db_name: Aliased<'BIOISIS' | 'BMCD' | 'BMRB' | 'EMDB' | 'NDB' | 'PDB' | 'PDB-Dev' | 'SASBDB' | 'TargetDB' | 'TargetTrack'>(str),
/**
* A description of the related entry.
*/
@@ -4939,25 +4939,19 @@ export const mmCIF_Schema = {
*/
ma_model_list: {
/**
* A unique identifier for the model / model group combination.
* A unique identifier for the structural model being deposited.
*/
ordinal_id: int,
/**
* A unique identifier for the structural model being deposited.
* This data item was practically a duplicate of _ma_model_list.ordinal_id
* and has been deprecated with dictionary version 1.4.7.
*/
model_id: int,
/**
* An identifier to group structural models into collections or sets.
* A cluster of models and its representative can either be grouped together
* or can be separate groups in the ma_model_list table. The choice between
* the two options should be decided based on how the modeling was carried out
* and how the representative was chosen. If the representative is a member of
* the ensemble (i.e., best scoring model), then it is recommended that the
* representative and the ensemble belong to the same model group. If the
* representative is calculated from the ensemble (i.e., centroid), then it is
* recommended that the representative be separated into a different group.
* If the models do not need to be grouped into collections, then the
* _ma_model_list.model_group_id is the same as _ma_model_list.model_id.
* This data item has been deprecated with dictionary version 1.4.7.
* See ma_model_group category.
*/
model_group_id: int,
/**
@@ -4966,6 +4960,8 @@ export const mmCIF_Schema = {
model_name: str,
/**
* A decsriptive name for the model group.
* This data item has been deprecated with dictionary version 1.4.7.
* See ma_model_group category.
*/
model_group_name: str,
/**
@@ -5131,11 +5127,11 @@ export const mmCIF_Schema = {
/**
* The type of QA metric.
*/
type: Aliased<'zscore' | 'energy' | 'distance' | 'normalized score' | 'pLDDT' | 'pLDDT in [0,1]' | 'pLDDT all-atom' | 'pLDDT all-atom in [0,1]' | 'PAE' | 'pTM' | 'ipTM' | 'contact probability' | 'other'>(str),
type: Aliased<'zscore' | 'energy' | 'distance' | 'normalized score' | 'pLDDT' | 'pLDDT in [0,1]' | 'pLDDT all-atom' | 'pLDDT all-atom in [0,1]' | 'pLDDT to polymer' | 'PAE' | 'pTM' | 'ipTM' | 'contact probability' | 'boolean' | 'other'>(str),
/**
* The mode of calculation of the QA metric.
*/
mode: Aliased<'local' | 'global' | 'local-pairwise'>(str),
mode: Aliased<'local' | 'global' | 'local-pairwise' | 'per-feature' | 'per-feature-pair'>(str),
/**
* Identifier to the set of software used to calculate the QA metric.
* This data item is a pointer to the _ma_software_group.group_id in the
@@ -5154,7 +5150,7 @@ export const mmCIF_Schema = {
ordinal_id: int,
/**
* The identifier for the structural model, for which global QA metric is provided.
* This data item is a pointer to _ma_model_list.model_id
* This data item is a pointer to _ma_model_list.ordinal_id
* in the MA_MODEL_LIST category.
*/
model_id: int,
@@ -5172,6 +5168,10 @@ export const mmCIF_Schema = {
/**
* Data items in the MA_QA_METRIC_LOCAL category captures the
* details of the local QA metrics, calculated at the residue-level.
* Data in this category can be extracted into a separate file which
* is linked to the main file using the categories
* ma_associated_archive_file_details or ma_entry_associated_files
* with file_content set to "QA metrics".
*/
ma_qa_metric_local: {
/**
@@ -5180,7 +5180,7 @@ export const mmCIF_Schema = {
ordinal_id: int,
/**
* The identifier for the structural model, for which local QA metric is provided.
* This data item is a pointer to _ma_model_list.model_id
* This data item is a pointer to _ma_model_list.ordinal_id
* in the MA_MODEL_LIST category.
*/
model_id: int,
@@ -5219,6 +5219,15 @@ export const mmCIF_Schema = {
/**
* Data items in the MA_QA_METRIC_LOCAL_PAIRWISE category captures the
* details of the local QA metrics, calculated at the pairwise residue level.
* In cases where the metric is symmetric, it is enough to store just one value per pair.
* For asymmetric metrics, the order of residues is expected to be meaningful
* (e.g. PAE where PAE_ij is defined by aligning residue i (label_*_1) and measuring
* the error on residue j (label_*_2)).
* In all cases, it is perfectly valid to only provide values for a subset of residue pairs.
* Data in this category is expected to be very large and can be extracted into a
* separate file which is linked to the main file using the categories
* ma_associated_archive_file_details or ma_entry_associated_files with file_content
* set to "QA metrics".
*/
ma_qa_metric_local_pairwise: {
/**
@@ -5227,7 +5236,7 @@ export const mmCIF_Schema = {
ordinal_id: int,
/**
* The identifier for the structural model, for which local QA metric is provided.
* This data item is a pointer to _ma_model_list.model_id
* This data item is a pointer to _ma_model_list.ordinal_id
* in the MA_MODEL_LIST category.
*/
model_id: int,

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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -129,6 +129,12 @@ namespace Box3D {
return out;
}
export function expandUniformly(out: Box3D, box: Box3D, delta: number): Box3D {
Vec3.subScalar(out.min, box.min, delta);
Vec3.addScalar(out.max, box.max, delta);
return out;
}
export function scale(out: Box3D, box: Box3D, scale: number) {
Vec3.scale(out.min, box.min, scale);
Vec3.scale(out.max, box.max, scale);
@@ -218,6 +224,18 @@ namespace Box3D {
Vec3.scale(out, dir, tmin);
return Vec3.set(out, out[0] + x, out[1] + y, out[2] + z);
}
export function center(out: Vec3, box: Box3D): Vec3 {
return Vec3.center(out, box.max, box.min);
}
export function exactEquals(a: Box3D, b: Box3D) {
return Vec3.exactEquals(a.min, b.min) && Vec3.exactEquals(a.max, b.max);
}
export function equals(a: Box3D, b: Box3D) {
return Vec3.equals(a.min, b.min) && Vec3.equals(a.max, b.max);
}
}
export { Box3D };

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*
@@ -86,7 +86,7 @@ namespace Plane3D {
}
export function projectPoint(out: Vec3, plane: Plane3D, point: Vec3) {
return Vec3.scaleAndAdd(out, out, plane.normal, -distanceToPoint(plane, point));
return Vec3.scaleAndAdd(out, point, plane.normal, -distanceToPoint(plane, point));
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-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>
@@ -27,6 +27,7 @@ import { Vec3 } from './vec3';
import { EPSILON } from './common';
import { assertUnreachable, NumberArray } from '../../../mol-util/type-helpers';
import { Euler } from './euler';
import { Mat4 } from './mat4';
interface Quat extends Array<number> { [d: number]: number, '@type': 'quat', length: 4 }
interface ReadonlyQuat extends Array<number> { readonly [d: number]: number, '@type': 'quat', length: 4 }
@@ -282,6 +283,12 @@ namespace Quat {
return out;
}
const m3tmp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] as unknown as Mat3;
export function fromMat4(out: Quat, m: Mat4) {
Mat3.fromMat4(m3tmp, m);
return fromMat3(out, m3tmp);
}
export function fromEuler(out: Quat, euler: Euler, order: Euler.Order) {
const [x, y, z] = euler;
@@ -367,6 +374,12 @@ namespace Quat {
return out;
}
const m4tmp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] as unknown as Mat4;
export function fromBasis(out: Quat, x: Vec3, y: Vec3, z: Vec3) {
Mat4.fromBasis(m4tmp, x, y, z);
return fromMat4(out, m4tmp);
}
export function clone(a: Quat) {
const out = zero();
out[0] = a[0];

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-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>
@@ -584,8 +584,8 @@ namespace Vec3 {
const by = angle(a, b);
if (Math.abs(by) < 0.0001) return Mat4.setIdentity(mat);
if (Math.abs(by - Math.PI) < EPSILON) {
// here, axis can be [0,0,0] but the rotation is a simple flip
return Mat4.fromScaling(mat, negUnit);
// choose arbitrary orthogonal axis
return Mat4.fromRotation(mat, Math.PI, Math.abs(a[0]) < 0.9 ? Vec3.unitX : Vec3.unitZ);
}
const axis = cross(rotTemp, a, b);
return Mat4.fromRotation(mat, by, axis);
@@ -645,6 +645,11 @@ namespace Vec3 {
return normalize(out, cross(out, triangleNormalTmpAB, triangleNormalTmpAC));
}
const centerTmpV = zero();
export function center(out: Vec3, a: Vec3, b: Vec3): Vec3 {
return Vec3.scaleAndAdd(out, a, Vec3.sub(centerTmpV, b, a), 0.5);
}
export function toString(a: Vec3, precision?: number) {
return `[${a[0].toPrecision(precision)} ${a[1].toPrecision(precision)} ${a[2].toPrecision(precision)}]`;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 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>
@@ -16,6 +16,7 @@ import { ElementIndex, ChainIndex } from '../../../mol-model/structure/model/ind
import { getCoarseRanges } from '../../../mol-model/structure/model/properties/utils/coarse-ranges';
import { IhmSphereObjSite, IhmGaussianObjSite, AtomSite, BasicSchema } from './schema';
import { Model } from '../../../mol-model/structure';
import { getCoarseIndex } from '../../../mol-model/structure/model/properties/utils/coarse-index';
export interface CoarseData {
model_id: number,
@@ -50,6 +51,7 @@ export function getCoarse(data: CoarseData, chemicalComponentMap: Model['propert
isDefined: true,
spheres: { ...sphereData, ...sphereKeys, ...sphereRanges },
gaussians: { ...gaussianData, ...gaussianKeys, ...gaussianRanges },
index: getCoarseIndex({ spheres: sphereData, gaussians: gaussianData })
},
conformation: {
id: UUID.create22(),

View File

@@ -15,6 +15,7 @@ import { AccessibleSurfaceArea } from '../accessible-surface-area/shrake-rupley'
import { CustomProperty } from '../../common/custom-property';
import { Location } from '../../../mol-model/location';
import { hash2 } from '../../../mol-data/util';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
const DefaultColor = Color(0xFAFAFA);
const Description = 'Assigns a color based on the relative accessible surface area of a residue.';
@@ -76,7 +77,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<AccessibleSurfaceAreaColorThemeParams, 'accessible-surface-area'> = {
name: 'accessible-surface-area',
label: 'Accessible Surface Area',
category: ColorTheme.Category.Residue,
category: ColorThemeCategory.Residue,
factory: AccessibleSurfaceAreaColorTheme,
getParams: getAccessibleSurfaceAreaColorThemeParams,
defaultValues: PD.getDefaultValues(AccessibleSurfaceAreaColorThemeParams),

View File

@@ -15,6 +15,7 @@ import { TableLegend } from '../../../mol-util/legend';
import { Interactions } from '../interactions/interactions';
import { CustomProperty } from '../../common/custom-property';
import { hash2 } from '../../../mol-data/util';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
const DefaultColor = Color(0xCCCCCC);
const Description = 'Assigns colors according the interaction type of a link.';
@@ -110,7 +111,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionTypeColorThemeParams, 'interaction-type'> = {
name: 'interaction-type',
label: 'Interaction Type',
category: ColorTheme.Category.Misc,
category: ColorThemeCategory.Misc,
factory: InteractionTypeColorTheme,
getParams: getInteractionTypeColorThemeParams,
defaultValues: PD.getDefaultValues(InteractionTypeColorThemeParams),

View File

@@ -11,6 +11,7 @@ import { ThemeDataContext } from '../../../mol-theme/theme';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { CustomProperty } from '../../common/custom-property';
import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property';
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
const DefaultColor = Color(0xCCCCCC);
const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. modeled / `ihm_cross_link_restraint.distance_threshold`).';
@@ -63,7 +64,7 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros
export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThemeParams, 'cross-link'> = {
name: 'cross-link',
label: 'Cross Link',
category: ColorTheme.Category.Misc,
category: ColorThemeCategory.Misc,
factory: CrossLinkColorTheme,
getParams: getCrossLinkColorThemeParams,
defaultValues: PD.getDefaultValues(CrossLinkColorThemeParams),

View File

@@ -7,6 +7,7 @@
import { Location } from '../../../mol-model/location';
import { Bond, StructureElement, Unit } 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 { getPalette, getPaletteParams } from '../../../mol-util/color/palette';
@@ -82,7 +83,7 @@ export function SIFTSMappingColorTheme(ctx: ThemeDataContext, props: PD.Values<S
export const SIFTSMappingColorThemeProvider: ColorTheme.Provider<SIFTSMappingColorThemeParams, 'sifts-mapping'> = {
name: 'sifts-mapping',
label: 'SIFTS Mapping',
category: ColorTheme.Category.Residue,
category: ColorThemeCategory.Residue,
factory: SIFTSMappingColorTheme,
getParams: getSIFTSMappingColorThemeParams,
defaultValues: PD.getDefaultValues(SIFTSMappingColorThemeParams),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 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>
*/
@@ -31,4 +31,17 @@ export function isDataLocation(x: any): x is DataLocation {
return !!x && x.kind === 'data-location';
}
export type Location = StructureElement.Location | Bond.Location | ShapeGroup.Location | PositionLocation | DataLocation | NullLocation | Volume.Segment.Location
/**
* A direct Location.
*
* For it, the location is implicitly clear from context and is not explicitly given.
* This is used for themes with direct-volume rendering where the location is the volume
* grid cell itself and coloring is applied in a shader on the GPU.
*/
export const DirectLocation = { kind: 'direct-location' as const };
export type DirectLocation = typeof DirectLocation
export function isDirectLocation(x: any): x is DirectLocation {
return !!x && x.kind === 'direct-location';
}
export type Location = StructureElement.Location | Bond.Location | ShapeGroup.Location | PositionLocation | DataLocation | NullLocation | DirectLocation | Volume.Cell.Location | Volume.Segment.Location

View File

@@ -9,6 +9,7 @@ import { Column } from '../../../../../mol-data/db';
import { Segmentation } from '../../../../../mol-data/int';
import { ElementIndex, ChainIndex, EntityIndex } from '../../indexing';
import { SortedRanges } from '../../../../../mol-data/int/sorted-ranges';
import { EmptyCoarseIndex } from '../utils/coarse-index';
export interface CoarsedElementKeys {
/** Assign a key to each element */
@@ -60,7 +61,8 @@ export type CoarseElements = CoarsedElementKeys & CoarseElementData & CoarseRang
export interface CoarseHierarchy {
isDefined: boolean,
spheres: CoarseElements,
gaussians: CoarseElements
gaussians: CoarseElements,
index: CoarseIndex
}
const EmptyCoarseElements: CoarseElements = {
@@ -81,10 +83,39 @@ const EmptyCoarseElements: CoarseElements = {
gapRanges: SortedRanges.ofSortedRanges([]),
};
export interface CoarseIndex {
/**
* Find element index of a sphere
* @param key
* @returns index or -1 if the atom is not present.
*/
findSphereElement(key: CoarseElementKey): ElementIndex
/**
* Find element index of a gaussian
* @param key
* @returns index or -1 if the atom is not present.
*/
findGaussianElement(key: CoarseElementKey): ElementIndex
/**
* Finds coarse element and assigns a reference to it.
* @param key
*/
findElement(key: CoarseElementKey, out: CoarseElementReference): boolean
}
export interface CoarseElementReference { kind?: 'spheres' | 'gaussians', index: ElementIndex }
export function CoarseElementReference(): CoarseElementReference { return { kind: undefined, index: -1 as ElementIndex }; }
export interface CoarseElementKey { label_entity_id: string, label_asym_id: string, label_seq_id: number }
export function CoarseElementKey(): CoarseElementKey { return { label_entity_id: '', label_asym_id: '', label_seq_id: -1 }; }
export namespace CoarseHierarchy {
export const Empty: CoarseHierarchy = {
isDefined: false,
spheres: EmptyCoarseElements,
gaussians: EmptyCoarseElements
gaussians: EmptyCoarseElements,
index: EmptyCoarseIndex,
};
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { ElementIndex } from '../../indexing';
import { CoarseElementData, CoarseElementReference, CoarseIndex, CoarseElementKey } from '../coarse';
export function getCoarseIndex(data: { spheres: CoarseElementData, gaussians: CoarseElementData }): CoarseIndex {
return new Index(data);
}
class EmptyIndex implements CoarseIndex {
findElement(key: CoarseElementKey, out: CoarseElementReference): boolean {
out.kind = undefined;
out.index = -1 as ElementIndex;
return false;
}
findSphereElement(key: CoarseElementKey): ElementIndex {
return -1 as ElementIndex;
}
findGaussianElement(key: CoarseElementKey): ElementIndex {
return -1 as ElementIndex;
}
}
export const EmptyCoarseIndex: CoarseIndex = new EmptyIndex();
class Index implements CoarseIndex {
private _sphereMapping: CoarseElementMapping | undefined = void 0;
private _gaussianMapping: CoarseElementMapping | undefined = void 0;
get sphereMapping() {
if (!this._sphereMapping) this._sphereMapping = buildMapping(this.data.spheres);
return this._sphereMapping;
}
get gaussianMapping() {
if (!this._gaussianMapping) this._gaussianMapping = buildMapping(this.data.gaussians);
return this._gaussianMapping;
}
findSphereElement(key: CoarseElementKey): ElementIndex {
const mapping = this.sphereMapping;
let xs: any = mapping[key.label_entity_id];
if (!xs) return -1 as ElementIndex;
xs = xs[key.label_asym_id];
if (!xs) return -1 as ElementIndex;
return xs[key.label_seq_id] ?? -1;
}
findGaussianElement(key: CoarseElementKey): ElementIndex {
const mapping = this.gaussianMapping;
let xs: any = mapping[key.label_entity_id];
if (!xs) return -1 as ElementIndex;
xs = xs[key.label_asym_id];
if (!xs) return -1 as ElementIndex;
return xs[key.label_seq_id] ?? -1;
}
findElement(key: CoarseElementKey, out: CoarseElementReference): boolean {
const sphere = this.findSphereElement(key);
if (sphere >= 0) {
out.kind = 'spheres';
out.index = sphere;
return true;
}
const gaussian = this.findGaussianElement(key);
if (gaussian >= 0) {
out.kind = 'gaussians';
out.index = gaussian;
return true;
}
return false;
}
constructor(private data: { spheres: CoarseElementData, gaussians: CoarseElementData }) {
}
}
type CoarseElementMapping = { [entityId: string]: { [chainId: string]: { [seqId: number]: ElementIndex } } };
function buildMapping({ count, entity_id, asym_id, seq_id_begin, seq_id_end }: CoarseElementData): CoarseElementMapping {
const ret: CoarseElementMapping = {};
for (let i = 0; i < count; i++) {
const entityId = entity_id.value(i);
const asymId = asym_id.value(i);
if (!ret[entityId]) ret[entityId] = {};
if (!ret[entityId][asymId]) ret[entityId][asymId] = {};
const elements = ret[entityId][asymId];
const seqIdBegin = seq_id_begin.value(i);
const seqIdEnd = seq_id_end.value(i);
for (let seqId = seqIdBegin; seqId <= seqIdEnd; seqId++) {
elements[seqId] = i as ElementIndex;
}
}
return ret;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-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>
@@ -112,9 +112,9 @@ const residue = {
const chain = {
key: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.chainIndex[l.element]),
label_asym_id: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.label_asym_id.value(l.unit.chainIndex[l.element])),
label_asym_id: p(l => !Unit.isAtomic(l.unit) ? l.unit.coarseElements.asym_id.value(l.element) : l.unit.model.atomicHierarchy.chains.label_asym_id.value(l.unit.chainIndex[l.element])),
auth_asym_id: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.auth_asym_id.value(l.unit.chainIndex[l.element])),
label_entity_id: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.chains.label_entity_id.value(l.unit.chainIndex[l.element]))
label_entity_id: p(l => !Unit.isAtomic(l.unit) ? l.unit.coarseElements.entity_id.value(l.element) : l.unit.model.atomicHierarchy.chains.label_entity_id.value(l.unit.chainIndex[l.element]))
};
const coarse = {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 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>
@@ -8,6 +8,7 @@
import { SpacegroupCell, Box3D, Sphere3D } from '../../mol-math/geometry';
import { Tensor, Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { Histogram, calculateHistogram } from '../../mol-math/histogram';
import { lerp } from '../../mol-math/interpolate';
/** The basic unit cell that contains the grid data. */
interface Grid {
@@ -76,6 +77,59 @@ namespace Grid {
}
return histograms[binCount];
}
export function makeGetTrilinearlyInterpolated(grid: Grid, transform: 'none' | 'relative') {
const cartnToGrid = Grid.getGridToCartesianTransform(grid);
Mat4.invert(cartnToGrid, cartnToGrid);
const gridCoords = Vec3();
const { stats } = grid;
const { dimensions, get } = grid.cells.space;
const data = grid.cells.data;
const [mi, mj, mk] = dimensions;
return function getTrilinearlyInterpolated(position: Vec3): number {
Vec3.copy(gridCoords, position);
Vec3.transformMat4(gridCoords, gridCoords, cartnToGrid);
const i = Math.trunc(gridCoords[0]);
const j = Math.trunc(gridCoords[1]);
const k = Math.trunc(gridCoords[2]);
if (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk) {
return Number.NaN;
}
const u = gridCoords[0] - i;
const v = gridCoords[1] - j;
const w = gridCoords[2] - k;
// Tri-linear interpolation for the value
const ii = Math.min(i + 1, mi - 1);
const jj = Math.min(j + 1, mj - 1);
const kk = Math.min(k + 1, mk - 1);
let a = get(data, i, j, k);
let b = get(data, ii, j, k);
let c = get(data, i, jj, k);
let d = get(data, ii, jj, k);
const x = lerp(lerp(a, b, u), lerp(c, d, u), v);
a = get(data, i, j, kk);
b = get(data, ii, j, kk);
c = get(data, i, jj, kk);
d = get(data, ii, jj, kk);
const y = lerp(lerp(a, b, u), lerp(c, d, u), v);
const value = lerp(x, y, w);
if (transform === 'relative') {
return (value - stats.mean) / stats.sigma;
} else {
return value;
}
};
}
}
export { Grid };

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2022 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>
*/
@@ -220,6 +220,18 @@ export namespace Volume {
export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume && OrderedSet.areEqual(a.indices, b.indices); }
export function isLociEmpty(loci: Loci) { return OrderedSet.size(loci.indices) === 0; }
export interface Location {
readonly kind: 'cell-location',
volume: Volume
cell: CellIndex
}
export function Location(volume?: Volume, cell?: CellIndex): Location {
return { kind: 'cell-location', volume: volume as any, cell: cell as any };
}
export function isLocation(x: any): x is Location {
return !!x && x.kind === 'cell-location';
}
const boundaryHelper = new BoundaryHelper('98');
const tmpBoundaryPos = Vec3();
export function getBoundingSphere(volume: Volume, indices: OrderedSet<CellIndex>, boundingSphere?: Sphere3D) {

View File

@@ -63,13 +63,13 @@ const DownloadStructure = StateAction.build({
}, { pivot: 'id' }),
options
}, { isFlat: true, label: 'PDB' }),
'pdb-dev': PD.Group({
'pdb-ihm': PD.Group({
provider: PD.Group({
id: PD.Text('PDBDEV_00000001', { label: 'PDB-Dev Id(s)', description: 'One or more comma/space separated ids.' }),
id: PD.Text('8zzc', { label: 'PDB-IHM Id(s)', description: 'One or more comma/space separated ids.' }),
encoding: PD.Select('bcif', PD.arrayToOptions(['cif', 'bcif'] as const)),
}, { pivot: 'id' }),
options
}, { isFlat: true, label: 'PDB-Dev' }),
}, { isFlat: true, label: 'PDB-IHM' }),
'swissmodel': PD.Group({
id: PD.Text('Q9Y2I8', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }),
options
@@ -124,22 +124,22 @@ const DownloadStructure = StateAction.build({
);
asTrajectory = !!src.params.options.asTrajectory;
break;
case 'pdb-dev':
case 'pdb-ihm':
const map = (id: string) => id.startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
downloadParams = await getDownloadParams(src.params.provider.id,
id => {
// 4 character PDB id, TODO: support extended PDB ID
if (id.match(/^[1-9][A-Z0-9]{3}$/i) !== null) {
return src.params.provider.encoding === 'bcif'
? `https://pdb-dev.wwpdb.org/bcif/${id.toLowerCase()}.bcif`
: `https://pdb-dev.wwpdb.org/cif/${id.toLowerCase()}.cif`;
? `https://pdb-ihm.org/bcif/${id.toLowerCase()}.bcif`
: `https://pdb-ihm.org/cif/${id.toLowerCase()}.cif`;
}
const nId = map(id.toUpperCase());
return src.params.provider.encoding === 'bcif'
? `https://pdb-dev.wwpdb.org/bcif/${nId}.bcif`
: `https://pdb-dev.wwpdb.org/cif/${nId}.cif`;
? `https://pdb-ihm.org/bcif/${nId}.bcif`
: `https://pdb-ihm.org/cif/${nId}.cif`;
},
id => { const nId = id.toUpperCase(); return nId.match(/^[1-9][A-Z0-9]{3}$/) ? `PDB-Dev: ${nId}` : map(nId); },
id => { const nId = id.toUpperCase(); return nId.match(/^[1-9][A-Z0-9]{3}$/) ? `PDB-IHM: ${nId}` : map(nId); },
src.params.provider.encoding === 'bcif'
);
asTrajectory = !!src.params.options.asTrajectory;
@@ -235,7 +235,7 @@ async function getPdbeDownloadParams(src: ReturnType<DownloadStructure['createDe
async function getPdbjDownloadParams(src: ReturnType<DownloadStructure['createDefaultParams']>['source']) {
if (src.name !== 'pdb' || src.params.provider.server.name !== 'pdbj') throw new Error('expected pdbj');
return getDownloadParams(src.params.provider.id, id => `https://data.pdbjbk1.pdbj.org/pub/pdb/data/structures/divided/mmCIF/${id.toLowerCase().substring(1, 3)}/${id.toLowerCase()}.cif`, id => `PDBj: ${id} (cif)`, false);
return getDownloadParams(src.params.provider.id, id => `https://data.pdbjlc1.pdbj.org/pub/pdb/data/structures/divided/mmCIF/${id.toLowerCase().substring(1, 3)}/${id.toLowerCase()}.cif`, id => `PDBj: ${id} (cif)`, false);
}
async function getRcsbDownloadParams(src: ReturnType<DownloadStructure['createDefaultParams']>['source']) {

View File

@@ -1,8 +1,9 @@
/**
* 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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/
import { PresetProvider } from '../preset-provider';
@@ -121,7 +122,7 @@ const auto = StructureRepresentationPresetProvider({
params: () => CommonParams,
apply(ref, params, plugin) {
const structure = StateObjectRef.resolveAndCheck(plugin.state.data, ref)?.obj?.data;
if (!structure) return { };
if (!structure) return {};
const thresholds = plugin.config.get(PluginConfig.Structure.SizeThresholds) || Structure.DefaultSizeThresholds;
const size = Structure.getSize(structure, thresholds);
@@ -151,7 +152,7 @@ const empty = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-empty',
display: { name: 'Empty', description: 'Removes all existing representations.' },
async apply(ref, params, plugin) {
return { };
return {};
}
});
@@ -396,12 +397,9 @@ const illustrative = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-illustrative',
display: {
name: 'Illustrative', group: 'Miscellaneous',
description: '...'
description: 'Show everything in spacefill representation with illustrative colors and ignore light.'
},
params: () => ({
...CommonParams,
showCarbohydrateSymbol: PD.Boolean(false)
}),
params: () => CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
if (!structureCell) return {};
@@ -416,7 +414,47 @@ const illustrative = StructureRepresentationPresetProvider({
const { update, builder, typeParams, color } = reprBuilder(plugin, params, structure);
const representations = {
all: builder.buildRepresentation(update, components.all, { type: 'spacefill', typeParams: { ...typeParams, ignoreLight: true }, color: 'illustrative' }, { tag: 'all' }),
all: builder.buildRepresentation(update, components.all, {
type: 'spacefill',
typeParams: { ...typeParams, ignoreLight: true },
color: 'illustrative',
colorParams: { style: { name: 'entity-id', params: { overrideWater: true } } },
}, { tag: 'all' }),
};
await update.commit({ revertOnError: true });
await updateFocusRepr(plugin, structure, params.theme?.focus?.name ?? color, params.theme?.focus?.params);
return { components, representations };
}
});
const molecularSurface = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-molecular-surface',
display: {
name: 'Molecular Surface', group: 'Miscellaneous',
description: 'Show everything in molecular surface representation with illustrative colors.'
},
params: () => CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
if (!structureCell) return {};
const components = {
all: await presetStaticComponent(plugin, structureCell, 'all'),
branched: undefined
};
const structure = structureCell.obj!.data;
const { update, builder, typeParams, color } = reprBuilder(plugin, params, structure);
const representations = {
all: builder.buildRepresentation(update, components.all, {
type: 'molecular-surface',
typeParams,
color: 'entity-id',
colorParams: { overrideWater: true },
}, { tag: 'all' }),
};
await update.commit({ revertOnError: true });
await updateFocusRepr(plugin, structure, params.theme?.focus?.name ?? color, params.theme?.focus?.params);
@@ -474,6 +512,7 @@ export const PresetStructureRepresentations = {
'protein-and-nucleic': proteinAndNucleic,
'coarse-surface': coarseSurface,
illustrative,
'molecular-surface': molecularSurface,
'auto-lod': autoLod,
};
export type PresetStructureRepresentations = typeof PresetStructureRepresentations;

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