Compare commits

...

27 Commits

Author SHA1 Message Date
dsehnal
4ac6f5c202 5.5.0 2025-12-22 12:50:36 +01:00
dsehnal
5726515707 changelog 2025-12-22 12:48:58 +01:00
Alexander Rose
f2ee7d1470 fix unit hash collision (#1729)
* fix unit hash collision

* cleanup

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

* subscribe method on the Viewer object

* export lib

* Add viewer.structureInteraction

* MVS tryGetPrimitivesFromLoci util

* param select indicator

* tweaks

* mapped control icon

* docs and tweaks

* viewer color themes

* tweak

* typo

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

* Add TM-align structure alignment algorithm

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

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

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

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

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

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

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

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

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

* changing reference test case

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

* Addressing comments in pull request

* Authorship added

---------

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

* Changelog + file header

* Address PR comments

* Remove residue-name.ts contribution info

* PR suggestion

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

View File

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

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

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

View File

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

View File

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

View File

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

412
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "molstar",
"version": "5.4.2",
"version": "5.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "molstar",
"version": "5.4.2",
"version": "5.5.0",
"license": "MIT",
"dependencies": {
"@types/argparse": "^2.0.17",
"@types/benchmark": "^2.1.5",
"@types/compression": "1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^20.19.25",
"@types/node": "^20.19.27",
"@types/node-fetch": "^2.6.13",
"@types/swagger-ui-dist": "3.30.6",
"argparse": "^2.0.1",
@@ -28,7 +28,7 @@
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"rxjs": "^7.8.2",
"swagger-ui-dist": "^5.30.3",
"swagger-ui-dist": "^5.31.0",
"tslib": "^2.8.1",
"util.promisify": "^1.1.3"
},
@@ -50,26 +50,26 @@
"@types/gl": "^6.0.5",
"@types/jest": "^30.0.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.26",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@types/webxr": "^0.5.24",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@typescript-eslint/eslint-plugin": "^8.50.0",
"@typescript-eslint/parser": "^8.50.0",
"benchmark": "^2.1.4",
"concurrently": "^9.2.1",
"cpx2": "^8.0.0",
"css-loader": "^7.1.2",
"esbuild": "^0.27.1",
"esbuild": "^0.27.2",
"esbuild-jest-transform": "^2.0.1",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^9.39.1",
"fs-extra": "^11.3.2",
"eslint": "^9.39.2",
"fs-extra": "^11.3.3",
"http-server": "^14.1.1",
"jest": "^30.2.0",
"jpeg-js": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.94.2",
"sass": "^1.97.1",
"simple-git": "^3.30.0",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
@@ -663,9 +663,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
"integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
@@ -680,9 +680,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
"integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
@@ -697,9 +697,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
"integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
@@ -714,9 +714,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
"integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
@@ -731,9 +731,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
"integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
@@ -748,9 +748,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
"integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
@@ -765,9 +765,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
"integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
@@ -782,9 +782,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
"integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
@@ -799,9 +799,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
"integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
@@ -816,9 +816,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
"integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
@@ -833,9 +833,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
"integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
@@ -850,9 +850,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
"integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
@@ -867,9 +867,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
"integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
@@ -884,9 +884,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
"integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
@@ -901,9 +901,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
"integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
@@ -918,9 +918,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
"integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
@@ -935,9 +935,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
"integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
@@ -952,9 +952,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
"integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
@@ -969,9 +969,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
"integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
@@ -986,9 +986,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
"integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
@@ -1003,9 +1003,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
"integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
@@ -1020,9 +1020,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
"integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
@@ -1037,9 +1037,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
"integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
@@ -1054,9 +1054,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
"integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
@@ -1071,9 +1071,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
"integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
@@ -1088,9 +1088,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
"integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
@@ -1199,9 +1199,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1211,7 +1211,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
@@ -1257,9 +1257,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2629,9 +2629,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"version": "20.19.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -2676,14 +2676,14 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
@@ -2759,18 +2759,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
"integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.48.1",
"@typescript-eslint/type-utils": "8.48.1",
"@typescript-eslint/utils": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1",
"graphemer": "^1.4.0",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/type-utils": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
@@ -2783,23 +2782,23 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.48.1",
"@typescript-eslint/parser": "^8.50.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz",
"integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.1",
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/typescript-estree": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2815,14 +2814,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz",
"integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.48.1",
"@typescript-eslint/types": "^8.48.1",
"@typescript-eslint/tsconfig-utils": "^8.50.0",
"@typescript-eslint/types": "^8.50.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2837,14 +2836,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz",
"integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1"
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2855,9 +2854,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz",
"integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2872,15 +2871,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz",
"integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/typescript-estree": "8.48.1",
"@typescript-eslint/utils": "8.48.1",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -2897,9 +2896,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz",
"integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2911,16 +2910,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz",
"integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.48.1",
"@typescript-eslint/tsconfig-utils": "8.48.1",
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1",
"@typescript-eslint/project-service": "8.50.0",
"@typescript-eslint/tsconfig-utils": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
@@ -2939,16 +2938,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz",
"integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.48.1",
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/typescript-estree": "8.48.1"
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2963,13 +2962,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz",
"integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/types": "8.50.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -4441,9 +4440,9 @@
}
},
"node_modules/csstype": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz",
"integrity": "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/data-view-buffer": {
@@ -4900,9 +4899,9 @@
}
},
"node_modules/esbuild": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
"integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -4914,32 +4913,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.1",
"@esbuild/android-arm": "0.27.1",
"@esbuild/android-arm64": "0.27.1",
"@esbuild/android-x64": "0.27.1",
"@esbuild/darwin-arm64": "0.27.1",
"@esbuild/darwin-x64": "0.27.1",
"@esbuild/freebsd-arm64": "0.27.1",
"@esbuild/freebsd-x64": "0.27.1",
"@esbuild/linux-arm": "0.27.1",
"@esbuild/linux-arm64": "0.27.1",
"@esbuild/linux-ia32": "0.27.1",
"@esbuild/linux-loong64": "0.27.1",
"@esbuild/linux-mips64el": "0.27.1",
"@esbuild/linux-ppc64": "0.27.1",
"@esbuild/linux-riscv64": "0.27.1",
"@esbuild/linux-s390x": "0.27.1",
"@esbuild/linux-x64": "0.27.1",
"@esbuild/netbsd-arm64": "0.27.1",
"@esbuild/netbsd-x64": "0.27.1",
"@esbuild/openbsd-arm64": "0.27.1",
"@esbuild/openbsd-x64": "0.27.1",
"@esbuild/openharmony-arm64": "0.27.1",
"@esbuild/sunos-x64": "0.27.1",
"@esbuild/win32-arm64": "0.27.1",
"@esbuild/win32-ia32": "0.27.1",
"@esbuild/win32-x64": "0.27.1"
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/esbuild-jest-transform": {
@@ -4998,9 +4997,9 @@
}
},
"node_modules/eslint": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -5011,7 +5010,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.1",
"@eslint/js": "9.39.2",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -5635,9 +5634,9 @@
}
},
"node_modules/fs-extra": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5972,13 +5971,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/h264-mp4-encoder": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/h264-mp4-encoder/-/h264-mp4-encoder-1.0.12.tgz",
@@ -10246,9 +10238,9 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.94.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
"version": "1.97.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11321,9 +11313,9 @@
}
},
"node_modules/swagger-ui-dist": {
"version": "5.30.3",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz",
"integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==",
"version": "5.31.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz",
"integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.4.2",
"version": "5.5.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -123,7 +123,8 @@
"Chetan Mishra <chetan.s115@gmail.com>",
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
"Kim Juho <juho_kim@outlook.com>",
"Victoria Doshchenko <doshchenko.victoria@gmail.com>"
"Victoria Doshchenko <doshchenko.victoria@gmail.com>",
"Diego del Alamo <diego.delalamo@gmail.com>"
],
"license": "MIT",
"devDependencies": {
@@ -131,26 +132,26 @@
"@types/gl": "^6.0.5",
"@types/jest": "^30.0.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.26",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@types/webxr": "^0.5.24",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@typescript-eslint/eslint-plugin": "^8.50.0",
"@typescript-eslint/parser": "^8.50.0",
"benchmark": "^2.1.4",
"concurrently": "^9.2.1",
"cpx2": "^8.0.0",
"css-loader": "^7.1.2",
"esbuild": "^0.27.1",
"esbuild": "^0.27.2",
"esbuild-jest-transform": "^2.0.1",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^9.39.1",
"fs-extra": "^11.3.2",
"eslint": "^9.39.2",
"fs-extra": "^11.3.3",
"http-server": "^14.1.1",
"jest": "^30.2.0",
"jpeg-js": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.94.2",
"sass": "^1.97.1",
"simple-git": "^3.30.0",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
@@ -160,7 +161,7 @@
"@types/benchmark": "^2.1.5",
"@types/compression": "1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^20.19.25",
"@types/node": "^20.19.27",
"@types/node-fetch": "^2.6.13",
"@types/swagger-ui-dist": "3.30.6",
"argparse": "^2.0.1",
@@ -175,7 +176,7 @@
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"rxjs": "^7.8.2",
"swagger-ui-dist": "^5.30.3",
"swagger-ui-dist": "^5.31.0",
"tslib": "^2.8.1",
"util.promisify": "^1.1.3"
},

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ludovic Autin <ludovic.autin@gmail.com>
@@ -36,6 +36,12 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
visuals: [merge ? 'structure-element-sphere' : 'element-sphere'],
interior: {
color: Color.fromNormalizedRgb(0, 0, 0),
colorStrength: 0.15,
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
substanceStrength: 1,
}
},
},
colorTheme: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -95,7 +95,10 @@ async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSDat
options
);
await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length);
entries.push(entry);
entries.push({
...entry,
_transientData: { sourceMvsSnapshot: snapshot }
});
if (ctx.shouldUpdate) {
await ctx.update({ message: 'Loading MVS...', current: i, max: multiData.snapshots.length });

View File

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

View File

@@ -96,6 +96,7 @@ export const Canvas3DParams = {
cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
transparentBackground: PD.Boolean(false),
checkeredTransparentBackground: PD.Boolean(false),
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
@@ -405,6 +406,22 @@ const cancelAnimationFrame = typeof window !== 'undefined'
? window.cancelAnimationFrame
: (handle: number) => clearImmediate(handle as unknown as NodeJS.Immediate);
function syncCanvasBackground(canvas: HTMLCanvasElement, canvasProps: Canvas3DProps) {
if (canvasProps.transparentBackground && canvasProps.checkeredTransparentBackground) {
Object.assign(canvas.style, {
'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)',
'background-size': '60px 60px',
'background-position': '0 0, 30px 30px'
});
} else {
Object.assign(canvas.style, {
'background-image': '',
'background-size': '',
'background-position': ''
});
}
}
namespace Canvas3D {
export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
@@ -435,6 +452,7 @@ namespace Canvas3D {
let forceNextRender = false;
let currentTime = 0;
syncCanvasBackground(canvas!, p);
updateViewport();
const scene = Scene.create(webgl, passes.draw.transparency, {
dColorMarker: p.renderer.colorMarker,
@@ -1061,6 +1079,7 @@ namespace Canvas3D {
cameraResetDurationMs: p.cameraResetDurationMs,
sceneRadiusFactor: p.sceneRadiusFactor,
transparentBackground: p.transparentBackground,
checkeredTransparentBackground: p.checkeredTransparentBackground,
dpoitIterations: p.dpoitIterations,
pickPadding: p.pickPadding,
userInteractionReleaseMs: p.userInteractionReleaseMs,
@@ -1311,6 +1330,7 @@ namespace Canvas3D {
}
if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
if (props.checkeredTransparentBackground !== undefined) p.checkeredTransparentBackground = props.checkeredTransparentBackground;
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
if (props.pickPadding !== undefined) {
p.pickPadding = props.pickPadding;
@@ -1357,6 +1377,12 @@ namespace Canvas3D {
p.camera.stereo.name = 'off';
}
if ('transparentBackground' in props
|| 'checkeredTransparentBackground' in props
|| (props.renderer && 'backgroundColor' in props.renderer)) {
syncCanvasBackground(canvas!, p);
}
shaderManager.updateRequired(p);
if (!doNotRequestDraw) {
requestDraw();

View File

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

View File

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

View File

@@ -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>
@@ -28,6 +28,7 @@ import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
import { RenderableState } from '../../../mol-gl/renderable';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
export interface Cylinders {
readonly kind: 'cylinders',
@@ -174,6 +175,7 @@ export namespace Cylinders {
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
interior: getInteriorParam(),
colorMode: PD.Select('default', PD.arrayToOptions(['default', 'interpolate'] as const), BaseGeometry.ShadingCategory)
};
export type Params = typeof Params
@@ -266,6 +268,8 @@ export namespace Cylinders {
dSolidInterior: ValueCell.create(props.solidInterior),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
dDualColor: ValueCell.create(props.colorMode === 'interpolate'),
};
}
@@ -287,6 +291,8 @@ export namespace Cylinders {
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
ValueCell.updateIfChanged(values.dDualColor, props.colorMode === 'interpolate');
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
import { Color } from '../../mol-util/color/color';
import { Material } from '../../mol-util/material';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
export function getInteriorParam() {
return PD.Group({
color: PD.Color(Color.fromRgb(76, 76, 76)),
colorStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
substance: Material.getParam(),
substanceStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
});
}
export type InteriorProp = ReturnType<typeof getInteriorParam>['defaultValue'];
export function areInteriorPropsEquals(a: InteriorProp, b: InteriorProp): boolean {
return a.color === b.color
&& a.colorStrength === b.colorStrength
&& Material.areEqual(a.substance, b.substance)
&& a.substanceStrength === b.substanceStrength;
}
export function getInteriorColor(props: InteriorProp, out: Vec4): Vec4 {
Color.toArrayNormalized(props.color, out, 0);
out[3] = props.colorStrength;
return out;
}
export function getInteriorSubstance(props: InteriorProp, out: Vec4): Vec4 {
Material.toArrayNormalized(props.substance, out, 0);
out[3] = props.substanceStrength;
return out;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-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>
@@ -29,6 +29,7 @@ import { arraySetAdd } from '../../../mol-util/array';
import { degToRad } from '../../../mol-math/misc';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
export interface Mesh {
readonly kind: 'mesh',
@@ -633,6 +634,7 @@ export namespace Mesh {
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
interior: getInteriorParam(),
};
export type Params = typeof Params
@@ -719,6 +721,8 @@ export namespace Mesh {
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
meta: ValueCell.create(mesh.meta),
};
@@ -741,6 +745,8 @@ export namespace Mesh {
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
}
function updateBoundingSphere(values: MeshValues, mesh: Mesh) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 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>
*/
@@ -27,6 +27,7 @@ import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
import { RenderableState } from '../../../mol-gl/renderable';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
export interface Spheres {
readonly kind: 'spheres',
@@ -256,6 +257,7 @@ export namespace Spheres {
alphaThickness: PD.Numeric(0, { min: 0, max: 20, step: 1 }, { ...BaseGeometry.ShadingCategory, description: 'If not zero, adjusts alpha for radius.' }),
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
interior: getInteriorParam(),
lodLevels: PD.ObjectList({
minDistance: PD.Numeric(0),
maxDistance: PD.Numeric(0),
@@ -356,6 +358,8 @@ export namespace Spheres {
uAlphaThickness: ValueCell.create(props.alphaThickness),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
lodLevels: spheres.shaderData.lodLevels,
centerBuffer: spheres.centerBuffer,
@@ -383,6 +387,8 @@ export namespace Spheres {
ValueCell.updateIfChanged(values.uAlphaThickness, props.alphaThickness);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
const lodLevels = getLodLevels(values.lodLevels.ref.value as LodLevelsValue);
if (!areLodLevelsEqual(props.lodLevels, lodLevels)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,15 +17,14 @@ precision highp int;
#include normal_frag_params
#include common_clip
uniform vec4 uInteriorColor;
uniform vec4 uInteriorSubstance;
void main() {
#include fade_lod
#include clip_pixel
#if defined(dFlipSided)
interior = gl_FrontFacing;
#else
interior = !gl_FrontFacing;
#endif
interior = !gl_FrontFacing;
float fragmentDepth = gl_FragCoord.z;
@@ -38,6 +37,10 @@ void main() {
vec3 normal = -normalize(vNormal);
if (uDoubleSided) normal *= float(gl_FrontFacing) * 2.0 - 1.0;
#endif
#if defined(dFlipSided)
normal *= -1.0;
#endif
#endif
#include assign_material_color
@@ -60,8 +63,8 @@ void main() {
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#include apply_light_color
#include apply_interior_color
#include apply_light_color
#include apply_marker_color
#if defined(dRenderVariant_color)

View File

@@ -19,6 +19,9 @@ precision highp int;
uniform mat4 uInvView;
uniform float uAlphaThickness;
uniform vec4 uInteriorColor;
uniform vec4 uInteriorSubstance;
varying float vRadius;
varying vec3 vPoint;
varying vec3 vPointViewPosition;
@@ -73,6 +76,11 @@ bool SphereImpostor(out vec3 modelPos, out vec3 cameraPos, out vec3 cameraNormal
if (!objectClipped) {
fragmentDepth = 0.0 + (0.0000001 / vRadius);
cameraNormal = -mix(normalize(vPoint), vec3(0.0, 0.0, -1.0), uIsOrtho);
// intersection of ray with near plane
float nearT = - (uNear + dot(rayOrigin, vec3(0.0, 0.0, 1.0))) / dot(rayDirection, vec3(0.0, 0.0, 1.0));
cameraPos = rayDirection * nearT + rayOrigin;
modelPos = (uInvView * vec4(cameraPos, 1.0)).xyz;
}
#endif
return true;
@@ -147,8 +155,8 @@ void main(void){
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#include apply_light_color
#include apply_interior_color
#include apply_light_color
#include apply_marker_color
#if defined(dRenderVariant_color)

View File

@@ -78,7 +78,7 @@ void main(void) {
float sum = 0.0;
float kernelSum = 0.0;
int halfKernelSize = dOcclusionKernelSize / 2;
const int halfKernelSize = dOcclusionKernelSize / 2;
// only if kernelSize is odd
for (int i = -halfKernelSize; i <= halfKernelSize; i++) {
if (abs(float(i)) > 1.0 && abs(float(i)) * pixelSize > 0.8) continue;

View File

@@ -235,7 +235,7 @@ export function createProgram(gl: GLRenderingContext, state: WebGLState, extensi
use: () => {
// console.log('use', programId)
if (isDebugMode && !finalized) throw new Error('program not finalized');
if (isDebugMode && !finalized) throw new Error(`program not finalized: ${variant}`);
state.currentProgramId = programId;
gl.useProgram(program);
},

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.408, IHM 1.28, MA 1.4.8.
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.409, IHM 1.28, MA 1.4.8.
*
* @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.408, IHM 1.28, MA 1.4.8.
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.409, IHM 1.28, MA 1.4.8.
*
* @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.408, IHM 1.28, MA 1.4.8.
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.409, IHM 1.28, MA 1.4.8.
*
* @author molstar/ciftools package
*/

View File

@@ -119,11 +119,12 @@ export async function calcMolecularSurface(ctx: RuntimeContext, position: Requir
const vx = px[j], vy = py[j], vz = pz[j];
const rad = radius[j];
const rSq = rad * rad;
const extended = ngPoints > 0;
lookup3d.find(vx, vy, vz, rad);
// Number of grid points, round this up...
const ng = Math.ceil(rad * scaleFactor);
const ng = Math.ceil(rad * scaleFactor) + ngPoints;
// Center of the atom, mapped to grid points (take floor)
const iax = Math.floor(scaleFactor * (vx - minX));
@@ -153,12 +154,9 @@ export async function calcMolecularSurface(ctx: RuntimeContext, position: Requir
const dz = gridz[zi] - vz;
const dSq = dxySq + dz * dz;
if (dSq < rSq) {
if (extended || dSq < rSq) {
const idx = zi + xyIdx;
// if unvisited, make positive
if (data[idx] < 0.0) data[idx] *= -1;
// Project on to the surface of the sphere
// sp is the projected point ( dx, dy, dz ) * ( ra / d )
const d = Math.sqrt(dSq);
@@ -167,12 +165,22 @@ export async function calcMolecularSurface(ctx: RuntimeContext, position: Requir
const spy = dy * ap + vy;
const spz = dz * ap + vz;
if (obscured(spx, spy, spz, j, -1) === -1) {
const dd = rad - d;
if (dd < data[idx]) {
data[idx] = dd;
idData[idx] = id[i];
const obs = obscured(spx, spy, spz, j, -1);
if (dSq < rSq) {
// if unvisited, make positive
if (data[idx] < 0.0) data[idx] *= -1;
if (obs === -1) {
const dd = rad - d;
if (dd < data[idx]) {
data[idx] = dd;
idData[idx] = id[i];
}
}
} else if (extended && obs === -1) {
const dd = rad - d;
if (dd > data[idx]) data[idx] = dd;
}
}
}
@@ -318,7 +326,8 @@ export async function calcMolecularSurface(ctx: RuntimeContext, position: Requir
// console.time('MolecularSurface createState')
const { resolution, probeRadius, probePositions } = props;
const scaleFactor = 1 / resolution;
const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor));
const ngTorus = 2 + Math.floor(probeRadius * scaleFactor);
const ngPoints = probeRadius < (resolution * 2) ? 1 : 0;
const cellSize = Vec3.create(maxRadius, maxRadius, maxRadius);
Vec3.scale(cellSize, cellSize, 2);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,308 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Diego del Alamo <diego.delalamo@gmail.com>
*/
import { TMAlign } from '../3d/tm-align';
import { Vec3 } from '../3d/vec3';
// Reference data from US-align for 6F34 vs 3TT3 chain A
const REFERENCE_6F34_3TT3 = {
structure1Length: 458,
structure2Length: 501,
alignedLength: 409,
rmsd: 4.10,
tmScore1: 0.72566, // normalized by structure 1 (6F34)
tmScore2: 0.67133, // normalized by structure 2 (3TT3)
sequenceIdentity: 0.147
};
describe('TMAlign', () => {
// Helper to create positions from coordinate arrays
function makePositions(coords: number[][]): TMAlign.Positions {
const n = coords.length;
const pos = TMAlign.Positions.empty(n);
for (let i = 0; i < n; i++) {
pos.x[i] = coords[i][0];
pos.y[i] = coords[i][1];
pos.z[i] = coords[i][2];
}
return pos;
}
describe('calculateD0', () => {
it('returns 0.5 for short proteins (L <= 21)', () => {
expect(TMAlign.calculateD0(10)).toBe(0.5);
expect(TMAlign.calculateD0(21)).toBe(0.5);
});
it('returns correct d0 for longer proteins', () => {
// d0 = 1.24 * (L - 15)^(1/3) - 1.8
const d0_100 = 1.24 * Math.pow(100 - 15, 1 / 3) - 1.8;
expect(TMAlign.calculateD0(100)).toBeCloseTo(d0_100, 5);
const d0_200 = 1.24 * Math.pow(200 - 15, 1 / 3) - 1.8;
expect(TMAlign.calculateD0(200)).toBeCloseTo(d0_200, 5);
});
it('matches reference d0 values', () => {
// From reference: L=458, d0=7.65; L=501, d0=7.95
expect(TMAlign.calculateD0(458)).toBeCloseTo(7.65, 1);
expect(TMAlign.calculateD0(501)).toBeCloseTo(7.95, 1);
});
});
describe('compute - basic cases', () => {
it('returns identity transform and TM-score=1 for identical structures', () => {
const coords = [
[0, 0, 0],
[3.8, 0, 0],
[7.6, 0, 0],
[11.4, 0, 0],
[15.2, 0, 0],
];
const pos = makePositions(coords);
const result = TMAlign.compute({ a: pos, b: pos });
// Identical structures should have perfect TM-score
expect(result.tmScoreA).toBeCloseTo(1.0, 2);
expect(result.tmScoreB).toBeCloseTo(1.0, 2);
expect(result.rmsd).toBeCloseTo(0, 5);
expect(result.alignedLength).toBe(5);
});
it('handles empty inputs', () => {
const empty = makePositions([]);
const result = TMAlign.compute({ a: empty, b: empty });
expect(result.tmScoreA).toBe(0);
expect(result.tmScoreB).toBe(0);
expect(result.alignedLength).toBe(0);
});
it('aligns translated structures correctly', () => {
// Structure A: simple helix-like
const coordsA = [
[0, 0, 0],
[1.5, 0, 1.0],
[3.0, 0, 0],
[4.5, 0, 1.0],
[6.0, 0, 0],
[7.5, 0, 1.0],
[9.0, 0, 0],
[10.5, 0, 1.0],
];
// Structure B: same structure translated by (10, 20, 30)
const translation = [10, 20, 30];
const coordsB = coordsA.map(c => [
c[0] + translation[0],
c[1] + translation[1],
c[2] + translation[2]
]);
const posA = makePositions(coordsA);
const posB = makePositions(coordsB);
const result = TMAlign.compute({ a: posA, b: posB });
// Should align well since they're identical structures
expect(result.tmScoreA).toBeGreaterThan(0.9);
expect(result.rmsd).toBeLessThan(1.0);
});
it('aligns rotated structures correctly', () => {
// Structure A
const coordsA = [
[0, 0, 0],
[3.8, 0, 0],
[7.6, 0, 0],
[11.4, 0, 0],
[15.2, 0, 0],
[19.0, 0, 0],
];
// Structure B: rotated 90 degrees around Z axis
const coordsB = coordsA.map(c => [-c[1], c[0], c[2]]);
const posA = makePositions(coordsA);
const posB = makePositions(coordsB);
const result = TMAlign.compute({ a: posA, b: posB });
// Should align perfectly since it's just a rotation
expect(result.tmScoreA).toBeGreaterThan(0.9);
expect(result.rmsd).toBeLessThan(0.5);
});
it('returns lower TM-score for dissimilar structures', () => {
// Structure A: extended chain
const coordsA: number[][] = [];
for (let i = 0; i < 20; i++) {
coordsA.push([i * 3.8, 0, 0]);
}
// Structure B: helix-like with different geometry
const coordsB: number[][] = [];
for (let i = 0; i < 20; i++) {
const angle = i * 100 * Math.PI / 180;
coordsB.push([
5 * Math.cos(angle),
5 * Math.sin(angle),
i * 1.5
]);
}
const posA = makePositions(coordsA);
const posB = makePositions(coordsB);
const result = TMAlign.compute({ a: posA, b: posB });
// Different structures should have lower TM-score
expect(result.tmScoreA).toBeLessThan(0.5);
});
it('produces valid transformation matrix', () => {
const coordsA = [
[0, 0, 0],
[3.8, 0, 0],
[7.6, 1, 0],
[11.4, 0, 0],
[15.2, 1, 0],
];
const coordsB = [
[5, 5, 5],
[8.8, 5, 5],
[12.6, 6, 5],
[16.4, 5, 5],
[20.2, 6, 5],
];
const posA = makePositions(coordsA);
const posB = makePositions(coordsB);
const result = TMAlign.compute({ a: posA, b: posB });
// Transform should be a valid 4x4 matrix
expect(result.bTransform).toBeDefined();
// Apply transform to B and check alignment improves
const transformedB: number[][] = [];
for (let i = 0; i < coordsB.length; i++) {
const v = Vec3.create(coordsB[i][0], coordsB[i][1], coordsB[i][2]);
Vec3.transformMat4(v, v, result.bTransform);
transformedB.push([v[0], v[1], v[2]]);
}
// After transformation, structures should be closer
let totalDistAfter = 0;
for (let i = 0; i < coordsA.length; i++) {
const dx = transformedB[i][0] - coordsA[i][0];
const dy = transformedB[i][1] - coordsA[i][1];
const dz = transformedB[i][2] - coordsA[i][2];
totalDistAfter += Math.sqrt(dx * dx + dy * dy + dz * dz);
}
expect(totalDistAfter / coordsA.length).toBeLessThan(2.0);
});
it('handles different length structures', () => {
const coordsA: number[][] = [];
for (let i = 0; i < 50; i++) {
coordsA.push([i * 3.8, Math.sin(i * 0.5), 0]);
}
const coordsB: number[][] = [];
for (let i = 0; i < 30; i++) {
coordsB.push([i * 3.8, Math.sin(i * 0.5), 0]);
}
const posA = makePositions(coordsA);
const posB = makePositions(coordsB);
const result = TMAlign.compute({ a: posA, b: posB });
// Should still produce valid results
expect(result.tmScoreA).toBeGreaterThan(0);
expect(result.tmScoreB).toBeGreaterThan(0);
expect(result.alignedLength).toBeGreaterThan(0);
expect(result.alignedLength).toBeLessThanOrEqual(Math.min(50, 30));
});
});
describe('TM-score properties', () => {
it('TM-score is length-normalized', () => {
// Create two identical small structures
const coordsSmall: number[][] = [];
for (let i = 0; i < 30; i++) {
coordsSmall.push([i * 3.8, Math.sin(i * 0.3) * 2, Math.cos(i * 0.3) * 2]);
}
// Create longer version with same pattern
const coordsLong: number[][] = [];
for (let i = 0; i < 100; i++) {
coordsLong.push([i * 3.8, Math.sin(i * 0.3) * 2, Math.cos(i * 0.3) * 2]);
}
const posSmall = makePositions(coordsSmall);
const posLong = makePositions(coordsLong);
const result = TMAlign.compute({ a: posLong, b: posSmall });
// TM-score normalized by longer structure should be lower
// than TM-score normalized by shorter structure
expect(result.tmScoreA).toBeLessThan(result.tmScoreB);
});
it('TM-score is between 0 and 1 for normalized length', () => {
const coordsA: number[][] = [];
for (let i = 0; i < 50; i++) {
coordsA.push([i * 3.8, Math.random() * 10, Math.random() * 10]);
}
const coordsB: number[][] = [];
for (let i = 0; i < 50; i++) {
coordsB.push([i * 3.8, Math.random() * 10, Math.random() * 10]);
}
const posA = makePositions(coordsA);
const posB = makePositions(coordsB);
const result = TMAlign.compute({ a: posA, b: posB });
expect(result.tmScoreA).toBeGreaterThanOrEqual(0);
expect(result.tmScoreA).toBeLessThanOrEqual(1);
expect(result.tmScoreB).toBeGreaterThanOrEqual(0);
expect(result.tmScoreB).toBeLessThanOrEqual(1);
});
});
describe('Reference comparison (6F34 vs 3TT3 chain A expected ranges)', () => {
// These tests verify that our implementation produces results
// in the expected ballpark for known protein pairs
// Exact values may differ due to algorithm implementation details
it('d0 calculation matches reference implementation', () => {
// Reference values from US-align output
const d0_458 = TMAlign.calculateD0(REFERENCE_6F34_3TT3.structure1Length);
const d0_501 = TMAlign.calculateD0(REFERENCE_6F34_3TT3.structure2Length);
// Should be within 5% of reference values
expect(d0_458).toBeCloseTo(7.65, 0);
expect(d0_501).toBeCloseTo(7.95, 0);
});
it('TM-score interpretation thresholds', () => {
// According to TM-align literature:
// TM-score > 0.5: same fold
// TM-score > 0.17: statistically significant
// 6F34 vs 3TT3: TM-scores ~0.73 and ~0.67 indicate same fold
// The reference TM-scores indicate same fold
expect(REFERENCE_6F34_3TT3.tmScore1).toBeGreaterThan(0.5); // same fold
expect(REFERENCE_6F34_3TT3.tmScore2).toBeGreaterThan(0.5); // same fold
});
});
});

File diff suppressed because one or more lines are too long

View File

@@ -88,6 +88,8 @@ const residue = {
key: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.residueIndex[l.element]),
group_PDB: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.group_PDB.value(l.unit.residueIndex[l.element])),
label_comp_id: p(compId),
auth_comp_id: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.auth_comp_id.value(l.element)),
label_seq_id: p(seqId),
auth_seq_id: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.auth_seq_id.value(l.unit.residueIndex[l.element])),
pdbx_PDB_ins_code: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.pdbx_PDB_ins_code.value(l.unit.residueIndex[l.element])),

View File

@@ -134,6 +134,7 @@ namespace StructureSymmetry {
for (let j = 0, _j = au.length; j < _j; j++) {
if (au[j].conformation !== bu[j].conformation) return false;
}
if (!SortedArray.areEqual(a[i].elements, b[i].elements)) return false;
}
return true;
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Diego del Alamo <diego.delalamo@gmail.com>
*
* Structure-level TM-align wrapper
*/
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
import { TMAlign } from '../../../../mol-math/linear-algebra/3d/tm-align';
import { StructureElement } from '../element';
import { getPositionTable } from './superposition';
export { tmAlign, tmAlignMultiple };
export type TMAlignResult = TMAlign.Result;
/**
* Perform TM-align on two structure element loci.
* Aligns structure B onto structure A (A is the reference).
*
* @param a Reference structure loci (will not be transformed)
* @param b Mobile structure loci (transformation returned)
* @returns TM-align result with transformation, scores, and alignment
*/
function tmAlign(a: StructureElement.Loci, b: StructureElement.Loci): TMAlignResult {
const lenA = StructureElement.Loci.size(a);
const lenB = StructureElement.Loci.size(b);
if (lenA === 0 || lenB === 0) {
return {
bTransform: Mat4.identity(),
tmScoreA: 0,
tmScoreB: 0,
rmsd: 0,
alignedLength: 0,
sequenceIdentity: 0,
alignmentA: [],
alignmentB: []
};
}
const posA = getPositionTable(a, lenA);
const posB = getPositionTable(b, lenB);
return TMAlign.compute({ a: posA, b: posB });
}
/**
* Perform TM-align on multiple structure element loci.
* The first structure is used as the reference; all others are aligned to it.
*
* @param xs Array of structure element loci (first is reference)
* @returns Array of TM-align results (length = xs.length - 1)
*/
function tmAlignMultiple(xs: StructureElement.Loci[]): TMAlignResult[] {
const results: TMAlignResult[] = [];
if (xs.length < 2) return results;
const refLoci = xs[0];
const lenRef = StructureElement.Loci.size(refLoci);
const posRef = getPositionTable(refLoci, lenRef);
for (let i = 1; i < xs.length; i++) {
const mobileLoci = xs[i];
const lenMobile = StructureElement.Loci.size(mobileLoci);
const posMobile = getPositionTable(mobileLoci, lenMobile);
results.push(TMAlign.compute({ a: posRef, b: posMobile }));
}
return results;
}

View File

@@ -16,7 +16,7 @@ export class StructureUnitTransforms {
private groupUnitTransforms: Float32Array[] = [];
/** maps unit.id to offset of transform in unitTransforms */
private unitOffsetMap = IntMap.Mutable<number>();
private groupIndexMap = IntMap.Mutable<number>();
private groupIndexMap = new Map<Unit.SymmetryGroup, number>();
private size: number;
private _isIdentity: boolean | undefined = undefined;
@@ -30,7 +30,7 @@ export class StructureUnitTransforms {
let groupOffset = 0;
for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
const g = structure.unitSymmetryGroups[i];
this.groupIndexMap.set(g.hashCode, i);
this.groupIndexMap.set(g, i);
const groupTransforms = this.unitTransforms.subarray(groupOffset, groupOffset + g.units.length * 16);
this.groupUnitTransforms.push(groupTransforms);
for (let j = 0, jl = g.units.length; j < jl; ++j) {
@@ -71,6 +71,6 @@ export class StructureUnitTransforms {
}
getSymmetryGroupTransforms(group: Unit.SymmetryGroup): Float32Array {
return this.groupUnitTransforms[this.groupIndexMap.get(group.hashCode)];
return this.groupUnitTransforms[this.groupIndexMap.get(group)!];
}
}

View File

@@ -13,7 +13,7 @@ export class PluginComponent {
private _ev: RxEventHelper | undefined;
private subs: Subscription[] | undefined = void 0;
protected subscribe<T>(obs: Observable<T> | undefined, action: (v: T) => void) {
subscribe<T>(obs: Observable<T> | undefined, action: (v: T) => void) {
if (!obs) return { unsubscribe: () => {} };
if (typeof this.subs === 'undefined') this.subs = [];

View File

@@ -265,6 +265,16 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
async getStateSnapshot(options?: { name?: string, description?: string, playOnLoad?: boolean, params?: PluginState.SnapshotParams }): Promise<PluginStateSnapshotManager.StateSnapshot> {
await this.syncCurrent(options);
const entries = this.state.entries.valueSeq().toArray();
for (let i = 0; i < entries.length; i++) {
if (!entries[i]._transientData) continue;
// Clone the entry to avoid modifying the original
entries[i] = { ...entries[i] };
// Remove any transient data before serialization
delete entries[i]._transientData;
}
return {
timestamp: +new Date(),
version: PLUGIN_VERSION,
@@ -275,7 +285,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
isPlaying: !!(options && options.playOnLoad),
nextSnapshotDelayInMs: this.state.nextSnapshotDelayInMs
},
entries: this.state.entries.valueSeq().toArray()
entries,
};
}
@@ -459,7 +469,9 @@ namespace PluginStateSnapshotManager {
export interface Entry extends EntryParams {
timestamp: number,
snapshot: PluginState.Snapshot
snapshot: PluginState.Snapshot,
/** Extra data that is not serialized */
_transientData?: any
}
export function Entry(snapshot: PluginState.Snapshot, params: EntryParams): Entry {

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 David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -15,7 +15,7 @@ import { StateBuilder, StateObjectRef, StateTransformer } from '../../../mol-sta
import { Task } from '../../../mol-task';
import { ColorTheme } from '../../../mol-theme/color';
import { SizeTheme } from '../../../mol-theme/size';
import { shallowEqual, UUID } from '../../../mol-util';
import { UUID } from '../../../mol-util';
import { ColorNames } from '../../../mol-util/color/names';
import { objectForEach } from '../../../mol-util/object';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -35,6 +35,7 @@ import { setStructureSubstance } from '../../helpers/structure-substance';
import { Material } from '../../../mol-util/material';
import { Clip } from '../../../mol-util/clip';
import { setStructureEmissive } from '../../helpers/structure-emissive';
import { areInteriorPropsEquals, getInteriorParam } from '../../../mol-geo/geometry/interior';
export { StructureComponentManager };
@@ -82,20 +83,21 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
p.ignoreLight = options.ignoreLight;
p.material = options.materialStyle;
p.clip = options.clipObjects;
p.interior = options.interior;
});
if (interactionChanged) await this.updateInterationProps();
});
}
private updateReprParams(update: StateBuilder.Root, component: StructureComponentRef) {
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip } = this.state.options;
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior } = this.state.options;
const ignoreHydrogens = hydrogens !== 'all';
const ignoreHydrogensVariant = hydrogens === 'only-polar' ? 'non-polar' : 'all';
for (const r of component.representations) {
if (r.cell.transform.transformer !== StructureRepresentation3D) continue;
const params = r.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
if (!!params.type.params.ignoreHydrogens !== ignoreHydrogens || params.type.params.ignoreHydrogensVariant !== ignoreHydrogensVariant || params.type.params.quality !== quality || params.type.params.ignoreLight !== ignoreLight || !shallowEqual(params.type.params.material, material) || !PD.areEqual(Clip.Params, params.type.params.clip, clip)) {
if (!!params.type.params.ignoreHydrogens !== ignoreHydrogens || params.type.params.ignoreHydrogensVariant !== ignoreHydrogensVariant || params.type.params.quality !== quality || params.type.params.ignoreLight !== ignoreLight || !Material.areEqual(params.type.params.material, material) || !PD.areEqual(Clip.Params, params.type.params.clip, clip) || !areInteriorPropsEquals(params.type.params.interior, interior)) {
update.to(r.cell).update(old => {
old.type.params.ignoreHydrogens = ignoreHydrogens;
old.type.params.ignoreHydrogensVariant = ignoreHydrogensVariant;
@@ -103,6 +105,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
old.type.params.ignoreLight = ignoreLight;
old.type.params.material = material;
old.type.params.clip = clip;
old.type.params.interior = interior;
});
}
}
@@ -321,10 +324,10 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
addRepresentation(components: ReadonlyArray<StructureComponentRef>, type: string) {
if (components.length === 0) return;
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip } = this.state.options;
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior } = this.state.options;
const ignoreHydrogens = hydrogens !== 'all';
const ignoreHydrogensVariant = hydrogens === 'only-polar' ? 'non-polar' : 'all';
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip };
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip, interior };
return this.plugin.dataTransaction(async () => {
for (const component of components) {
@@ -359,10 +362,10 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
const xs = structures || this.currentStructures;
if (xs.length === 0) return;
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip } = this.state.options;
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior } = this.state.options;
const ignoreHydrogens = hydrogens !== 'all';
const ignoreHydrogensVariant = hydrogens === 'only-polar' ? 'non-polar' : 'all';
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip };
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip, interior };
const componentKey = UUID.create22();
for (const s of xs) {
@@ -483,6 +486,7 @@ namespace StructureComponentManager {
materialStyle: Material.getParam(),
clipObjects: PD.Group(Clip.Params),
interactions: PD.Group(InteractionsProvider.defaultParams, { label: 'Non-covalent Interactions' }),
interior: getInteriorParam(),
};
export type Options = PD.Values<typeof OptionsParams>

View File

@@ -25,7 +25,7 @@ import { PluginUIContext } from '../context';
import { ActionMenu } from './action-menu';
import { ColorOptions, ColorValueOption, CombinedColorControl } from './color';
import { Button, ControlGroup, ControlRow, ExpandGroup, IconButton, TextInput, ToggleButton } from './common';
import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, MoreHorizSvg, WarningSvg } from './icons';
import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, TuneSvg, WarningSvg } from './icons';
import { legendFor } from './legend';
import { LineGraphComponent } from './line-graph/line-graph-component';
import { Slider, Slider2 } from './slider';
@@ -526,7 +526,7 @@ export class SelectControl extends React.PureComponent<ParamProps<PD.Select<stri
: void 0;
return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign, overflow: 'hidden', textOverflow: 'ellipsis' }}
label={label} title={label as string} icon={icon} toggle={toggle} isSelected={this.state.showOptions} />;
label={label} title={label as string} icon={icon} toggle={toggle} isSelected={this.state.showOptions} className='msp-select-toggle' />;
}
renderAddOn() {
@@ -1192,7 +1192,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>
if (!this.state.isExpanded) {
return <div className='msp-mapped-parameter-group'>
{ctrl}
<IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
<IconButton svg={TuneSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} style={{ opacity: 0.7 }} />
</div>;
}
@@ -1203,7 +1203,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>
return <div className='msp-mapped-parameter-group'>
{ctrl}
<IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
<IconButton svg={TuneSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
<div className='msp-control-offset'>
{this.pivotedPresets()}
<ParameterControls params={filtered} onEnter={this.props.onEnter} values={this.props.value} onChange={this.onChangeParam} isDisabled={this.props.isDisabled} />
@@ -1312,7 +1312,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
if (!this.areParamsEmpty(param.params)) {
return <div className='msp-mapped-parameter-group'>
{Select}
<IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} />
<IconButton svg={TuneSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} style={{ opacity: this.state.isExpanded ? undefined : 0.7 }} />
{this.state.isExpanded && <GroupControl inMapped param={param} value={value.params} name={value.name} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />}
</div>;
}

View File

@@ -286,6 +286,27 @@
color: $msp-btn-commit-on-hover-font-color;
}
.msp-select-toggle::after {
content: "";
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid $hover-font-color;
opacity: 0;
pointer-events: none;
}
.msp-select-toggle:hover::after {
opacity: 1;
}
.msp-btn-action {
height: $row-height;
line-height: $row-height;

View File

@@ -22,27 +22,6 @@
z-index: 1000;
}
.msp-viewport-host3d {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-touch-callout: none;
touch-action: manipulation;
>canvas {
background-color: $default-background;
background-image: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey),
linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey);
background-size: 60px 60px;
background-position: 0 0, 30px 30px;
}
}
.msp-viewport-controls {
position: absolute;
right: $control-spacing;

View File

@@ -11,6 +11,7 @@ import { SIFTSMapping } from '../../mol-model-props/sequence/sifts-mapping';
import { QueryContext, Structure, StructureElement, StructureProperties, StructureSelection } from '../../mol-model/structure';
import { alignAndSuperpose, superpose } from '../../mol-model/structure/structure/util/superposition';
import { alignAndSuperposeWithSIFTSMapping } from '../../mol-model/structure/structure/util/superposition-sifts-mapping';
import { tmAlign } from '../../mol-model/structure/structure/util/tm-align';
import { StructureSelectionQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
import { PluginStateObject } from '../../mol-plugin-state/objects';
@@ -61,7 +62,7 @@ const SuperpositionTag = 'SuperpositionTransform';
type SuperpositionControlsState = {
isBusy: boolean,
action?: 'byChains' | 'byAtoms' | 'options',
action?: 'byChains' | 'byAtoms' | 'byTMAlign' | 'options',
canUseDb?: boolean,
options: StructureSuperpositionOptions
}
@@ -222,6 +223,32 @@ export class SuperpositionControls extends PurePluginUIComponent<{ }, Superposit
}
};
superposeTMAlign = async () => {
const { query } = this.state.options.traceOnly ? StructureSelectionQueries.trace : StructureSelectionQueries.polymer;
const entries = this.chainEntries;
const locis = entries.map(e => {
const s = StructureElement.Loci.toStructure(e.loci);
const loci = StructureSelection.toLociWithSourceUnits(query(new QueryContext(s)));
return StructureElement.Loci.remap(loci, this.getRootStructure(e.loci.structure));
});
const pivot = this.plugin.managers.structure.hierarchy.findStructure(locis[0]?.structure);
const coordinateSystem = pivot?.transform?.cell.obj?.data.coordinateSystem;
const eA = entries[0];
for (let i = 1, il = locis.length; i < il; ++i) {
const eB = entries[i];
const result = tmAlign(locis[0], locis[i]);
const { bTransform, tmScoreA, tmScoreB, rmsd, alignedLength } = result;
await this.transform(eB.cell, bTransform, coordinateSystem);
const labelA = stripTags(eA.label);
const labelB = stripTags(eB.label);
this.plugin.log.info(`TM-align [${labelA}] and [${labelB}]: TM-score=${tmScoreA.toFixed(4)}/${tmScoreB.toFixed(4)}, RMSD=${rmsd.toFixed(2)} Å, aligned ${alignedLength} residues.`);
}
await this.cameraReset();
};
async cameraReset() {
await new Promise(res => requestAnimationFrame(res));
PluginCommands.Camera.Reset(this.plugin);
@@ -229,6 +256,7 @@ export class SuperpositionControls extends PurePluginUIComponent<{ }, Superposit
toggleByChains = () => this.setState({ action: this.state.action === 'byChains' ? void 0 : 'byChains' });
toggleByAtoms = () => this.setState({ action: this.state.action === 'byAtoms' ? void 0 : 'byAtoms' });
toggleByTMAlign = () => this.setState({ action: this.state.action === 'byTMAlign' ? void 0 : 'byTMAlign' });
toggleOptions = () => this.setState({ action: this.state.action === 'options' ? void 0 : 'options' });
highlight(loci: StructureElement.Loci) {
@@ -361,6 +389,21 @@ export class SuperpositionControls extends PurePluginUIComponent<{ }, Superposit
</>;
}
addByTMAlign() {
const entries = this.chainEntries;
return <>
{entries.length > 0 && <div className='msp-control-offset'>
{entries.map((e, i) => this.lociEntry(e, i))}
</div>}
{entries.length < 2 && <div className='msp-control-offset msp-help-text'>
<div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add 2 or more selections{this.toggleHint()} from separate structures. Selections must be limited to single polymer chains. TM-align performs structure-based alignment independent of sequence.</div>
</div>}
{entries.length > 1 && <Button title='Superpose structures using TM-align (structure-based alignment).' className='msp-btn-commit msp-btn-commit-on' onClick={this.superposeTMAlign} style={{ marginTop: '1px' }}>
TM-align Superpose
</Button>}
</>;
}
superposeByDbMapping() {
return <>
<Button icon={SuperposeChainsSvg} title='Superpose structures using intersection of residues from SIFTS UNIPROT mapping.' className='msp-btn msp-btn-block' onClick={this.superposeDb} style={{ marginTop: '1px' }} disabled={this.state.isBusy}>
@@ -378,11 +421,13 @@ export class SuperpositionControls extends PurePluginUIComponent<{ }, Superposit
<div className='msp-flex-row'>
<ToggleButton icon={SuperposeChainsSvg} label='Chains' toggle={this.toggleByChains} isSelected={this.state.action === 'byChains'} disabled={this.state.isBusy} />
<ToggleButton icon={SuperposeAtomsSvg} label='Atoms' toggle={this.toggleByAtoms} isSelected={this.state.action === 'byAtoms'} disabled={this.state.isBusy} />
<ToggleButton icon={SuperposeChainsSvg} label='TM-align' toggle={this.toggleByTMAlign} isSelected={this.state.action === 'byTMAlign'} disabled={this.state.isBusy} />
{this.state.canUseDb && this.superposeByDbMapping()}
<ToggleButton icon={TuneSvg} label='' title='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isBusy} style={{ flex: '0 0 40px', padding: 0 }} />
</div>
{this.state.action === 'byChains' && this.addByChains()}
{this.state.action === 'byAtoms' && this.addByAtoms()}
{this.state.action === 'byTMAlign' && this.addByTMAlign()}
{this.state.action === 'options' && <div className='msp-control-offset'>
<ParameterControls params={StructureSuperpositionParams} values={this.state.options} onChangeValues={this.setOptions} isDisabled={this.state.isBusy} />
</div>}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 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>
@@ -20,6 +20,7 @@ import { PluginCommands } from '../../../commands';
import { PluginContext } from '../../../context';
import { Material } from '../../../../mol-util/material';
import { Clip } from '../../../../mol-util/clip';
import { getInteriorParam } from '../../../../mol-geo/geometry/interior';
const StructureFocusRepresentationParams = (plugin: PluginContext) => {
const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(void 0, plugin) as PD.Params;
@@ -56,6 +57,7 @@ const StructureFocusRepresentationParams = (plugin: PluginContext) => {
ignoreLight: PD.Boolean(false),
material: Material.getParam(),
clip: PD.Group(Clip.Params),
interior: getInteriorParam(),
};
};
@@ -81,7 +83,7 @@ class StructureFocusRepresentationBehavior extends PluginBehavior.WithSubscriber
...reprParams,
type: {
name: reprParams.type.name,
params: { ...reprParams.type.params, ignoreHydrogens: this.params.ignoreHydrogens, ignoreHydrogensVariant: this.params.ignoreHydrogensVariant, ignoreLight: this.params.ignoreLight, material: this.params.material, clip: this.params.clip }
params: { ...reprParams.type.params, ignoreHydrogens: this.params.ignoreHydrogens, ignoreHydrogensVariant: this.params.ignoreHydrogensVariant, ignoreLight: this.params.ignoreLight, material: this.params.material, clip: this.params.clip, interior: this.params.interior }
}
};
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2024-25 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -19,6 +19,10 @@ export class PluginContainer {
this.parent.parentElement?.removeChild(this.parent);
}
/**
* options.checkeredCanvasBackground has no effect. Use canvas3d.checkeredTransparentBackground instead.
* TODO: remove in v6
*/
constructor(public options?: { checkeredCanvasBackground?: boolean, canvas?: HTMLCanvasElement }) {
const parent = document.createElement('div');
Object.assign(parent.style, {
@@ -36,13 +40,6 @@ export class PluginContainer {
let canvas = options?.canvas;
if (!canvas) {
canvas = document.createElement('canvas');
if (options?.checkeredCanvasBackground) {
Object.assign(canvas.style, {
'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)',
'background-size': '60px 60px',
'background-position': '0 0, 30px 30px'
});
}
parent.appendChild(canvas);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -246,6 +246,9 @@ export class PluginContext {
if (!this._initViewer(container.canvas, container.parent, options?.canvas3dContext)) {
return false;
}
if (options?.checkeredCanvasBackground) {
this.canvas3d?.setProps({ checkeredTransparentBackground: true });
}
this.container = container;
return true;
}

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>
@@ -28,6 +28,12 @@ import { StructureGroup } from './visual/util/common';
import { Substance } from '../../mol-theme/substance';
import { LocationCallback } from '../util';
import { Emissive } from '../../mol-theme/emissive';
import { HashMap } from '../../mol-util/map';
function createVisualsMap<P extends StructureParams>() {
return new HashMap<Unit.SymmetryGroup, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>(group => group.hashCode, Unit.SymmetryGroup.areInvariantElementsEqual);
}
export interface UnitsVisual<P extends StructureParams> extends Visual<StructureGroup, P> { }
@@ -39,7 +45,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
const renderObjects: GraphicsRenderObject[] = [];
const geometryState = new Representation.GeometryState();
const _state = StructureRepresentationStateBuilder.create();
let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>();
let visuals = createVisualsMap<P>();
let _structure: Structure;
let _groups: ReadonlyArray<Unit.SymmetryGroup>;
@@ -67,7 +73,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
if (promise) await promise;
setVisualState(visual, group, _state); // current state for new visual
visuals.set(group.hashCode, { visual, group });
visuals.set(group, { visual, group });
if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length });
}
} else if (structure && (!Structure.areUnitIdsAndIndicesEqual(structure, _structure) || structure.child !== _structure.child)) {
@@ -77,10 +83,10 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
_groups = structure.unitSymmetryGroups;
// const newGroups: Unit.SymmetryGroup[] = []
const oldVisuals = visuals;
visuals = new Map();
visuals = createVisualsMap<P>();
for (let i = 0; i < _groups.length; i++) {
const group = _groups[i];
const visualGroup = oldVisuals.get(group.hashCode);
const visualGroup = oldVisuals.get(group);
if (visualGroup) {
// console.log(label, 'found visualGroup to reuse');
// console.log('old', visualGroup.group)
@@ -96,8 +102,8 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
if (promise) await promise;
}
visuals.set(group.hashCode, { visual, group });
oldVisuals.delete(group.hashCode);
visuals.set(group, { visual, group });
oldVisuals.delete(group);
// Remove highlight
// TODO: remove selection too??
@@ -112,7 +118,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
if (promise) await promise;
setVisualState(visual, group, _state); // current state for new visual
visuals.set(group.hashCode, { visual, group });
visuals.set(group, { visual, group });
}
if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length });
}
@@ -130,7 +136,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
// console.log('old', _structure.unitSymmetryGroups)
for (let i = 0; i < _groups.length; i++) {
const group = _groups[i];
const visualGroup = visuals.get(group.hashCode);
const visualGroup = visuals.get(group);
if (visualGroup) {
let { visual } = visualGroup;
if (visual.mustRecreate?.({ group, structure }, _props, ctx.webgl)) {

View File

@@ -6,12 +6,11 @@
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
import { MolecularSurfaceCalculationParams } from '../../../mol-math/geometry/molecular-surface';
import { VisualContext } from '../../visual';
import { Unit, Structure } from '../../../mol-model/structure';
import { Theme } from '../../../mol-theme/theme';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { computeStructureMolecularSurface, computeUnitMolecularSurface } from './util/molecular-surface';
import { CommonMolecularSurfaceCalculationParams, computeStructureMolecularSurface, computeUnitMolecularSurface } from './util/molecular-surface';
import { computeMarchingCubesMesh } from '../../../mol-geo/util/marching-cubes/algorithm';
import { ElementIterator, getElementLoci, eachElement, getSerialElementLoci, eachSerialElement } from './util/element';
import { VisualUpdateState } from '../../util';
@@ -21,16 +20,10 @@ import { MeshValues } from '../../../mol-gl/renderable/mesh';
import { Texture } from '../../../mol-gl/webgl/texture';
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { applyMeshColorSmoothing } from '../../../mol-geo/geometry/mesh/color-smoothing';
import { BaseGeometry, ColorSmoothingParams, getColorSmoothingProps } from '../../../mol-geo/geometry/base';
import { ColorSmoothingParams, getColorSmoothingProps } from '../../../mol-geo/geometry/base';
import { ValueCell } from '../../../mol-util';
import { ComplexMeshVisual, ComplexVisual } from '../complex-visual';
const CommonMolecularSurfaceCalculationParams = {
...MolecularSurfaceCalculationParams,
resolution: { ...MolecularSurfaceCalculationParams.resolution, ...BaseGeometry.CustomQualityParamInfo },
probePositions: { ...MolecularSurfaceCalculationParams.probePositions, ...BaseGeometry.CustomQualityParamInfo },
};
export const MolecularSurfaceMeshParams = {
...UnitsMeshParams,
...CommonMolecularSurfaceCalculationParams,

View File

@@ -1,17 +1,16 @@
/**
* Copyright (c) 2019-2022 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>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { UnitsVisual, UnitsLinesVisual, UnitsLinesParams } from '../units-visual';
import { MolecularSurfaceCalculationParams } from '../../../mol-math/geometry/molecular-surface';
import { VisualContext } from '../../visual';
import { Unit, Structure } from '../../../mol-model/structure';
import { Theme } from '../../../mol-theme/theme';
import { Lines } from '../../../mol-geo/geometry/lines/lines';
import { computeUnitMolecularSurface, MolecularSurfaceProps } from './util/molecular-surface';
import { CommonMolecularSurfaceCalculationParams, computeUnitMolecularSurface, MolecularSurfaceProps } from './util/molecular-surface';
import { computeMarchingCubesLines } from '../../../mol-geo/util/marching-cubes/algorithm';
import { ElementIterator, getElementLoci, eachElement } from './util/element';
import { VisualUpdateState } from '../../util';
@@ -20,7 +19,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
export const MolecularSurfaceWireframeParams = {
...UnitsLinesParams,
...MolecularSurfaceCalculationParams,
...CommonMolecularSurfaceCalculationParams,
...CommonSurfaceParams,
sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 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>
*/
@@ -8,10 +8,17 @@ import { Unit, Structure } from '../../../../mol-model/structure';
import { Task, RuntimeContext } from '../../../../mol-task';
import { getUnitConformationAndRadius, CommonSurfaceProps, ensureReasonableResolution, getStructureConformationAndRadius } from './common';
import { PositionData, DensityData, Box3D } from '../../../../mol-math/geometry';
import { MolecularSurfaceCalculationProps, calcMolecularSurface } from '../../../../mol-math/geometry/molecular-surface';
import { MolecularSurfaceCalculationParams, MolecularSurfaceCalculationProps, calcMolecularSurface } from '../../../../mol-math/geometry/molecular-surface';
import { OrderedSet } from '../../../../mol-data/int';
import { Boundary } from '../../../../mol-math/geometry/boundary';
import { SizeTheme } from '../../../../mol-theme/size';
import { BaseGeometry } from '../../../../mol-geo/geometry/base';
export const CommonMolecularSurfaceCalculationParams = {
...MolecularSurfaceCalculationParams,
resolution: { ...MolecularSurfaceCalculationParams.resolution, ...BaseGeometry.CustomQualityParamInfo },
probePositions: { ...MolecularSurfaceCalculationParams.probePositions, ...BaseGeometry.CustomQualityParamInfo },
};
export type MolecularSurfaceProps = MolecularSurfaceCalculationProps & CommonSurfaceProps

View File

@@ -19,6 +19,7 @@ import { MoleculeTypeColorThemeProvider } from './color/molecule-type';
import { PolymerIdColorThemeProvider } from './color/polymer-id';
import { PolymerIndexColorThemeProvider } from './color/polymer-index';
import { ResidueNameColorThemeProvider } from './color/residue-name';
import { ResidueChargeColorThemeProvider } from './color/residue-charge';
import { SecondaryStructureColorThemeProvider } from './color/secondary-structure';
import { SequenceIdColorThemeProvider } from './color/sequence-id';
import { ShapeGroupColorThemeProvider } from './color/shape-group';
@@ -199,6 +200,7 @@ namespace ColorTheme {
'partial-charge': PartialChargeColorThemeProvider,
'polymer-id': PolymerIdColorThemeProvider,
'polymer-index': PolymerIndexColorThemeProvider,
'residue-charge': ResidueChargeColorThemeProvider,
'residue-name': ResidueNameColorThemeProvider,
'secondary-structure': SecondaryStructureColorThemeProvider,
'sequence-id': SequenceIdColorThemeProvider,

View File

@@ -0,0 +1,178 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Lukáš Polák <admin@lukaspolak.cz>
*/
import { Color, ColorMap } from '../../mol-util/color';
import { StructureElement, Unit, Bond, ElementIndex } from '../../mol-model/structure';
import { Location } from '../../mol-model/location';
import type { ColorTheme } from '../color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ThemeDataContext } from '../theme';
import { TableLegend } from '../../mol-util/legend';
import { getAdjustedColorMap } from '../../mol-util/color/color';
import { getColorMapParams } from '../../mol-util/color/params';
import { ColorThemeCategory } from './categories';
// Colors for charged residues (by-name)
export const ChargedResidueColors = ColorMap({
// standard amino acids (charged)
'ARG': 0x0000FF,
'ASP': 0xFF0000,
'GLU': 0xFF0000,
'HIS': 0x33C3F9,
'LYS': 0x0000FF,
// standard amino acids (uncharged)
'ALA': 0xFFFFFF,
'ASN': 0xFFFFFF,
'CYS': 0xFFFFFF,
'GLN': 0xFFFFFF,
'GLY': 0xFFFFFF,
'ILE': 0xFFFFFF,
'LEU': 0xFFFFFF,
'MET': 0xFFFFFF,
'PHE': 0xFFFFFF,
'PRO': 0xFFFFFF,
'SER': 0xFFFFFF,
'THR': 0xFFFFFF,
'TRP': 0xFFFFFF,
'TYR': 0xFFFFFF,
'VAL': 0xFFFFFF,
// common from CCD
'MSE': 0xFFFFFF,
'SEP': 0xFFFFFF,
'TPO': 0xFFFFFF,
'PTR': 0xFFFFFF,
'PCA': 0xFFFFFF,
'HYP': 0xFFFFFF,
// charmm ff
'HSD': 0xFFFFFF,
'HSE': 0xFFFFFF,
'HSP': 0x0000FF,
'LSN': 0xFFFFFF,
'ASPP': 0xFFFFFF,
'GLUP': 0xFFFFFF,
// amber ff
'HID': 0xFFFFFF,
'HIE': 0xFFFFFF,
'HIP': 0x0000FF,
'LYN': 0xFFFFFF,
'ASH': 0xFFFFFF,
'GLH': 0xFFFFFF,
// rna bases
'A': 0xFFFFFF,
'G': 0xFFFFFF,
'I': 0xFFFFFF,
'C': 0xFFFFFF,
'T': 0xFFFFFF,
'U': 0xFFFFFF,
// dna bases
'DA': 0xFFFFFF,
'DG': 0xFFFFFF,
'DI': 0xFFFFFF,
'DC': 0xFFFFFF,
'DT': 0xFFFFFF,
'DU': 0xFFFFFF,
// peptide bases
'APN': 0xFFFFFF,
'GPN': 0xFFFFFF,
'CPN': 0xFFFFFF,
'TPN': 0xFFFFFF,
});
export type ChargedResidueColors = typeof ChargedResidueColors
const DefaultResidueChargeColor = Color(0xFF00FF);
const Description = 'Assigns a color to every residue based on its charge state.';
export const ResidueChargeColorThemeParams = {
method: PD.MappedStatic('by-name', {
'by-name': PD.Group({
saturation: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
colors: PD.MappedStatic('default', {
'default': PD.EmptyGroup(),
'custom': PD.Group(getColorMapParams(ChargedResidueColors)),
})
}, { isFlat: true })
})
};
export type ResidueChargeColorThemeParams = typeof ResidueChargeColorThemeParams;
export function getResidueChargeColorThemeParams(ctx: ThemeDataContext) {
return PD.clone(ResidueChargeColorThemeParams);
}
function getAtomicCompId(unit: Unit.Atomic, element: ElementIndex) {
return unit.model.atomicHierarchy.atoms.label_comp_id.value(element);
}
function getCoarseCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIndex) {
const seqIdBegin = unit.coarseElements.seq_id_begin.value(element);
const seqIdEnd = unit.coarseElements.seq_id_end.value(element);
if (seqIdBegin === seqIdEnd) {
const entityKey = unit.coarseElements.entityKey[element];
const seq = unit.model.sequence.byEntityKey[entityKey].sequence;
return seq.compId.value(seqIdBegin - 1); // 1-indexed
}
}
export function residueChargeColor(colorMap: ColorMap<Record<string, Color>>, residueName: string): Color {
const c = colorMap[residueName];
return c === undefined ? DefaultResidueChargeColor : c;
}
export function ResidueChargeColorTheme(ctx: ThemeDataContext, props: PD.Values<ResidueChargeColorThemeParams>): ColorTheme<ResidueChargeColorThemeParams> {
const { saturation, lightness, colors } = props.method.params;
const colorMap = getAdjustedColorMap(props.method.params.colors.name === 'default' ? ChargedResidueColors : colors.params, saturation, lightness);
function color(location: Location): Color {
if (StructureElement.Location.is(location)) {
if (Unit.isAtomic(location.unit)) {
const compId = getAtomicCompId(location.unit, location.element);
return residueChargeColor(colorMap, compId);
} else {
const compId = getCoarseCompId(location.unit, location.element);
if (compId) return residueChargeColor(colorMap, compId);
}
} else if (Bond.isLocation(location)) {
if (Unit.isAtomic(location.aUnit)) {
const compId = getAtomicCompId(location.aUnit, location.aUnit.elements[location.aIndex]);
return residueChargeColor(colorMap, compId);
} else {
const compId = getCoarseCompId(location.aUnit, location.aUnit.elements[location.aIndex]);
if (compId) return residueChargeColor(colorMap, compId);
}
}
return DefaultResidueChargeColor;
}
return {
factory: ResidueChargeColorTheme,
granularity: 'group',
preferSmoothing: true,
color,
props,
description: Description,
legend: TableLegend(Object.keys(colorMap).map(name => {
return [name, (colorMap as any)[name] as Color] as [string, Color];
}).concat([['Unknown', DefaultResidueChargeColor]]))
};
}
export const ResidueChargeColorThemeProvider: ColorTheme.Provider<ResidueChargeColorThemeParams, 'residue-charge'> = {
name: 'residue-charge',
label: 'Residue Charge',
category: ColorThemeCategory.Residue,
factory: ResidueChargeColorTheme,
getParams: getResidueChargeColorThemeParams,
defaultValues: PD.getDefaultValues(ResidueChargeColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
};

View File

@@ -44,4 +44,77 @@ export function arrayMapAdd<K, V>(map: Map<K, V[]>, key: K, value: V) {
} else {
map.set(key, [value]);
}
}
}
export class HashMap<K, V> {
private buckets = new Map<number, Array<{ key: K, value: V }>>();
set(key: K, value: V): void {
const hashCode = this.hashCode(key);
let bucket = this.buckets.get(hashCode);
if (!bucket) {
bucket = [];
this.buckets.set(hashCode, bucket);
}
for (let i = 0; i < bucket.length; i++) {
if (this.areEqual(bucket[i].key, key)) {
bucket[i].value = value;
return;
}
}
bucket.push({ key, value });
}
get(key: K): V | undefined {
const hashCode = this.hashCode(key);
const bucket = this.buckets.get(hashCode);
if (!bucket) {
return undefined;
}
for (const entry of bucket) {
if (this.areEqual(entry.key, key)) {
return entry.value;
}
}
return undefined;
}
delete(key: K): boolean {
const hashCode = this.hashCode(key);
const bucket = this.buckets.get(hashCode);
if (!bucket) {
return false;
}
for (let i = 0; i < bucket.length; i++) {
if (this.areEqual(bucket[i].key, key)) {
bucket.splice(i, 1);
if (bucket.length === 0) {
this.buckets.delete(hashCode);
}
return true;
}
}
return false;
}
forEach(cb: (value: V, key: K) => void): void {
for (const bucket of this.buckets.values()) {
for (const entry of bucket) {
cb(entry.value, entry.key);
}
}
}
clear(): void {
this.buckets.clear();
}
constructor(private hashCode: (key: K) => number, private areEqual: (a: K, b: K) => boolean) {
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -30,6 +30,17 @@ export namespace Material {
return array;
}
export function toArrayNormalized<T extends NumberArray>(material: Material, array: T, offset: number) {
array[offset] = material.metalness;
array[offset + 1] = material.roughness;
array[offset + 2] = material.bumpiness;
return array;
}
export function areEqual(a: Material, b: Material): boolean {
return a.metalness === b.metalness && a.roughness === b.roughness && a.bumpiness === b.bumpiness;
}
export function toString({ metalness, roughness, bumpiness }: Material) {
return `M ${metalness.toFixed(2)} | R ${roughness.toFixed(2)} | B ${bumpiness.toFixed(2)}`;
}