Compare commits

..

1 Commits

Author SHA1 Message Date
dsehnal
14e619d6d2 experimental sequence theme 2025-08-28 06:26:52 +02:00
548 changed files with 14715 additions and 30492 deletions

View File

@@ -1,14 +0,0 @@
# added semicolons to linting rules
fb0634a0f4aab3764b7e6368e38d8dea7615e591
# new linting rules (no default exports, no named tuples)
6c5224f33e9de20fe9967a82536c269bacf29738
# lint: add space-in-parens rule
1d21787e7ea1971817813c008351541e4640c261
# lint: add object-curly-spacing rule
b31302ba3ad4ab7f98aedd500b762be642374ff0
# fix eslint warnings
3b1513adc0048dc4879f1d70874b3e56aaffd10e

View File

@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
node-version: 18
- run: npm ci
- run: sudo apt-get install xvfb
- name: Lint

View File

@@ -4,251 +4,6 @@ All notable changes to this project will be documented in this file, following t
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
## [Unreleased]
- Fix exported image artifacts on transparent background with emissive, bloom, or antialiasing
- Fix cel-shaded ambient color being stripped to luminance (now uses full RGB, matching the classic lighting path)
- Fix empty transforms default in `ShapeFromPly`
- Use morton order for spheres in dot visual with lod-levels
- Add `Camera.changed` event and rotation/translation setter/getter
- Add `instanceGranularity: 'auto'` as a memory guard
- Honor `instanceGranularity` in `Visual.getLoci`
- Add mesoscale representation preset
- Add presets option to `ObjectList` param definition
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
- Fix bugs in ModelServer surroundingLigands endpoint, resulting in omitWater not honored
- Fix `Volume` and `Isosurface` getBoundingSphere ignoring instances
- Fix SSAO half/quarter resolution textures for multi-scale
- Camera improvements
- Add the option to approximate "least obstructed direction" when focusing camera, accessibe via `PluginContext.managers.camera.focusLoci` with `optimizeDirection` option
- Add `CameraFocusOptions.zoomOut` option that zooms out to to make the entire scene visible before focusing on the target
- Add easing support in camera transtion
## [v5.9.0] - 2026-05-03
- Fix edge case when `PluginSpec.animations` is empty
- Add 8K UHD option to `ViewportScreenshotHelper`
- Handle MRC files with empty length header fields
- Handle CCD bonds with Deuterium atoms
- [Breaking] ComponentBond.Entry.map now returns ComponentBond.Pairs
- Fix volume slice marking performance regression
- Add GPU procedural animation (wiggle & tumble)
- Per-vertex wiggle via fbm noise (position & group mode)
- Per-instance tumble via fbm noise (rotation + translation)
- `Wiggle` theme layer for data-driven per-group wiggle
- `enableAnimation` Canvas3D param for global toggle
- Add `AnimateTime` built-in for, e.g., exporting procedural animation
- Add Procedural Animation panels
- Viewer: structure dynamics & uncertainty
- Mesoscale Explorer: entity dynamics
- Fix `GraphQLClient` missing required headers
- [Breaking] Use Record instead of Array for headers (assets & data-source utils)
## [v5.8.0] - 2026-04-03
- Dependencies: remove `utils.promisify`, `node-fetch` (#1797)
- Fix circular dependency which causes crash in bundlers (#1791)
- Add `putty` as a mol-view-spec representation.
- Fix detecting sidechain-only structures as coarse-grained (#1420)
- Fix clip-object transform due to missing axis normalization
- Sequence alignment: Fix return type & improve scoring for unknown residues
- Use PDB SEQRES block to show unresolved residues in Sequence toolbar
- Canvas3D debug-helpers
- [Breaking] Move helpers to an extension as a PluginBehavior (params are no longer part of Canvas3D)
- Add helpers for clip-object, direct-volume, image, mesh
- Fix StructureComponent node update throwing error when substructure empty
- CSS: Avoid tooltip box flickering when hovering something under it
- Volume slice visual
- Fix support for volume instances
- Fix plane mode: ensure normalized & correctly oriented
- MolViewSpec
- Add `VolumeStreamingExtension` (`molstar_volume_streaming` custom property)
- Fix focusing empty selections
- Avoid re-calculating static model properties for trajectories
## [v5.7.0] - 2026-02-28
- Text label improvements
- Improve label background vertical centering
- Handle label depth variant for correct transparent background
- Draw border under text using fragment depth to prevent overlap on adjacent characters
- Clamp border width to avoid exceeding SDF range
- Increase font atlas quality (2x font size multiplier)
- TM-align performance improvements (#1745)
- Disable transparent outline close to opaque elements
- Add axis param to trackball spin & rock animation
- Color smoothing fixes (#1747)
- Use correct instance for non instance-type
- Never transform for non instance-type
- Add extra radius to gaussian surface boundingsphere
- MolViewSpec
- Add `MVSData.toMVSX` function and `mvs-mvsj-to-mvsx.js` CLI utility
- [Breaking] Add PQR file format support (#157)
- Replace `isPdbqt` with `variant` param in `TrajectoryFromPDB`
- Add `CustomVolumeProperty` (like for models and structures)
- Geometry export
- Fix missing `usePalette` support
- Fix vertex-based coloring for non-mesh geometries
- Support line-strips
- Support vertex-based sizing
- Support memory efficient line-strips in Lines geometry,
- Add `StripLinesBuilder`
- Add `computeFrenetFrames` helper
- Streamlines support
- Add basic calculation method
- Add custom-volume-property
- Add representation with lines and tube-mesh visuals
- Fix `TextCtrl` always moving cursor to end position
- Add `vertex` and `vertexInstance` granularity support for size themes
- Add `transform` and `domain` parameters to volume-value size theme
- Fix parsing of single charge type_symbols (e.g., N+) in cif-core
- Detect metal-coordination when parsing pdb
- Handle additional elements in `guessElementSymbol*` (As, Li, Ga)
- Add more element-pair thresholds for bonding (Ag-S, CoSb, Ga-F)
- Add `metalCoordination` style param (dashed, solid) for bonds
- Fix `unitSymmetryGroups` for representations with `includeParent` enabled
- Add `convexHull` helper
- Add `Structure.coordination` sites
- Add `Polyhedron` representation showing coordination sites
- Guard against `xr-spatial-tracking` blocked in `Permissions-Policy`
## [v5.6.1] - 2026-01-23
- Disable occlusion culling in `ImagePass` (#1758)
- MolViewSpec
- Fix `MVSAnnotationStructureComponent` not updating properly when parent structure changes
## [v5.6.0] - 2026-01-18
- Handle Hex codes that are submitted with alpha channels by ignoring the alpha channel (#1746)
- Only show "already registered transformer" warnings in non-production builds
- Fix `label_seq_id` assignment in PDB parser to use 1-based linear indexing (#1730) if:
- when insertion codes are present
- `SEQRES` records are present
- Viewer app
- Add `action: 'focus'` support to `Viewer.structureInteractivity`
- Add `viewportFocusBehavior: 'secondary-zoom'`
- MolViewSpec
- Validation treats `undefined` same as missing value
- Increase default size of `carbohydrate` representation
- `color_from_uri` and `color_from_source` take `selector` parameter
- Add `keepCameraOrientation` option for loading functions
- `label_from_*` and `tooltip_from_*` take `text_format` parameter
- `label_from_*` take `group_by_fields` parameter
- Tweak Gaussian Density smoothness default range (less artefacts)
- Support `includeParent` for Gaussian Surface (disables GPU support)
- Support floodfill before surface extraction (`off`, `interior`, `exterior`)
- For Isosurface, Molecular Surface, Gaussian Surface
- Fix `to_mmCIF` writing duplicate categories under certain conditions (#1738)
- Add stable random number generator (PCG)
- ME grayscale colors; dot offset; SSAO hemisphere vectors
- Use blue noise for SSAO hemisphere vectors
- Fix SSAO darkening when sampling background/offscreen pixels
- Adding structure wireframe visuals on molecular and gaussian surfaces
- Fix caching of `__srcIndexArray__`
- Prevent self-occlusion on quaternary amine
- Fix outline postprocessing artifacts (black bands) on membrane layers at grazing view angles in Illustrative mode (#1749)
- Remove fence from `Canvas3D.render` to not interfer with `requestAnimationFrame`
- Fix boundingSphere reuse in structure visuals (was triggering extra calculation)
- Use PDB seqres record to deduce entity information
- Add lipid components names used in amber ff
## [v5.5.0] - 2025-12-22
- Viewer app
- Move viewer extensions, options, and presets to a separate file
- Add `molstar.lib` export providing access to a wide range of functionality previously not available from the compiled bundle
- Add `Viewer.subscribe` method that keeps track of subscribed plugin events and disposes them together with the parent viewer
- Add `Viewer.structureInteractivity` that makes it easy to highlight/select elements on the loaded structure
- Add `viewportBackgroundColor` and `viewportFocusBehavior` options
- Add `mvs.html` example to showcase the new functionality combined with MolViewSpec
- Add dark and blue color theme support (import `theme/dark.css` or `theme/blue.css` instead of the default `molstar.css`)
- MolViewSpec extension
- Add `tryGetPrimitivesFromLoci` that makes it easier to access primitive element data from hover/click interactions
- Add `getCurrentMVSSnapshot` to obtain source data for the currently displayed snapshot
- Add TM-align structure-based protein alignment algorithm
- New `TMAlign` namespace in `mol-math/linear-algebra/3d/tm-align.ts`
- New `tmAlign` function in `mol-model/structure/structure/util/tm-align.ts`
- Returns TM-score, RMSD, alignment mapping, and transformation matrix
- Molecular Surface
- Fix "auto" quality params not hidden
- Fix calculation when probe diameter is smaller then resolution
- Fix webgl1 shader syntax
- Fix program not compiled for sync picking
- Fix missing `gl.flush` for async picking (needed for Safari)
- Add Residue Charge color scheme (#1722)
- Add dropdown indicator for mapped parameter definitions and adjust "more options" icon
- Fix `flipSided` for meshes
- [Breaking] Interior coloring
- Remove global `interiorDarkening`, `interiorColorFlag`, `interiorColor`
- Add per-geometry `interiorColor`, `interiorSubstance`
- Add `label/auth_comp_id` to `StructureProperties.residue`
- Previously, this has been only been present on `.atom` (since residue name can alter on per-atom basis), but this has been a bit confusing for the general use-case
- Move canvas "checkered background" logic to `canvas3d.ts` and only apply it when `transparentBackground` is on
- This prevents ugly flickering during plugin initialization
- Fix unit hash collision issues (#1721)
## [v5.4.2] - 2025-12-07
- Fix postprocessing issues with SSAO and outlines for large structures (#1387)
- Reduce automatic quality on standalone HMD devices
## [v5.4.1] - 2025-11-16
- Fix ugly camera clipping in snapshot transitions
- Add viewport button to toggle illumination mode
- Fix bounding sphere computation for 3D text
- Structure bounding sphere includes atom VDW radii / coarse sphere radii
- Relax camera limits to allow focusing any selection with >1 atom
- MolViewSpec
- Fix `appendSnapshots` when loading MVSX
- Fix all-selector color not applying on substructure
- Fix primitives in root not being transformed with reference structure
- Color themes do not prefer smoothing (improves performance in animations)
- Allow canvas background interpolation
- Fix `direct-volume` not drawn in illumination mode
- Fix default trackball animated spin speed
- Use `PluginCommands` to set canvas3d props in camera behavior
- Volume improvements
- Add `Volume.periodicity`
- Wrap isosurfaces for periodic volumes
- Fix dimensions for slices
- Add support for Input Method Editor (IME) to text params input
- Update `guessCifVariant` to detect density files not generated by the VolumeServer
## [v5.3.0] - 2025-11-05
- Update loading message in MVS Stories Viewer
- Add `Canvas3D.setAttribs`
- Fix `normalizeWheel` "spin" calculation fallback
- MolViewSpec
- Add support for "topology" formats (TOP, PRMTOP, PSF)
- Add support for additional "coordiates" formats (NCTRAJ, DCD, TRR)
- Fix coarse structure selection
- Fix missing default param values in `primitives_from_uri`
## [v5.2.0] - 2025-10-31
- Handle transparency updates on ImagePass
- Fix CIF parser edge case when the last token is escaped
- MolViewSpec
- Fix tooltips persisting across snapshots
- Fix CIF annotations with no selector columns being ignored
- Fix trackpad lock when camera up parallel to direction
- Add clipping support for primitives
- Support near camera distance
## [v5.1.2] - 2025-10-25
- Fix createColorScaleByType when offsets are available
- Get bond orders from non-standard CONECT records in PDB files
- Remove outdated `gl_FrontFacing` workaround for buggy drivers
- Fix clip objects for direct-volume rendering
- Support "magic window" style AR (via WebXR)
- Fix `PluginState.getStateTransitionFrameIndex`
- Update `GlycamSaccharideNames` and `Monosaccharides` in `carbohydrates/constants.ts`
- Support custom ref resolvers in `State`
- Add full-screen mode support to layout manager
- Add `show-toggle-fullscreen` URL param option to Viewer app
- MolViewSpec
- Support accessing Mol* State nodes by MVS-provided ref
- Add support for DX map format
- Better support for coarse structures in MVS:
- Support for MVS annotations on coarse structures (color_from_*, tooltip_from_*)
- Support for MVS labels on coarse structures (label, label_from_*)
- (Other things already worked on coarse structures before: tooltip, color,component, primitives, component_from_*, primitives_from_*)
- Tidy up MVS builder:
- Add `sphere` and `angle` methods
- [Breaking] Rename builder method primitives_from_uri -> primitivesFromUri
## [v5.0.0] - 2025-09-28
- [Breaking] Renamed some color schemes ('inferno' -> 'inferno-no-black', 'magma' -> 'magma-no-black', 'turbo' -> 'turbo-no-black', 'rainbow' -> 'simple-rainbow')
- [Breaking] `Box3D.nearestIntersectionWithRay` -> `Ray3D.intersectBox3D`
- [Breaking] `Plane3D.distanceToSpher3D` -> `distanceToSphere3D` (fix spelling)
@@ -269,7 +24,7 @@ Note that since we don't clearly distinguish between a public and private interf
- `representation` node: support custom property `molstar_representation_params`
- Add `backbone` and `line` representation types
- `primitives` node: support custom property `molstar_mesh/label/line_params`
- `canvas` node: support custom property `molstar_postprocessing` with the ability to customize outline, depth of field, bloom, shadow, occlusion (SSAO), fog, and background
- `canvas` node: support custom property `molstar_postprocessing` with the ability to customize outline, depth of field, bloom, shadow, occlusion (SSAO), and fog
- `clip` node support for structure and volume representations
- `grid_slice` representation support for volumes
- Support tethers and background for primitive labels
@@ -289,7 +44,6 @@ Note that since we don't clearly distinguish between a public and private interf
- Support loading trajectory coordinates from separate nodes
- Trigger markdown commands from primitives using `molstar_markdown_commands` custom extensions
- Support `molstar_on_load_markdown_commands` custom state on the `root` node
- Print tree validation errors to plugin log
- Added new color schemes, synchronized with D3.js ('inferno', 'magma', 'turbo', 'rainbow', 'sinebow', 'warm', 'cool', 'cubehelix-default', 'category-10', 'observable-10', 'tableau-10')
- Snapshot Markdown improvements
- Add `MarkdownExtensionManager` (`PluginContext.managers.markdownExtensions`)
@@ -346,30 +100,6 @@ Note that since we don't clearly distinguish between a public and private interf
- Add plugin config item ShowReset (shows/hides "Reset Zoom" button)
- Fix transform params not being normalized when used together with param hash version
- Replace `immer` with `mutative`
- Fix renderer transparency check
- Add outlines improvements
- VolumeServer & "VolumeCIF": default to P 1 spacegroup
- Fix `ColorScale` for continuous case without offsets (broke in v4.13.0)
- Experimental: support for custom color themes in Sequence Panel
- Switch files.rcsb.org validation report URL to new endpoint /validation/view
- Improve picking of objects with too many groups, pick whole instance/object
- Add WebXR support
- Requires immersive AR/VR headset
- Supplements non-XR: enter/exit XR anytime and see (mostly) the same scene
- Add `Canvas3D.xr` for managing XR sessions
- Add `PointerHelper` for rendering XR input devices
- Add XR button to Viewer and Mesoscale Explorer
- Add XR button to render-structure in tests/browser
- Fix illumination denoising with transparency on transparent background
- Change the `to_mmCIF` function parameter from `structure` to `structures` to support either a single structure or an array of structures
- ModelServer and VolumeServer: add configurable robots.txt
- Adaptive parallel shader compilation
- Split shader compilation into linking and finalizing
- Start linking as early as possible and wait with finalizing to avoid blocking main thread
- Use of `KHR_parallel_shader_compile` extension when available to check status
- Add `ShaderManager` to compile shaders based on `Canvas3D` params and `Scene` content
- Draw `Scene` only when shaders are ready
- Fix incorrect animation loop handling in the screenshot code
## [v4.18.0] - 2025-06-08
- MolViewSpec extension:

View File

@@ -126,16 +126,16 @@ and navigate to `build/viewer`
**Ion names**
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
**Saccharide names**
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
### Other scripts
**Create chem comp bond table**
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
**Test model server**

View File

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

View File

@@ -14,7 +14,6 @@ chemical.melting_point
chemical_formula.moiety
chemical_formula.sum
chemical_formula.iupac
chemical_formula.weight
atom_type.symbol
@@ -26,8 +25,6 @@ atom_type_scat.source
space_group.crystal_system
space_group.name_h-m_full
space_group.name_h-m_alt
space_group.name_hall
space_group.it_number
space_group_symop.operation_xyz
1 audit.block_doi
14 chemical_formula.iupac chemical_formula.weight
15 chemical_formula.weight atom_type.symbol
16 atom_type.symbol atom_type.description
atom_type.description
17 atom_type_scat.dispersion_real
18 atom_type_scat.dispersion_imag
19 atom_type_scat.source
25 space_group_symop.operation_xyz cell.length_b
26 cell.length_a cell.length_c
27 cell.length_b cell.angle_alpha
cell.length_c
cell.angle_alpha
28 cell.angle_beta
29 cell.angle_gamma
30 cell.volume

View File

@@ -33,11 +33,11 @@ npm run build
For a watch task to automatically rebuild the source code on changes, run
```
npm run dev
npm run watch
```
or if working just with the Viewer app for better performance
```
npm run dev:viewer
npm run watch-viewer
```

View File

@@ -48,7 +48,4 @@
* CLR (e.g. 3GKI) - four fused rings
* Assembly symmetries
* 5M30 (Assembly 1, C3 local and pseudo)
* 1RB8 (Assembly 1, I global)
* Deuterium atoms
* 3CWH (XUL with D and DOD)
* 8TT8 (HOH and other with D)
* 1RB8 (Assembly 1, I global)

View File

@@ -1,6 +1,6 @@
# Building a Custom Library
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page using the `esbuild` tool.
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page.
## Setup

View File

@@ -15,33 +15,10 @@ 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 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`
- 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.
### molstar.js and molstar.css sources
- Download `molstar` NPM package and use the files from `build/viewer` diractory
- Use `jsdelivr` CDN
- `<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.js" />`
- `<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.css" />`
- `@latest` can be replaced by a specific Mol* version, e.g., `@5.4.2`
- Clone & build the GitHub repository
- This option allows for quite straightforward extension customization, e.g., not including movie export, which reduces the bundle size by ~0.5MB
### Bundle size
By default, the `Viewer` includes all the available extensions. This increases the bundle size significantly, especially by including the `mp4-export`, which is responsible for almost `0.5MB` of compressed bundle size.
It is quite easy to reduce this bundle size by cloning the Mol\* repository, editing [extensions.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/options.ts) and rebuilding it with `npm run build:apps`. The new build will be available
in the `build/viewer` directory (the JS file you will find there is uncompressed, but your hosting setup should include automatic gzip compression, significantly reducing the size).
Alternatively, you can explore building your own "viewer" using the base Mol\* library. For this, see the options below.
### Example
Example usage without using WebPack:
```HTML
<style>
@@ -58,7 +35,7 @@ Alternatively, you can explore building your own "viewer" using the base Mol\* l
- 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>
@@ -85,15 +62,13 @@ Alternatively, you can explore building your own "viewer" using the base Mol\* l
</script>
```
### Using WebPack/esbuild/...
When using WebPack (or other bundler) with the Mol* NPM package installed, the viewer class can be imported using
When using WebPack (or possibly other build tool) with the Mol* NPM package installed, the viewer class can be imported using
```ts
import { Viewer } from 'molstar/lib/apps/viewer/app'
import { Viewer } from 'molstar/build/viewer/molstar'
function initViewer(target: string | HTMLElement) {
return Viewer.create(target, { /* options */}) // returns a Promise
return new Viewer(target, { /* options */})
}
```
@@ -164,8 +139,6 @@ 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,
@@ -274,7 +247,7 @@ async function init() {
const canvas = <HTMLCanvasElement> document.getElementById('molstar-canvas');
const parent = <HTMLDivElement> document.getElementById('molstar-parent');
if (!(await plugin.initViewerAsync(canvas, parent))) {
if (!(await plugin.initViewer(canvas, parent))) {
console.error('Failed to init Mol*');
return;
}

View File

@@ -1,44 +1,59 @@
# Selections
## Basic Concepts
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the following method you can select structural data.
### Location
### Selecting directly from the `hierarchy` manager
The selection model in Mol\* is based on a generic concept called *location*. A location is a pointer to a selectable element within a scene. For example:
One can select a subcomponent's data directly from the plugin manager.
- A structure element location (an atom or a coarse element) is an object composed of `{ structure: Structure, unit: Unit, element: UnitIndex }` (you can think of a `Unit` as a generalized chain)
- A bond location is very similar to structure element, requiring pointers to two units and elements
- A "shape" (generally a mesh) location consists of pointer to the parent shape and a group of triangles
```typescript
import { Structure } from '../mol-model/structure';
### Loci
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
Structures and other renderable elements generally consist of many locations and simply using a list of locations would be
prohibitively expensive (e.g., large selections in structures with hundreds of thousands of atoms).
plugin.managers.camera.focusLoci(ligandLoci);
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
```
This is why Mol\* introduces
the concept of `Loci` &mdash; a compressed representation of multiple locations. Instead of having a list of structure element locations (`{ structure: Structure, unit: Unit, element: UnitIndex }[]`), the representation becomes (simplified) `{ structure: Structure, unit: Unit, elements: OrderedSet<UnitIndex> }`. The ordered set can be further compressed for continuous ranges, keeping only the index of the 1st and last element.
## Selection callbacks
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
### Bundle
Here's an example of passing in a React "set" function to update selected residue positions.
```typescript
import {
Structure,
StructureProperties,
} from "molstar/lib/mol-model/structure"
// setSelected is assumed to be a "set" function returned by useState
// (selected: any[]) => void
plugin.behaviors.interaction.click.subscribe(
(event: InteractivityManager.ClickEvent) => {
const selections = Array.from(
plugin.managers.structure.selection.entries.values()
);
// This bit can be customized to record any piece information you want
const localSelected: any[] = [];
for (const { structure } of selections) {
if (!structure) continue;
Structure.eachAtomicHierarchyElement(structure, {
residue: (loc) => {
const position = StructureProperties.residue.label_seq_id(loc);
localSelected.push({ position });
},
});
}
setSelected(localSelected);
}
)
```
Locations and loci point to the raw JavaScript data structures representing the underlying molecules, making them not serializable in JSON. A *bundle* is a serializable version of the loci.
### `Molscript` language
### Structure Queries
Molscript is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
Defining selections directly using the loci would be very cumbersome. For this reason, Mol\* includes the [MolQl query language](https://molql.org) to help define selections.
## Selection Methods
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the methods you can use to create selections.
### MolQL (`mol-script`) language
[MolQL](https://molql.org) (`mol-script`) is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
**Example:** Querying a structure for a specific chain and residue range
Select residues with `12<res_id<200 of chain with auth_asym_id=A`
### Querying a structure for a specific chain and residue range (select residues with 12<res_id<200 of chain with auth_asym_id==A) :
```typescript
import { compileIdListSelection } from 'molstar/lib/mol-script/util/id-list'
@@ -47,12 +62,12 @@ const query = compileIdListSelection('A 12-200', 'auth');
window.molstar?.managers.structure.selection.fromCompiledQuery('add',query);
```
### Selection Queries
## Selection Queries
Another way to create a selection is via a `SelectionQuery` object. This is a more programmatic way to create a selection. The following example shows how to select a chain and a residue range using a `SelectionQuery` object.
This relies on the concept of `Expression` which is basically a intermediate representation between a Molscript statement and a selection query.
**Example:** Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object
### Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object:
```typescript
import { MolScriptBuilder as MS, MolScriptBuilder } from 'molstar/lib/mol-script/language/builder';
@@ -92,7 +107,7 @@ var sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
let loci = StructureSelection.toLociWithSourceUnits(sel);
```
### Query Functions
## Query Functions
Instead of building expressions, query functions can be created directly, e.g.:
@@ -110,7 +125,7 @@ const selection = query(new QueryContext(structure));
// ...
```
### Selection Schema
## Selection Schema
For simple selections, the `StructureElement.Schema` can be used to reference elements within a protein structure using mmCIF `atom_site` field names, e.g.:
@@ -128,63 +143,6 @@ const loci = StructureElement.Loci.fromSchema(structure, residues);
Usually, a code editor such as VS Code will auto-suggest all the available field names.
### Using the `hierarchy` manager
It is possible to select a subcomponent's data directly from the plugin manager.
```typescript
import { Structure } from '../mol-model/structure';
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
plugin.managers.camera.focusLoci(ligandLoci);
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
```
## Selection Events
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
Here's an example of passing in a React "set" function to update selected residue positions.
```typescript
import {
Structure,
StructureProperties,
} from "molstar/lib/mol-model/structure"
// setSelected is assumed to be a "set" function returned by useState
// (selected: any[]) => void
plugin.behaviors.interaction.click.subscribe(
(event: InteractivityManager.ClickEvent) => {
const selections = Array.from(
plugin.managers.structure.selection.entries.values()
);
// This bit can be customized to record any piece information you want
const localSelected: any[] = [];
for (const { structure } of selections) {
if (!structure) continue;
Structure.eachAtomicHierarchyElement(structure, {
residue: (loc) => {
const position = StructureProperties.residue.label_seq_id(loc);
localSelected.push({ position });
},
});
}
setSelected(localSelected);
}
)
```
## Helper Functions
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
### `Viewer` app
The `Viewer` app provides the `structureInteractivity` function which allows easy selection/highlighting of the loaded structure. For example:
```ts
viewer.structureInteractivity({
elements: { beg_auth_seq_id: 10, end_auth_seq_id: 50 },
action: 'select',
});
```
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.

View File

@@ -1,152 +0,0 @@
# 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,7 +33,6 @@ 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'

Binary file not shown.

Binary file not shown.

View File

@@ -1,264 +0,0 @@
%VERSION VERSION_STAMP = V0001.000 DATE = 11/04/25 11:55:47
%FLAG TITLE
%FORMAT(20a4)
alanine-dipeptide.solvated.pdb
%FLAG POINTERS
%FORMAT(10I8)
22 7 12 9 25 11 39 19 0 0
99 3 9 11 19 7 11 20 0 0
0 0 0 0 0 0 0 1 10 0
0 1
%FLAG ATOM_NAME
%FORMAT(20a4)
H1 CH3 H2 H3 C O N H CA HA CB HB1 HB2 HB3 C O N H C H1
H2 H3
%FLAG ATOMIC_NUMBER
%FORMAT(10I8)
1 6 1 1 6 8 7 1 6 1
6 1 1 1 6 8 7 1 6 1
1 1
%FLAG RESIDUE_LABEL
%FORMAT(20a4)
ACE ALA NME
%FLAG RESIDUE_POINTER
%FORMAT(10I8)
1 7 17
%FLAG RESIDUE_NUMBER
%FORMAT(20I4)
1 2 3
%FLAG RESIDUE_ICODE
%FORMAT(20a4)
%FLAG RESIDUE_CHAINID
%FORMAT(20a4)
B B B
%FLAG SOLVENT_POINTERS
%FORMAT(3I8)
0 1 0
%FLAG ATOMS_PER_MOLECULE
%FORMAT(10I8)
22
%FLAG MASS
%FORMAT(5E16.8)
3.02400000E+00 5.96200000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
1.60000000E+01 1.19940000E+01 3.02400000E+00 9.99400000E+00 3.02400000E+00
5.96200000E+00 3.02400000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
1.60000000E+01 1.19940000E+01 3.02400000E+00 5.96200000E+00 3.02400000E+00
3.02400000E+00 3.02400000E+00
%FLAG CHARGE
%FORMAT(5E16.8)
2.04636429E+00 -6.67300626E+00 2.04636429E+00 2.04636429E+00 1.08823576E+01
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 6.14091510E-01 1.49969529E+00
-3.32556975E+00 1.09880469E+00 1.09880469E+00 1.09880469E+00 1.08841798E+01
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 -2.71512270E+00 1.77849648E+00
1.77849648E+00 1.77849648E+00
%FLAG AMBER_ATOM_TYPE
%FORMAT(20a4)
a0 a1 a0 a0 a2 a3 a4 a5 a1 a6 a1 a0 a0 a0 a2 a3 a4 a5 a1 a6
a6 a6
%FLAG ATOM_TYPE_INDEX
%FORMAT(10I8)
1 2 1 1 3 4 5 6 2 7
2 1 1 1 3 4 5 6 2 7
7 7
%FLAG NONBONDED_PARM_INDEX
%FORMAT(10I8)
1 2 4 7 11 16 22 2 3 5
8 12 17 23 4 5 6 9 13 18
24 7 8 9 10 14 19 25 11 12
13 14 15 20 26 16 17 18 19 20
21 27 22 23 24 25 26 27 28
%FLAG LENNARD_JONES_ACOEF
%FORMAT(5E16.8)
7.51607703E+03 9.71708117E+04 1.04308023E+06 8.61541883E+04 9.24822269E+05
8.19971662E+05 5.44261042E+04 6.47841732E+05 5.74393458E+05 3.79876399E+05
8.96776989E+04 9.95480466E+05 8.82619071E+05 6.06829343E+05 9.44293233E+05
1.07193645E+02 2.56678134E+03 2.27577560E+03 1.02595236E+03 2.12601181E+03
1.39982777E-01 4.98586847E+03 6.78771368E+04 6.01816484E+04 3.69471530E+04
6.20665998E+04 5.94667299E+01 3.25969625E+03
%FLAG LENNARD_JONES_BCOEF
%FORMAT(5E16.8)
2.17257828E+01 1.26919150E+02 6.75612247E+02 1.12529845E+02 5.99015525E+02
5.31102864E+02 1.11805549E+02 6.26720080E+02 5.55666449E+02 5.64885984E+02
1.36131731E+02 7.36907417E+02 6.53361429E+02 6.77220874E+02 8.01323529E+02
2.59456373E+00 2.06278363E+01 1.82891803E+01 1.53505284E+01 2.09604198E+01
9.37598976E-02 1.76949863E+01 1.06076943E+02 9.40505981E+01 9.21192137E+01
1.13252062E+02 1.93248820E+00 1.43076527E+01
%FLAG NUMBER_EXCLUDED_ATOMS
%FORMAT(10I8)
6 7 4 3 7 3 10 4 10 7
6 3 2 1 7 3 5 4 3 2
1 1
%FLAG EXCLUDED_ATOMS_LIST
%FORMAT(10I8)
2 3 4 5 6 7 3 4 5 6
7 8 9 4 5 6 7 5 6 7
6 7 8 9 10 11 15 7 8 9
8 9 10 11 12 13 14 15 16 17
9 10 11 15 10 11 12 13 14 15
16 17 18 19 11 12 13 14 15 16
17 12 13 14 15 16 17 13 14 15
14 15 15 16 17 18 19 20 21 22
17 18 19 18 19 20 21 22 19 20
21 22 20 21 22 21 22 22 0
%FLAG BOND_FORCE_CONSTANT
%FORMAT(5E16.8)
3.40000000E+02 4.34000000E+02 3.17000000E+02 5.70000000E+02 4.90000000E+02
3.37000000E+02 3.10000000E+02
%FLAG BOND_EQUIL_VALUE
%FORMAT(5E16.8)
1.09000000E+00 1.01000000E+00 1.52200000E+00 1.22900000E+00 1.33500000E+00
1.44900000E+00 1.52600000E+00
%FLAG BONDS_INC_HYDROGEN
%FORMAT(10I8)
0 3 1 3 6 1 3 9 1 18
21 2 24 27 1 30 33 1 30 36
1 30 39 1 48 51 2 54 57 1
54 60 1 54 63 1
%FLAG BONDS_WITHOUT_HYDROGEN
%FORMAT(10I8)
3 12 3 12 15 4 12 18 5 18
24 6 24 42 3 24 30 7 42 48
5 42 45 4 48 54 6
%FLAG ANGLE_FORCE_CONSTANT
%FORMAT(5E16.8)
3.50000000E+01 5.00000000E+01 5.00000000E+01 5.00000000E+01 8.00000000E+01
7.00000000E+01 5.00000000E+01 8.00000000E+01 8.00000000E+01 6.30000000E+01
6.30000000E+01
%FLAG ANGLE_EQUIL_VALUE
%FORMAT(5E16.8)
1.91113553E+00 1.91113553E+00 2.09439510E+00 2.06018665E+00 2.10137642E+00
2.03505391E+00 2.12755636E+00 2.14500965E+00 1.91462619E+00 1.92160751E+00
1.93906080E+00
%FLAG ANGLES_INC_HYDROGEN
%FORMAT(10I8)
0 3 6 1 0 3 9 1 0 3
12 2 6 3 9 1 6 3 12 2
9 3 12 2 12 18 21 3 18 24
27 2 21 18 24 4 24 30 33 2
24 30 36 2 24 30 39 2 27 24
30 2 27 24 42 2 33 30 36 1
33 30 39 1 36 30 39 1 42 48
51 3 48 54 57 2 48 54 60 2
48 54 63 2 51 48 54 4 57 54
60 1 57 54 63 1 60 54 63 1
%FLAG ANGLES_WITHOUT_HYDROGEN
%FORMAT(10I8)
3 12 15 5 3 12 18 6 12 18
24 7 15 12 18 8 18 24 30 9
18 24 42 10 24 42 45 5 24 42
48 6 30 24 42 11 42 48 54 7
45 42 48 8
%FLAG DIHEDRAL_FORCE_CONSTANT
%FORMAT(5E16.8)
8.00000000E-01 8.00000000E-02 2.50000000E+00 2.50000000E+00 2.00000000E+00
1.55555556E-01 1.10000000E+00 0.00000000E+00 0.00000000E+00 8.00000000E-01
1.80000000E+00 4.20000000E-01 2.70000000E-01 5.50000000E-01 1.58000000E+00
4.50000000E-01 4.00000000E-01 2.00000000E-01 2.00000000E-01 1.05000000E+01
%FLAG DIHEDRAL_PERIODICITY
%FORMAT(5E16.8)
1.00000000E+00 3.00000000E+00 2.00000000E+00 2.00000000E+00 1.00000000E+00
3.00000000E+00 2.00000000E+00 1.00000000E+00 1.00000000E+00 3.00000000E+00
2.00000000E+00 3.00000000E+00 2.00000000E+00 3.00000000E+00 2.00000000E+00
1.00000000E+00 3.00000000E+00 2.00000000E+00 1.00000000E+00 2.00000000E+00
%FLAG DIHEDRAL_PHASE
%FORMAT(5E16.8)
0.00000000E+00 3.14159265E+00 3.14159265E+00 3.14159265E+00 0.00000000E+00
0.00000000E+00 3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00 3.14159265E+00
3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00
%FLAG SCEE_SCALE_FACTOR
%FORMAT(5E16.8)
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 0.00000000E+00
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 1.20000000E+00
0.00000000E+00 1.20000000E+00 0.00000000E+00 1.20000000E+00 0.00000000E+00
0.00000000E+00 1.20000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
%FLAG SCNB_SCALE_FACTOR
%FORMAT(5E16.8)
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 0.00000000E+00
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 2.00000000E+00
0.00000000E+00 2.00000000E+00 0.00000000E+00 2.00000000E+00 0.00000000E+00
0.00000000E+00 2.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
%FLAG DIHEDRALS_INC_HYDROGEN
%FORMAT(10I8)
0 3 12 15 1 0 3 -12 15 2
3 12 18 21 3 6 3 12 15 1
6 3 -12 15 2 9 3 12 15 1
9 3 -12 15 2 15 12 18 21 4
15 12 -18 21 5 18 24 30 33 6
18 24 30 36 6 18 24 30 39 6
24 42 48 51 3 27 24 30 33 6
27 24 30 36 6 27 24 30 39 6
27 24 42 45 1 27 24 -42 45 2
42 24 30 33 6 42 24 30 36 6
42 24 30 39 6 45 42 48 51 4
45 42 -48 51 5 21 18 -24 -12 7
51 48 -54 -42 7 51 48 54 60 8
21 18 24 30 8 42 48 54 57 8
6 3 12 18 9 42 48 54 63 8
51 48 54 57 8 21 18 24 42 8
0 3 12 18 9 42 48 54 60 8
27 24 42 48 8 21 18 24 27 8
51 48 54 63 8 9 3 12 18 9
12 18 24 27 8
%FLAG DIHEDRALS_WITHOUT_HYDROGEN
%FORMAT(10I8)
3 12 18 24 3 12 18 24 30 10
12 18 -24 30 11 12 18 -24 30 5
12 18 24 42 12 12 18 -24 42 13
15 12 18 24 3 18 24 42 48 14
18 24 -42 48 15 18 24 -42 48 16
24 42 48 54 3 30 24 42 48 17
30 24 -42 48 18 30 24 -42 48 19
45 42 48 54 3 15 12 -18 -3 20
45 42 -48 -24 20 18 24 42 45 8
30 24 42 45 8
%FLAG SOLTY
%FORMAT(5E16.8)
%FLAG HBOND_ACOEF
%FORMAT(5E16.8)
%FLAG HBOND_BCOEF
%FORMAT(5E16.8)
%FLAG HBCUT
%FORMAT(5E16.8)
%FLAG TREE_CHAIN_CLASSIFICATION
%FORMAT(20a4)
BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA
BLA BLA
%FLAG JOIN_ARRAY
%FORMAT(10I8)
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0
%FLAG IROTAT
%FORMAT(10I8)
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0
%FLAG BOX_DIMENSIONS
%FORMAT(5E16.8)
9.00000000E+01 3.00000000E+01 3.00000000E+01 3.00000000E+01
%FLAG RADIUS_SET
%FORMAT(1a80)
0
%FLAG RADII
%FORMAT(5E16.8)
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00
%FLAG SCREEN
%FORMAT(5E16.8)
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
0.00000000E+00 0.00000000E+00
%FLAG IPOL
%FORMAT(1I8)
0

View File

@@ -1,26 +0,0 @@
CRYST1 30.000 30.000 30.000 90.00 90.00 90.00 P 1 1
ATOM 1 H1 ACE A 1 2.000 1.000 -0.000 0.00 0.00 H
ATOM 2 CH3 ACE A 1 2.000 2.090 0.000 0.00 0.00 C
ATOM 3 H2 ACE A 1 1.486 2.454 0.890 0.00 0.00 H
ATOM 4 H3 ACE A 1 1.486 2.454 -0.890 0.00 0.00 H
ATOM 5 C ACE A 1 3.427 2.641 -0.000 0.00 0.00 C
ATOM 6 O ACE A 1 4.391 1.877 -0.000 0.00 0.00 O
ATOM 7 N ALA A 2 3.555 3.970 -0.000 0.00 0.00 N
ATOM 8 H ALA A 2 2.733 4.556 -0.000 0.00 0.00 H
ATOM 9 CA ALA A 2 4.853 4.614 -0.000 0.00 0.00 C
ATOM 10 HA ALA A 2 5.408 4.316 0.890 0.00 0.00 H
ATOM 11 CB ALA A 2 5.661 4.221 -1.232 0.00 0.00 C
ATOM 12 HB1 ALA A 2 5.123 4.521 -2.131 0.00 0.00 H
ATOM 13 HB2 ALA A 2 6.630 4.719 -1.206 0.00 0.00 H
ATOM 14 HB3 ALA A 2 5.809 3.141 -1.241 0.00 0.00 H
ATOM 15 C ALA A 2 4.713 6.129 0.000 0.00 0.00 C
ATOM 16 O ALA A 2 3.601 6.653 0.000 0.00 0.00 O
ATOM 17 N NME A 3 5.846 6.835 0.000 0.00 0.00 N
ATOM 18 H NME A 3 6.737 6.359 -0.000 0.00 0.00 H
ATOM 19 C NME A 3 5.846 8.284 0.000 0.00 0.00 C
ATOM 20 H1 NME A 3 4.819 8.648 0.000 0.00 0.00 H
ATOM 21 H2 NME A 3 6.360 8.648 0.890 0.00 0.00 H
ATOM 22 H3 NME A 3 6.360 8.648 -0.890 0.00 0.00 H
TER 23 NME A 3
CONECT 5 7
CONECT 15 17

View File

@@ -1,14 +0,0 @@
alanine-dipeptide.solvated.pdb
22
0.7494821 1.2436848 0.8743532 1.0856344 2.2423820 0.5955986
0.4304414 2.9747953 1.0671825 1.0497815 2.3544810 -0.4880289
2.5015950 2.4471725 1.0820421 3.1003812 1.5343071 1.6479120
3.0220696 3.6519467 0.8741013 2.4411554 4.3533213 0.4373955
4.3920715 4.0500473 1.2160543 4.7674596 3.4172266 2.0202454
5.2805058 3.8202998 -0.0180103 4.9565949 4.4537317 -0.8438106
6.3180425 4.0583459 0.2164072 5.2327259 2.7740601 -0.3200050
4.4431625 5.5106563 1.7135265 3.4307644 6.2198007 1.6891606
5.6170320 5.9613562 2.1744082 6.3997462 5.3231585 2.1616313
5.8784762 7.3296314 2.6320299 5.1056278 8.0184146 2.2908769
5.9253575 7.3544224 3.7207393 6.8360338 7.6745804 2.2419090
30.0000000 30.0000000 30.0000000 90.0000000 90.0000000 90.0000000

21731
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.9.0",
"version": "5.0.0-dev.10",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -11,7 +11,7 @@
"url": "https://github.com/molstar/molstar/issues"
},
"engines": {
"node": ">=22.0.0"
"node": ">=18.0.0"
},
"scripts": {
"lint": "eslint .",
@@ -74,7 +74,7 @@
"js"
],
"transform": {
"\\.ts$": ["esbuild-jest-transform", { "tsconfigRaw": "{\"compilerOptions\":{\"useDefineForClassFields\":false}}" }]
"\\.ts$": "ts-jest"
},
"moduleDirectories": [
"node_modules",
@@ -123,61 +123,60 @@
"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>",
"Diego del Alamo <diego.delalamo@gmail.com>",
"Tianzhen Lin (Tangent) <tangent@usa.net>"
"Victoria Doshchenko <doshchenko.victoria@gmail.com>"
],
"license": "MIT",
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/gl": "^6.0.5",
"@types/jest": "^30.0.0",
"@types/jest": "^29.5.14",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.28",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/webxr": "^0.5.24",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"benchmark": "^2.1.4",
"concurrently": "^9.2.1",
"cpx2": "^8.0.2",
"css-loader": "^7.1.4",
"esbuild": "^0.28.0",
"esbuild-jest-transform": "^2.0.1",
"esbuild-sass-plugin": "^3.7.0",
"eslint": "^10.3.0",
"fs-extra": "^11.3.4",
"globals": "^17.6.0",
"concurrently": "^9.1.2",
"cpx2": "^8.0.0",
"css-loader": "^7.1.2",
"esbuild": "^0.25.5",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^9.29.0",
"fs-extra": "^11.3.0",
"http-server": "^14.1.1",
"jest": "^30.3.0",
"jest": "^29.7.0",
"jpeg-js": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.99.0",
"simple-git": "^3.36.0",
"tsc-alias": "^1.8.17",
"typescript": "^6.0.3"
"sass": "^1.89.1",
"simple-git": "^3.28.0",
"ts-jest": "^29.3.4",
"tsc-alias": "^1.8.16",
"typescript": "^5.8.3"
},
"dependencies": {
"@types/argparse": "^2.0.17",
"@types/benchmark": "^2.1.5",
"@types/compression": "1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^22.19.17",
"@types/swagger-ui-dist": "3.30.6",
"@types/express": "^5.0.3",
"@types/node": "^18.19.111",
"@types/node-fetch": "^2.6.12",
"@types/swagger-ui-dist": "3.30.5",
"argparse": "^2.0.1",
"compression": "^1.8.1",
"cors": "^2.8.6",
"express": "^5.2.1",
"compression": "^1.8.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"h264-mp4-encoder": "^1.0.12",
"immutable": "^5.1.5",
"immutable": "^5.1.2",
"io-ts": "^2.2.22",
"mutative": "^1.3.0",
"mutative": "^1.2.0",
"node-fetch": "^2.7.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"rxjs": "^7.8.2",
"swagger-ui-dist": "^5.32.5",
"tslib": "^2.8.1"
"swagger-ui-dist": "^5.24.0",
"tslib": "^2.8.1",
"util.promisify": "^1.1.3"
},
"peerDependencies": {
"@google-cloud/storage": "^7.14.0",
@@ -205,4 +204,4 @@
"optional": true
}
}
}
}

View File

@@ -13,7 +13,7 @@ import * as os from 'os';
const Apps = [
// Apps
{ kind: 'app', name: 'viewer', themes: ['light', 'dark', 'blue'] },
{ kind: 'app', name: 'viewer' },
{ kind: 'app', name: 'docking-viewer' },
{ kind: 'app', name: 'mesoscale-explorer' },
{ kind: 'app', name: 'mvs-stories', globalName: 'mvsStories', filename: 'mvs-stories.js' },
@@ -131,6 +131,7 @@ function getPaths(app) {
async function createBundle(app) {
const { name, kind } = app;
const { prefix, entry, outfile } = getPaths(app);
const ctx = await esbuild.context({
@@ -160,7 +161,6 @@ async function createBundle(app) {
color: true,
logLevel: 'info',
define: {
'process.env.NODE_ENV': JSON.stringify(NODE_ENV_PRD ? 'production' : 'development'),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
__MOLSTAR_PLUGIN_VERSION__: JSON.stringify(VERSION),
__MOLSTAR_BUILD_TIMESTAMP__: `${TIMESTAMP}`,
@@ -172,41 +172,6 @@ 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', ''));
@@ -264,7 +229,6 @@ 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();
@@ -296,14 +260,7 @@ async function main() {
const promises = [];
console.log(isProduction ? 'Building apps...' : 'Initial build...');
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 app of apps) promises.push(createBundle(app));
for (const example of examples) promises.push(createBundle(example));
for (const browserTest of browserTests) promises.push(createBundle(browserTest));

View File

@@ -253,10 +253,6 @@ export class MesoscaleExplorer {
},
cameraFog: { name: 'off', params: {} },
hiZ: { enabled: true },
xr: {
disablePostprocessing: false,
sceneRadiusInMeters: 0.75,
},
});
plugin.representation.structure.registry.clear();

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -21,7 +21,6 @@ const Key = Binding.TriggerKey;
const DefaultMesoFocusLociBindings = {
clickCenter: Binding([
Trigger(B.Flag.Primary, M.create()),
Trigger(B.Flag.Trigger),
], 'Camera center', 'Click element using ${triggers}'),
clickCenterFocus: Binding([
Trigger(B.Flag.Secondary, M.create()),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -24,8 +24,7 @@ const Trigger = Binding.Trigger;
const DefaultMesoSelectLociBindings = {
click: Binding([
Trigger(B.Flag.Primary, M.create()),
Trigger(B.Flag.Trigger),
Trigger(B.Flag.Primary, M.create())
], 'Click', 'Click element using ${triggers}'),
clickToggleSelect: Binding([
Trigger(B.Flag.Primary, M.create({ shift: true })),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 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,12 +36,6 @@ 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: {
@@ -55,7 +49,7 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
sizeTheme: {
name: 'physical',
params: {
scale: 1,
value: 1,
}
},
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -50,12 +50,6 @@ 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,11 +1,10 @@
/**
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
import { Model } from '../../../../mol-model/structure/model/model';
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../../../mol-plugin/context';
@@ -41,12 +40,6 @@ 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: {
@@ -60,7 +53,7 @@ function getSpacefillParams(color: Color, scaleFactor: number, graphics: Graphic
sizeTheme: {
name: 'physical',
params: {
scale: scaleFactor,
value: 1,
}
},
};
@@ -103,8 +96,6 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
});
}
const coarseGrained = Model.isCoarseGrained(model.data!);
const entGroups = new Map<string, StateObjectSelector>();
const entIds = new Map<string, { idx: number, members: Map<number, number> }>();
const entColors = new Map<string, Color[]>();
@@ -173,7 +164,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
for (let i = 0; i < entities._rowCount; i++) {
const t = getEntityType(i);
const color = entColors.get(t)![entIds.get(t)!.members.get(i)!];
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || (coarseGrained ? 2 : 1);
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || 1;
build = build
.toRoot()

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -35,12 +35,6 @@ 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) 2023-2026 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>
*/
@@ -10,7 +10,6 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Task } from '../../../mol-task';
import { Color } from '../../../mol-util/color';
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
import { getAnimationParam } from '../../../mol-geo/geometry/animation';
import { Clip } from '../../../mol-util/clip';
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
@@ -22,10 +21,10 @@ import { Hcl } from '../../../mol-util/color/spaces/hcl';
import { StateObjectCell, StateObjectRef, StateSelection } from '../../../mol-state';
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/representation/spacefill';
import { assertUnreachable } from '../../../mol-util/type-helpers';
import { MesoscaleExplorerState } from '../app';
import { saturate } from '../../../mol-math/interpolate';
import { Material } from '../../../mol-util/material';
import { PCG } from '../../../mol-data/util/hash-functions';
function getHueRange(hue: number, variability: number) {
let min = hue - variability;
@@ -38,11 +37,10 @@ function getHueRange(hue: number, variability: number) {
function getGrayscaleColors(count: number, luminance: number, variability: number) {
const out: Color[] = [];
const pcg = new PCG();
for (let i = 0; i < count; ++ i) {
const l = saturate(luminance / 100);
const v = saturate(variability / 180) * pcg.float();
const s = pcg.float() > 0.5 ? 1 : -1;
const v = saturate(variability / 180) * Math.random();
const s = Math.random() > 0.5 ? 1 : -1;
const d = Math.abs(l + s * v) % 1;
out[i] = Color.fromNormalizedRgb(d, d, d);
}
@@ -174,8 +172,6 @@ export const LodParams = {
approximate: Spheres.Params.approximate,
};
export const AnimationParams = getAnimationParam().params;
export const SimpleClipParams = {
type: PD.Select('none', PD.objectToOptions(Clip.Type, t => stringToWords(t))),
invert: PD.Boolean(false),
@@ -283,7 +279,6 @@ export const MesoscaleGroupParams = {
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
lod: PD.Group(LodParams),
clip: PD.Group(SimpleClipParams),
animation: PD.Group(AnimationParams),
};
export type MesoscaleGroupProps = PD.Values<typeof MesoscaleGroupParams>;
@@ -321,7 +316,38 @@ export function getMesoscaleGroupParams(graphicsMode: GraphicsMode): MesoscaleGr
export type LodLevels = typeof SpacefillRepresentationProvider.defaultValues['lodLevels']
export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): LodLevels {
return Spheres.LodLevelsPresets[graphicsMode];
switch (graphicsMode) {
case 'performance':
return [
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
];
case 'balanced':
return [
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
];
case 'quality':
return [
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
];
case 'ultra':
return [
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
];
default:
assertUnreachable(graphicsMode);
}
}
export type GraphicsMode = 'ultra' | 'quality' | 'balanced' | 'performance' | 'custom';

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -18,7 +18,7 @@ import { CombinedColorControl } from '../../../mol-plugin-ui/controls/color';
import { MarkerAction } from '../../../mol-util/marker-action';
import { EveryLoci, Loci } from '../../../mol-model/loci';
import { deepEqual } from '../../../mol-util';
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, AnimationParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
import React, { useState } from 'react';
import { MesoscaleExplorerState } from '../app';
import { StructureElement } from '../../../mol-model/structure/structure/element';
@@ -828,26 +828,6 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
update.commit();
};
updateAnimation = (values: PD.Values) => {
const update = this.plugin.state.data.build();
for (const r of this.allFilteredEntities) {
update.to(r).update(old => {
if (old.type) {
old.type.params.animation = values;
}
});
}
for (const g of this.allGroups) {
update.to(g).update(old => {
old.animation = values;
});
}
update.commit();
};
update = (props: MesoscaleGroupProps) => {
this.plugin.state.data.build().to(this.ref).update(props);
};
@@ -885,7 +865,6 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
const rootValue = this.cell.params?.values.color;
const clipValue = this.cell.params?.values.clip;
const lodValue = this.cell.params?.values.lod;
const animationValue = this.cell.params?.values.animation;
const isRoot = this.cell.params?.values.root;
const groups = this.groups;
@@ -925,7 +904,6 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<ParameterControls params={SimpleClipParams} values={clipValue} onChangeValues={this.updateClip} />
<ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />
<ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />
</ControlGroup>
</div>}
{this.state.action === 'root' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
@@ -1102,19 +1080,6 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
};
}
get animationValue(): PD.Values<typeof AnimationParams> | undefined {
const p = this.cell.transform.params?.type?.params?.animation;
if (!p) return;
return {
wiggleMode: p.wiggleMode,
wiggleSpeed: p.wiggleSpeed,
wiggleAmplitude: p.wiggleAmplitude,
wiggleFrequency: p.wiggleFrequency,
tumbleSpeed: p.tumbleSpeed,
tumbleAmplitude: p.tumbleAmplitude,
};
}
get patternValue(): { amplitude: number, frequency: number } | undefined {
const p = this.cell.transform.params;
if (p.type) return;
@@ -1229,15 +1194,6 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
}
};
updateAnimation = (values: PD.Values) => {
const params = this.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
if (!params.type) return;
this.plugin.build().to(this.ref).update(old => {
old.type.params.animation = values;
}).commit();
};
updatePattern = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (!old.type) {
@@ -1257,7 +1213,6 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
const opacityValue = this.opacityValue;
const emissiveValue = this.emissiveValue;
const lodValue = this.lodValue;
const animationValue = this.animationValue;
const patternValue = this.patternValue;
const l = getEntityLabel(this.plugin, this.cell);
@@ -1296,7 +1251,6 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<ParameterMappingControl mapping={this.clipMapping} />
{lodValue && <ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />}
{animationValue && <ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />}
</ControlGroup>
</div>}
</>;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -13,19 +13,17 @@ import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/
import { MesoscaleExplorerState } from '../app';
import { MesoscaleState } from '../data/state';
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, MesoProceduralAnimationControls, ExplorerInfo } from './states';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
import { RendererParams } from '../../../mol-gl/renderer';
import { TrackballControlsParams } from '../../../mol-canvas3d/controls/trackball';
import { XRManagerParams } from '../../../mol-canvas3d/helper/xr-manager';
const Spacer = () => <div style={{ height: '2em' }} />;
const ViewportParams = {
renderer: PD.Group(RendererParams),
trackball: PD.Group(TrackballControlsParams),
xr: PD.Group(XRManagerParams, { label: 'XR' }),
};
class ViewportSettingsUI extends CollapsableControls<{}, {}> {
@@ -145,7 +143,6 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
<StructureMeasurementsControls initiallyCollapsed={true}/>
</>
<MesoQuickStylesControls />
<MesoProceduralAnimationControls />
<Spacer />
<SectionHeader title='Entities' />
<EntityControls />

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -8,7 +8,7 @@ import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
import { AnimationSvg, GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
import { CollapsableControls, PluginUIComponent } from '../../../mol-plugin-ui/base';
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
@@ -24,7 +24,7 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
import { createGenericHierarchy } from '../data/generic/preset';
import { createMmcifHierarchy } from '../data/mmcif/preset';
import { createPetworldHierarchy } from '../data/petworld/preset';
import { getAllEntities, getAllGroups, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
import { getAllEntities, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
import { isTimingMode } from '../../../mol-util/debug';
import { now } from '../../../mol-util/now';
import { readFromFile } from '../../../mol-util/data-source';
@@ -46,6 +46,8 @@ function adjustPluginProps(ctx: PluginContext) {
dimColor: Color(0xffffff),
dimStrength: 1,
markerPriority: 2,
interiorColorFlag: false,
interiorDarkening: 0.15,
exposure: 1.1,
xrayEdgeFalloff: 3,
},
@@ -779,110 +781,3 @@ export class MesoQuickStyles extends PluginUIComponent {
</>;
}
}
export class MesoProceduralAnimationControls extends CollapsableControls {
defaultState() {
return {
isCollapsed: true,
header: 'Procedural Animation',
brand: { accent: 'gray' as const, svg: AnimationSvg }
};
}
renderControls() {
return <>
<MesoProceduralAnimation />
</>;
}
}
class MesoProceduralAnimation extends PluginUIComponent {
private isMembrane(cell: { transform: { tags?: string[] } }) {
return cell.transform.tags?.some(t => t.includes('mem')) ?? false;
}
async dynamics() {
const update = this.plugin.state.data.build();
const entities = getAllEntities(this.plugin);
const groups = getAllGroups(this.plugin);
for (const entity of entities) {
const membrane = this.isMembrane(entity);
update.to(entity).update(old => {
if (old.type) {
old.type.params.animation = {
...old.type.params.animation,
wiggleMode: 'position',
wiggleSpeed: 7,
wiggleAmplitude: 1,
wiggleFrequency: 0.2,
tumbleSpeed: 1,
tumbleAmplitude: membrane ? 0 : 4,
tumbleFrequency: 0.2,
};
}
});
}
for (const group of groups) {
const membrane = this.isMembrane(group);
update.to(group).update(old => {
old.animation = {
...old.animation,
wiggleMode: 'position',
wiggleSpeed: 7,
wiggleAmplitude: 1,
wiggleFrequency: 0.2,
tumbleSpeed: 1,
tumbleAmplitude: membrane ? 0 : 4,
tumbleFrequency: 0.2,
};
});
}
await update.commit();
}
async clear() {
const update = this.plugin.state.data.build();
const entities = getAllEntities(this.plugin);
const groups = getAllGroups(this.plugin);
for (const entity of entities) {
update.to(entity).update(old => {
if (old.type) {
old.type.params.animation = {
...old.type.params.animation,
wiggleAmplitude: 0,
tumbleAmplitude: 0,
};
}
});
}
for (const group of groups) {
update.to(group).update(old => {
old.animation = {
...old.animation,
wiggleAmplitude: 0,
tumbleAmplitude: 0,
};
});
}
await update.commit();
}
render() {
return <>
<div className='msp-flex-row'>
<Button noOverflow title='Enable wiggle for all entities and tumble for non-membrane entities' onClick={() => this.dynamics()} style={{ width: 'auto' }}>
Dynamics
</Button>
<Button noOverflow title='Set wiggle and tumble amplitude to zero for all entities' onClick={() => this.clear()} style={{ width: 'auto' }}>
Clear
</Button>
</div>
</>;
}
}

View File

@@ -9,14 +9,14 @@ import { MVSData } from '../../extensions/mvs/mvs-data';
import type { MVSStoriesViewerModel } from './elements/viewer';
export type MVSStoriesCommand =
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData | string | Uint8Array<ArrayBuffer> }
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData | string | Uint8Array }
export class MVSStoriesContext {
commands = new BehaviorSubject<MVSStoriesCommand | undefined>(undefined);
state = {
viewers: new BehaviorSubject<{ name?: string, model: MVSStoriesViewerModel }[]>([]),
currentStoryData: new BehaviorSubject<string | Uint8Array<ArrayBuffer> | undefined>(undefined),
currentStoryData: new BehaviorSubject<string | Uint8Array | undefined>(undefined),
isLoading: new BehaviorSubject(false),
};

View File

@@ -12,7 +12,7 @@ import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
import { createRoot } from 'react-dom/client';
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
import { PluginReactContext } from '../../../mol-plugin-ui/base';
import { CSSProperties, useEffect, useState } from 'react';
import { CSSProperties } from 'react';
import { Markdown } from '../../../mol-plugin-ui/controls/markdown';
export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
@@ -70,28 +70,6 @@ export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
}
}
function Loading() {
return <div>
<div style={{ marginBottom: 16 }}><i>Loading times may vary depending on the story size, your internet connection, and device performance</i></div>
<div>Fetching data<Dots /></div>
<div>Generating animations<Dots /></div>
<div>Preparing visuals<Dots /></div>
</div>;
}
function Dots() {
const [dots, setDots] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setDots(d => (d + 1) % 4);
}, Math.random() * 500 + 300);
return () => clearInterval(interval);
}, []);
return <span>{'.'.repeat(dots)}</span>;
}
export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnapshotMarkdownModel }) {
const state = useBehavior(model.state);
const isLoading = useBehavior(model.context.state.isLoading);
@@ -101,8 +79,7 @@ export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnaps
if (isLoading) {
return <div style={style} className={className}>
<h3>The story will be ready momentarily</h3>
<Loading />
<i>Loading...</i>
</div>;
}

View File

@@ -66,7 +66,7 @@ export class MVSStoriesViewerModel extends PluginComponent {
loadedData = await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
}
if (StringLike.is(loadedData) || loadedData instanceof Uint8Array) {
this.context.state.currentStoryData.next(loadedData as string | Uint8Array<ArrayBuffer>);
this.context.state.currentStoryData.next(loadedData as string | Uint8Array);
} else if (loadedData) {
this.context.state.currentStoryData.next(JSON.stringify(loadedData));
}

View File

@@ -94,8 +94,8 @@
</div>
<div id="links">
<span id="open-in-stories" style="display: none;"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a>&nbsp;<span class="sep"></span></span>
<span id="open-in-molstar" style="display: none;"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a>&nbsp;<span class="sep"></span></span>
<span id="open-in-stories"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a>&nbsp;<span class="sep"></span></span>
<span id="open-in-molstar"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a>&nbsp;<span class="sep"></span></span>
<a href="#" id="mvs-data" title="MolViewSpec State for this story. Can be opened in the Mol* app.">Download MVS</a> <span class="sep"></span> <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>

View File

@@ -28,7 +28,7 @@ export function loadFromURL(url: string, options?: { format: 'mvsx' | 'mvsj', co
}, 0);
}
export function loadFromData(data: MVSData | string | Uint8Array<ArrayBuffer>, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
export function loadFromData(data: MVSData | string | Uint8Array, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
setTimeout(() => {
getContext(options?.contextName).dispatch({
kind: 'load-mvs',

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2026 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>
@@ -7,20 +7,34 @@
* @author Adam Midlik <midlik@gmail.com>
*/
import { AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
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 { loadMVSData, loadMVSX } from '../../extensions/mvs/components/formats';
import { loadMVS, MolstarLoadingExtension } from '../../extensions/mvs/load';
import { MVSData } from '../../extensions/mvs/mvs-data';
import { StringLike } from '../../mol-io/common/string-like';
import { Structure, StructureElement } from '../../mol-model/structure';
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 { 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 { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { PluginComponent } from '../../mol-plugin-state/component';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
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';
@@ -28,39 +42,97 @@ 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 { createPluginUI } from '../../mol-plugin-ui';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { createPluginUI } from '../../mol-plugin-ui';
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 } from '../../mol-plugin/config';
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { PluginSpec } from '../../mol-plugin/spec';
import { PluginState } from '../../mol-plugin/state';
import { MolScriptBuilder } from '../../mol-script/language/builder';
import { Expression } from '../../mol-script/language/expression';
import { StateObjectSelector } from '../../mol-state';
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
import { Task } from '../../mol-task';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import { ExtensionMap } from './extensions';
import { DefaultViewerOptions, ViewerOptions } from './options';
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';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { consoleStats, isDebugMode, isProductionMode, isTimingMode, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
export { consoleStats, setDebugMode, setProductionMode, setTimingMode, isProductionMode, isDebugMode, isTimingMode } from '../../mol-util/debug';
import { decodeColor } from '../../mol-util/color/utils';
import '../../mol-util/polyfill';
import { ViewerAutoPreset } from './presets';
import { CameraFocusLociOptions } from '../../mol-plugin-state/manager/camera';
import { PluginSpec } from '../../mol-plugin/spec';
import { NoPrimaryFocusLociBindings } from '../../mol-plugin/behavior/dynamic/camera';
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,
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;
export class Viewer {
private _events = new PluginComponent();
public readonly plugin: PluginUIContext;
constructor(plugin: PluginUIContext) {
this.plugin = plugin;
constructor(public plugin: PluginUIContext) {
}
static async create(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
@@ -75,31 +147,11 @@ export class Viewer {
const defaultSpec = DefaultPluginUISpec();
const disabledExtension = new Set(o.disabledExtensions ?? []);
let baseBehaviors = defaultSpec.behaviors;
if (o.viewportFocusBehavior === 'disabled') {
baseBehaviors = baseBehaviors.filter(b =>
b.transformer !== PluginBehaviors.Camera.FocusLoci
&& b.transformer !== PluginBehaviors.Representation.FocusLoci
);
} else if (o.viewportFocusBehavior === 'secondary-zoom') {
baseBehaviors = baseBehaviors.filter(b =>
b.transformer !== PluginBehaviors.Camera.FocusLoci
&& b.transformer !== PluginBehaviors.Representation.FocusLoci
);
baseBehaviors.push(PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci, {
bindings: NoPrimaryFocusLociBindings
}));
}
const spec: PluginUISpec = {
canvas3d: {
...defaultSpec.canvas3d,
},
actions: defaultSpec.actions,
behaviors: [
...baseBehaviors,
...defaultSpec.behaviors,
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
],
animations: [...defaultSpec.animations || []],
@@ -141,7 +193,6 @@ export class Viewer {
[PluginConfig.Viewport.ShowScreenshotControls, o.viewportShowScreenshotControls],
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowToggleFullscreen, o.viewportShowToggleFullscreen],
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
@@ -175,23 +226,10 @@ 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 });
@@ -483,7 +521,7 @@ export class Viewer {
return { model, coords, preset };
}
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
if (format === 'mvsj') {
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
const mvsData = MVSData.fromMVSJ(StringLike.toString(data));
@@ -491,7 +529,7 @@ export class Viewer {
} else if (format === 'mvsx') {
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
await this.plugin.runTask(Task.create('Load MVSX file', async ctx => {
const parsed = await loadMVSX(this.plugin, ctx, data, { doNotClearAssets: options?.appendSnapshots });
const parsed = await loadMVSX(this.plugin, ctx, data);
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl, ...options });
}));
} else {
@@ -502,7 +540,7 @@ export class Viewer {
/** Load MolViewSpec from `data`.
* If `format` is 'mvsj', `data` must be a string or a Uint8Array containing a UTF8-encoded string.
* If `format` is 'mvsx', `data` must be a Uint8Array or a string containing base64-encoded binary data prefixed with 'base64,'. */
loadMvsData(data: string | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
return loadMVSData(this.plugin, data, format, options);
}
@@ -527,66 +565,7 @@ export class Viewer {
this.plugin.layout.events.updated.next(void 0);
}
/**
* Triggers structure element selection or highlighting based on the provided
* MolScript expression or StructureElement schema. Focus action will only apply to the
* first structure that matches the criteria.
*
* If neither `expression` nor `elements` are provided, all selections/highlights
* will be cleared based on the specified `action`.
*/
structureInteractivity({ expression, elements, action: action_, applyGranularity = false, filterStructure, focusOptions }: {
expression?: (queryBuilder: typeof MolScriptBuilder) => Expression,
elements?: StructureElement.Schema,
action: 'highlight' | 'select' | 'focus' | ('highlight' | 'select' | 'focus')[],
applyGranularity?: boolean,
filterStructure?: (structure: Structure) => boolean,
focusOptions?: Partial<CameraFocusLociOptions>
}) {
const plugin = this.plugin;
const actions = Array.isArray(action_) ? action_ : [action_];
if (!expression && !elements) {
if (actions.includes('select')) {
plugin.managers.interactivity.lociSelects.deselectAll();
}
if (actions.includes('highlight')) {
plugin.managers.interactivity.lociHighlights.clearHighlights();
}
return;
}
if (actions.includes('select')) {
plugin.managers.interactivity.lociSelects.deselectAll();
}
const structures = this.plugin.state.data.selectQ(Q => Q.rootsOfType(PluginStateObject.Molecule.Structure));
let focused = false;
for (const s of structures) {
if (!s.obj?.data) continue;
if (filterStructure && !filterStructure(s.obj.data)) continue;
const loci = expression
? StructureElement.Loci.fromExpression(s.obj.data, expression)
: StructureElement.Loci.fromSchema(s.obj.data, elements!);
for (const action of actions) {
if (action === 'select') {
plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
} else if (action === 'highlight') {
plugin.managers.interactivity.lociHighlights.highlight({ loci }, applyGranularity);
} else if (action === 'focus' && !StructureElement.Loci.isEmpty(loci) && !focused) {
plugin.managers.camera.focusLoci(loci, focusOptions);
focused = true;
if (actions.length === 1) return; // if only focusing, focus the first matching structure and return immediately
}
}
}
}
dispose() {
this._events.dispose();
this.plugin.dispose();
}
}
@@ -605,12 +584,52 @@ export interface VolumeIsovalueInfo {
export interface LoadTrajectoryParams {
model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format?: BuiltInTrajectoryFormat /* mmcif */ }
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
| { kind: 'topology-url', url: string, format: BuiltInTopologyFormat, isBinary?: boolean }
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInTopologyFormat },
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuiltInTopologyFormat },
modelLabel?: string,
coordinates: { kind: 'coordinates-url', url: string, format: BuiltInCoordinatesFormat, isBinary?: boolean }
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInCoordinatesFormat },
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, 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

@@ -1,71 +0,0 @@
/**
* Copyright (c) 2018-2026 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 { DebugHelpers } from '../../extensions/debug-helpers';
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),
'debug-helpers': PluginSpec.Behavior(DebugHelpers),
'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

@@ -28,10 +28,10 @@
}
#app {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
left: 100px;
top: 100px;
width: 800px;
height: 600px;
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
@@ -66,7 +66,6 @@
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
var illumination = getParam('illumination', '[^&]+').trim() === '1';
var resolutionMode = getParam('resolution-mode', '[^&]+').trim().toLowerCase();
var viewportShowToggleFullscreen = getParam('show-toggle-fullscreen', '[^&]+').trim() === '1';
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
@@ -74,7 +73,6 @@
disabledExtensions: [], // anything from Object.keys(molstar.ExtensionMap)
layoutShowControls: !hideControls,
viewportShowExpand: false,
viewportShowToggleFullscreen: viewportShowToggleFullscreen,
collapseLeftPanel: collapseLeftPanel,
pdbProvider: pdbProvider || 'pdbe',
emdbProvider: emdbProvider || 'pdbe',
@@ -89,7 +87,7 @@
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
illumination: illumination,
resolutionMode: resolutionMode || 'auto',
resolutionMode: resolutionMode || 'auto'
}).then(viewer => {
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);

View File

@@ -1,16 +1,12 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2022 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';

View File

@@ -1,58 +0,0 @@
/**
* 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
}
};

View File

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

View File

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

View File

@@ -1,42 +0,0 @@
/**
* 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

@@ -1,7 +0,0 @@
/**
* 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

@@ -1,7 +0,0 @@
/**
* 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

@@ -1,7 +0,0 @@
/**
* 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

@@ -1,16 +1,17 @@
#!/usr/bin/env node
/**
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Josh McMenemy <josh.mcmenemy@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as argparse from 'argparse';
import * as path from 'path';
import util from 'util';
import fs from 'fs';
const writeFileAsync = fs.promises.writeFile;
require('util.promisify').shim();
const writeFile = util.promisify(fs.writeFile);
import { DatabaseCollection } from '../../mol-data/db';
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
@@ -31,7 +32,7 @@ function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
function writeIonNamesFile(filePath: string, ionNames: string[]) {
const output = `/**
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated ion names params file. Names extracted from CCD components.
*
@@ -40,7 +41,7 @@ function writeIonNamesFile(filePath: string, ionNames: string[]) {
export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
`;
writeFileAsync(filePath, output);
writeFile(filePath, output);
}
async function run(out: string, options = DefaultDataOptions) {

View File

@@ -1,15 +1,16 @@
#!/usr/bin/env node
/**
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as argparse from 'argparse';
import * as path from 'path';
import util from 'util';
import fs from 'fs';
const writeFileAsync = fs.promises.writeFile;
require('util.promisify').shim();
const writeFile = util.promisify(fs.writeFile);
import { DatabaseCollection } from '../../mol-data/db';
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
@@ -43,7 +44,7 @@ function writeSaccharideNamesFile(filePath: string, ionNames: string[]) {
export const SaccharideNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
`;
writeFileAsync(filePath, output);
writeFile(filePath, output);
}
async function run(out: string, options = DefaultDataOptions) {

View File

@@ -1,15 +1,16 @@
#!/usr/bin/env node
/**
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as argparse from 'argparse';
import * as util from 'util';
import * as path from 'path';
import * as fs from 'fs';
const writeFileAsync = fs.promises.writeFile;
require('util.promisify').shim();
const writeFile = util.promisify(fs.writeFile);
import { Database, Table, DatabaseCollection } from '../../mol-data/db';
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
@@ -249,14 +250,14 @@ async function run(out: string, binary = false, options = DefaultDataOptions, cc
if (!fs.existsSync(path.dirname(out))) {
fs.mkdirSync(path.dirname(out));
}
writeFileAsync(out, ccbCif);
writeFile(out, ccbCif);
if (!!ccaOut) {
const ccaCif = getEncodedCif(CCA_TABLE_NAME, atoms, binary);
if (!fs.existsSync(path.dirname(ccaOut))) {
fs.mkdirSync(path.dirname(ccaOut));
}
writeFileAsync(ccaOut, ccaCif);
writeFile(ccaOut, ccaCif);
}
}

View File

@@ -1,15 +1,17 @@
/**
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as util from 'util';
import * as path from 'path';
import * as fs from 'fs';
import * as zlib from 'zlib';
const readFileAsync = fs.promises.readFile;
const writeFileAsync = fs.promises.writeFile;
import fetch from 'node-fetch';
require('util.promisify').shim();
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
import { Progress } from '../../mol-task';
import { Database } from '../../mol-data/db';
@@ -25,9 +27,9 @@ export async function ensureAvailable(path: string, url: string, forceDownload =
fs.mkdirSync(DATA_DIR);
}
if (url.endsWith('.gz')) {
await writeFileAsync(path, zlib.gunzipSync(await data.arrayBuffer()));
await writeFile(path, zlib.gunzipSync(await data.buffer()));
} else {
await writeFileAsync(path, await data.text());
await writeFile(path, await data.text());
}
console.log(`done downloading ${url}`);
}
@@ -39,7 +41,7 @@ export async function ensureDataAvailable(options: DataOptions) {
}
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
const parsed = await parseCif(await readFileAsync(path, 'utf8'));
const parsed = await parseCif(await readFile(path, 'utf8'));
return CIF.toDatabaseCollection(schema, parsed.result);
}

View File

@@ -1,9 +1,8 @@
/**
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import { CIF, CifCategory, getCifFieldType, CifField, CifFile } from '../../mol-io/reader/cif';
@@ -23,14 +22,14 @@ function showProgress(p: Progress) {
process.stdout.write(`\r${Progress.format(p)}`);
}
const readFileAsync = fs.promises.readFile;
const readFileAsync = util.promisify(fs.readFile);
const unzipAsync = util.promisify<zlib.InputType, Buffer>(zlib.unzip);
async function readFile(ctx: RuntimeContext, filename: string): Promise<ReaderResult<CifFile>> {
const isGz = /\.gz$/i.test(filename);
if (filename.match(/\.bcif/)) {
let input = await readFileAsync(filename);
if (isGz) input = await unzipAsync(input) as NonSharedBuffer;
if (isGz) input = await unzipAsync(input);
return await CIF.parseBinary(new Uint8Array(input)).runInContext(ctx);
} else {
const data = isGz ? await unzipAsync(await readFileAsync(filename)) : await readFileAsync(filename);

View File

@@ -12,6 +12,7 @@ import * as fs from 'fs';
import * as zlib from 'zlib';
import { convert } from './converter';
require('util.promisify').shim();
async function process(srcPath: string, outPath: string, configPath?: string, filterPath?: string) {
const config = configPath ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : void 0;

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node
/**
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as argparse from 'argparse';
import * as fs from 'fs';
import * as path from 'path';
import fetch from 'node-fetch';
import { parseCsv } from '../../mol-io/reader/csv/parser';
import { CifFrame, CifBlock } from '../../mol-io/reader/cif';
@@ -166,9 +166,9 @@ const MA_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist
const CIF_CORE_DIC_PATH = `${DIC_DIR}/cif_core.dic`;
const CIF_CORE_DIC_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/cif_core.dic';
const CIF_CORE_ENUM_PATH = `${DIC_DIR}/templ_enum.cif`;
const CIF_CORE_ENUM_URL = 'https://raw.githubusercontent.com/COMCIFS/Enumeration_Templates/refs/heads/main/templ_enum.cif';
const CIF_CORE_ENUM_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_enum.cif';
const CIF_CORE_ATTR_PATH = `${DIC_DIR}/templ_attr.cif`;
const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/Attribute_Templates/refs/heads/main/templ_attr.cif';
const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_attr.cif';
const parser = new argparse.ArgumentParser({
add_help: true,

View File

@@ -93,7 +93,6 @@ export function getFieldType(type: string, description: string, values?: string[
case 'Implied':
case 'Word':
case 'Uri':
case 'Iri':
return wrapContainer('str', ',', description, container);
case 'Real':
return wrapContainer('float', ',', description, container);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -65,9 +65,7 @@ function getTypeDef(c: Column): string {
case 'float': return 'float';
case 'coord': return 'coord';
case 'enum':
return c.subType === 'int'
? `Aliased<${c.values.join(' | ')}>(${c.subType})`
: `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
return `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
case 'matrix':
return `Matrix(${c.rows}, ${c.columns})`;
case 'vector':

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node
/**
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as argparse from 'argparse';
import * as fs from 'fs';
import * as path from 'path';
import fetch from 'node-fetch';
import { UniqueArray } from '../../mol-data/generic';
const LIPIDS_DIR = path.resolve(__dirname, '../../../../build/lipids/');
@@ -33,14 +33,6 @@ async function ensureLipidsAvailable() { await ensureAvailable(MARTINI_LIPIDS_PA
const extraLipids = ['DMPC'];
const v2lipids = ['DAPC', 'DBPC', 'DFPC', 'DGPC', 'DIPC', 'DLPC', 'DNPC', 'DOPC', 'DPPC', 'DRPC', 'DTPC', 'DVPC', 'DXPC', 'DYPC', 'LPPC', 'PAPC', 'PEPC', 'PGPC', 'PIPC', 'POPC', 'PRPC', 'PUPC', 'DAPE', 'DBPE', 'DFPE', 'DGPE', 'DIPE', 'DLPE', 'DNPE', 'DOPE', 'DPPE', 'DRPE', 'DTPE', 'DUPE', 'DVPE', 'DXPE', 'DYPE', 'LPPE', 'PAPE', 'PGPE', 'PIPE', 'POPE', 'PQPE', 'PRPE', 'PUPE', 'DAPS', 'DBPS', 'DFPS', 'DGPS', 'DIPS', 'DLPS', 'DNPS', 'DOPS', 'DPPS', 'DRPS', 'DTPS', 'DUPS', 'DVPS', 'DXPS', 'DYPS', 'LPPS', 'PAPS', 'PGPS', 'PIPS', 'POPS', 'PQPS', 'PRPS', 'PUPS', 'DAPG', 'DBPG', 'DFPG', 'DGPG', 'DIPG', 'DLPG', 'DNPG', 'DOPG', 'DPPG', 'DRPG', 'DTPG', 'DVPG', 'DXPG', 'DYPG', 'LPPG', 'PAPG', 'PGPG', 'PIPG', 'POPG', 'PRPG', 'DAPA', 'DBPA', 'DFPA', 'DGPA', 'DIPA', 'DLPA', 'DNPA', 'DOPA', 'DPPA', 'DRPA', 'DTPA', 'DVPA', 'DXPA', 'DYPA', 'LPPA', 'PAPA', 'PGPA', 'PIPA', 'POPA', 'PRPA', 'PUPA', 'DPP', 'DPPI', 'PAPI', 'PIPI', 'POP', 'POPI', 'PUPI', 'PVP', 'PVPI', 'PADG', 'PIDG', 'PODG', 'PUDG', 'PVDG', 'APC', 'CPC', 'IPC', 'LPC', 'OPC', 'PPC', 'TPC', 'UPC', 'VPC', 'BNSM', 'DBSM', 'DPSM', 'DXSM', 'PGSM', 'PNSM', 'POSM', 'PVSM', 'XNSM', 'DPCE', 'DXCE', 'PNCE', 'XNCE'];
const amberLipids = [
// acyl chains
'PA', 'ST', 'OL', 'LEO', 'LEN', 'AR', 'DHA',
// head groups
'PC', 'PE', 'PS', 'PH-', 'P2-', 'PGR', 'PGS', 'PI',
// other
'CHL'
];
async function run(out: string) {
await ensureLipidsAvailable();
@@ -63,17 +55,13 @@ async function run(out: string) {
UniqueArray.add(lipids, v, v);
}
for (const v of amberLipids) {
UniqueArray.add(lipids, v, v);
}
const lipidNames = JSON.stringify(lipids.array);
if (out) {
const output = `/**
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated lipid params file. Names from Martini FF and Amber.
* Code-generated lipid params file. Names extracted from Martini FF lipids itp.
*
* @author molstar/lipid-params cli
*/

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env node
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*
* Command-line application for converting MolViewSpec MVSJ into MSVX files
* Build: npm run build
* Run: node lib/commonjs/cli/mvs/mvs-mvsj-to-mvsx -i examples/mvs/1cbs.mvsj -o tmp/1cbs.mvsx
*/
import { ArgumentParser } from 'argparse';
import fs from 'fs';
import { MVSData } from '../../extensions/mvs/mvs-data';
import { setFSModule } from '../../mol-util/data-source';
setFSModule(fs);
/** Command line argument values for `main` */
interface Args {
input: string[],
output: string[] | undefined,
base_uri: string | undefined,
skip_external: boolean,
}
/** Return parsed command line arguments for `main` */
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Command-line application for converting MolViewSpec MVSJ into MSVX files' });
parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj format.' });
parser.add_argument('-o', '--output', { required: false, nargs: '+', help: 'File path(s) for output files in .mvsx format (one output path for each input file). If ommitted, filenames will be created automatically by replacing file extension.' });
parser.add_argument('--base-uri', { help: 'Base URI/path used to resolve relative URIs in the input file (default: path of the input file itself). Use `--base-uri .` for using the current working directory as base URI.' });
parser.add_argument('--skip-external', { action: 'store_true', help: 'Do not include external resources (i.e. absolute URIs) in the MVSX.' });
const args: Args = parser.parse_args();
if (args.output && args.output.length !== args.input.length) {
parser.error(`argument: --output: must specify the same number of input and output file paths (specified ${args.input.length} input path${args.input.length !== 1 ? 's' : ''} but ${args.output.length} output path${args.output.length !== 1 ? 's' : ''})`);
}
return { ...args };
}
/** Main workflow for converting MVSJ to MVSX files. */
async function main(args: Args): Promise<void> {
const cache = {};
for (let i = 0; i < args.input.length; i++) {
const input = args.input[i];
const output = args.output?.[i] ?? input.replace(/(\.mvsj)?$/i, '.mvsx');
console.log(`Processing ${input} -> ${output}`);
const mvsj = fs.readFileSync(input, { encoding: 'utf8' });
const mvsData = MVSData.fromMVSJ(mvsj);
const mvsx = await MVSData.toMVSX(mvsData, {
baseUri: args.base_uri ?? input,
skipExternal: args.skip_external,
cache,
});
fs.writeFileSync(output, mvsx);
}
}
main(parseArguments());

View File

@@ -11,7 +11,7 @@
*/
import { ArgumentParser } from 'argparse';
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-validation';
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';

View File

@@ -1,16 +1,18 @@
/**
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as util from 'util';
import * as fs from 'fs';
import fetch from 'node-fetch';
require('util.promisify').shim();
import { CIF } from '../../mol-io/reader/cif';
import { Progress } from '../../mol-task';
const readFileAsync = fs.promises.readFile;
const readFileAsync = util.promisify(fs.readFile);
async function readFile(path: string) {
if (path.match(/\.bcif$/)) {

View File

@@ -7,6 +7,7 @@
*/
import * as argparse from 'argparse';
require('util.promisify').shim();
import { CifFrame } from '../../mol-io/reader/cif';
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing, Trajectory } from '../../mol-model/structure';

View File

@@ -1,14 +1,13 @@
#!/usr/bin/env node
/**
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018 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 Paul Pillot <paul.pillot@tandemai.com>
*/
import * as fs from 'fs';
import * as argparse from 'argparse';
import * as util from 'util';
import { Volume } from '../../mol-model/volume';
import { downloadCif } from './helpers';
@@ -20,7 +19,8 @@ import { createVolumeIsosurfaceMesh } from '../../mol-repr/volume/isosurface';
import { Theme } from '../../mol-theme/theme';
import { volumeFromDensityServerData, DscifFormat } from '../../mol-model-formats/volume/density-server';
const writeFileAsync = fs.promises.writeFile;
require('util.promisify').shim();
const writeFileAsync = util.promisify(fs.writeFile);
async function getVolume(url: string): Promise<Volume> {
const cif = await downloadCif(url, true);
@@ -38,7 +38,7 @@ function print(volume: Volume) {
}
async function doMesh(volume: Volume, filename: string) {
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5), wrap: 'auto', floodfill: 'off' })).run();
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
// Export the mesh in OBJ format.

View File

@@ -21,10 +21,8 @@ import { StripedResidues } from './coloring';
import { CustomToastMessage } from './controls';
import { CustomColorThemeProvider } from './custom-theme';
import './index.html';
import './tm-align.html';
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData, tmAlignStructures, loadStructuresNoAlignment, sequenceAlignStructures } from './superposition';
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData } from './superposition';
import '../../mol-plugin-ui/skin/light.scss';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
@@ -96,7 +94,7 @@ class BasicWrapper {
...trackball,
animate: trackball.animate.name === 'spin'
? { name: 'off', params: {} }
: { name: 'spin', params: { speed: 0.1, axis: Vec3.create(0, -1, 0) } }
: { name: 'spin', params: { speed: 1 } }
}
}
});
@@ -192,45 +190,6 @@ 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,9 +5,8 @@
*/
import { Mat4 } from '../../mol-math/linear-algebra';
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 { QueryContext, StructureSelection } from '../../mol-model/structure';
import { superpose } from '../../mol-model/structure/structure/util/superposition';
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
import { PluginContext } from '../../mol-plugin/context';
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
@@ -117,217 +116,4 @@ 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

@@ -1,39 +0,0 @@
<!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

@@ -1,11 +1,11 @@
/**
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import express from 'express';
import fetch from 'node-fetch';
import { createMapping } from './mapping';
async function getMappings(id: string) {

View File

@@ -1,10 +1,10 @@
/**
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import fetch from 'node-fetch';
import { createMapping } from './mapping';
(async function () {

View File

@@ -111,28 +111,12 @@ A story showcasing MolViewSpec animation capabilities.
const builder = createMVSBuilder();
const _1cbs = structure(builder, '1cbs');
const [poly, repr] = polymer(_1cbs, { color: Colors['1cbs'] });
repr.colorFromSource({
ref: 'residue_colors',
schema: 'residue',
category_name: 'atom_site',
field_name: 'label_comp_id',
palette: {
kind: 'categorical',
missing_color: 'white',
colors: {
ALA: 'red',
ILE: 'white',
LYS: 'white',
}
}
});
const [poly,] = polymer(_1cbs, { color: Colors['1cbs'] });
const surface = poly.representation({
type: 'surface',
surface_type: 'gaussian',
}).opacity({ opacity: 0.33 });
});
_1cbs.component({ selector: 'ligand' })
.transform({
@@ -206,20 +190,6 @@ A story showcasing MolViewSpec animation capabilities.
end: Colors['ligand-docked'],
});
anim.interpolate({
kind: 'color',
target_ref: 'residue_colors',
duration_ms: 2000,
property: ['palette', 'colors'],
start: {
ALA: 'yellow',
},
end: {
ILE: 'blue',
LYS: 'purple',
},
});
return builder;
},
camera: {
@@ -341,12 +311,10 @@ function structure(builder: Root, id: string): MVSStructure {
.modelStructure();
}
function polymer(structure: MVSStructure, options?: { color?: ColorT }) {
function polymer(structure: MVSStructure, options: { color: ColorT }) {
const component = structure.component({ selector: { label_asym_id: 'A' } });
const reprensentation = component.representation({ type: 'cartoon' });
if (options?.color) {
reprensentation.color({ color: options.color });
}
reprensentation.color({ color: options.color });
return [component, reprensentation] as const;
}
@@ -364,21 +332,6 @@ export function buildStory(): MVSData_States {
molstar_postprocessing: {
enable_outline: true,
enable_ssao: true,
background: {
name: 'horizontalGradient',
params: {
topColor: 0x777777,
bottomColor: 0xffffff,
}
},
// Example with background image:
// background: {
// name: 'image',
// params: {
// // URL can also be filename in MVSX archive
// source: { name: 'url', params: 'URL' }
// }
// }
}
}
});

View File

@@ -13,7 +13,7 @@ import { buildStory as motm1 } from './motm1';
export const Stories = [
{ id: 'kinase', name: 'BCR-ABL: A Kinase Out of Control', buildStory: kinase },
{ id: 'tata', name: 'TATA-Binding Protein and its Role in Transcription Initiation ', buildStory: tbp },
{ id: 'motm1', name: 'RCSB PDB Molecule of the Month #1', buildStory: motm1 },
{ id: 'motm1', name: 'RCSB Molecule of the Month #1', buildStory: motm1 },
{ id: 'animation-example', name: 'Molecular Animation Example', buildStory: animation },
{ id: 'audio-example', name: 'Audio Playback Example', buildStory: audio },
] as const;

View File

@@ -962,7 +962,7 @@ export function buildStory(): MVSData_States {
kind: 'multiple',
snapshots,
metadata: {
title: 'RCSB PDB Molecule of the Month 1',
title: 'RCSB Molecule of the Month 1',
version: '1.0',
timestamp: new Date().toISOString(),
}

View File

@@ -30,7 +30,6 @@ import { createProteopediaCustomTheme } from './coloring';
import { LoadParams, ModelInfo, RepresentationStyle, StateElements, SupportedFormats } from './helpers';
import './index.html';
import { volumeStreamingControls } from './ui/controls';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
require('../../mol-plugin-ui/skin/light.scss');
class MolStarProteopediaWrapper {
@@ -268,7 +267,7 @@ class MolStarProteopediaWrapper {
...trackball,
animate: trackball.animate.name === 'spin'
? { name: 'off', params: {} }
: { name: 'spin', params: { speed: 0.1, axis: Vec3.create(0, -1, 0) } }
: { name: 'spin', params: { speed: 1 } }
}
}
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2026 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 David Sehnal <david.sehnal@gmail.com>
*/
@@ -118,7 +118,6 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
sourceData: CubeGridFormat(data),
customProperties: new CustomProperties(),
_propertyData: Object.create(null),
_localPropertyData: Object.create(null)
};
if (params.clampValues?.name === 'on') {
@@ -152,7 +151,6 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
sourceData: CubeGridFormat(data),
customProperties: new CustomProperties(),
_propertyData: Object.create(null),
_localPropertyData: Object.create(null)
};
if (params.clampValues?.name === 'on') {

View File

@@ -1,403 +0,0 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Color } from '../../mol-util/color';
import { ColorNames } from '../../mol-util/color/names';
import { Clip } from '../../mol-util/clip';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { Box } from '../../mol-geo/primitive/box';
import { Plane } from '../../mol-geo/primitive/plane';
import { Cylinder } from '../../mol-geo/primitive/cylinder';
import { Sphere } from '../../mol-geo/primitive/sphere';
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
export const ClipObjectHelperParams = {
clipObjects: PD.Boolean(false, { description: 'Show clip-objects of visible render objects.' }),
};
export type ClipObjectHelperParams = typeof ClipObjectHelperParams;
export type ClipObjectHelperProps = PD.Values<ClipObjectHelperParams>;
//
/** Serializes clip object params to a string key for deduplication */
function clipObjectKey(type: number, invert: boolean, position: ArrayLike<number>, posOffset: number, rotation: ArrayLike<number>, rotOffset: number, scale: ArrayLike<number>, scaleOffset: number, transform: ArrayLike<number>, transformOffset: number): string {
// Round floats to 5 decimal places to avoid floating point noise
const r = (v: number) => Math.round(v * 100000) / 100000;
const parts = [
type, invert ? 1 : 0,
r(position[posOffset]), r(position[posOffset + 1]), r(position[posOffset + 2]),
r(rotation[rotOffset]), r(rotation[rotOffset + 1]), r(rotation[rotOffset + 2]), r(rotation[rotOffset + 3]),
r(scale[scaleOffset]), r(scale[scaleOffset + 1]), r(scale[scaleOffset + 2]),
];
for (let j = 0; j < 16; ++j) {
parts.push(r(transform[transformOffset + j]));
}
return parts.join(',');
}
type ClipObjectData = {
key: string,
renderObject: GraphicsRenderObject,
indicatorRenderObject: GraphicsRenderObject,
mesh: Mesh,
}
const clipObjectColors: Record<number, Color> = {
[Clip.Type.plane]: ColorNames.orange,
[Clip.Type.sphere]: ColorNames.green,
[Clip.Type.cube]: ColorNames.dodgerblue,
[Clip.Type.cylinder]: ColorNames.gold,
[Clip.Type.infiniteCone]: ColorNames.crimson,
};
const clipMaterialId = getNextMaterialId();
const indicatorMaterialId = getNextMaterialId();
// Pre-rotation matrices for aligning primitives to GLSL SDF local frames
// Plane: Rx(-90°) maps primitive Z-normal to GLSL Y-normal
const preRotPlaneQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), -Math.PI / 2);
const preRotPlaneMat = Mat4.fromQuat(Mat4(), preRotPlaneQuat);
// Cone: Rx(+90°) maps primitive Y-axis to GLSL Z-axis
const preRotConeQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), Math.PI / 2);
const preRotConeMat = Mat4.fromQuat(Mat4(), preRotConeQuat);
// Temp variables for constructing transforms
const _position = Vec3();
const _rotation = Quat();
const _scale = Vec3();
const _clipTransform = Mat4();
const _invClipTransform = Mat4();
const _rotMat = Mat4();
const _translateMat = Mat4();
const _baseMat = Mat4();
const _tmpMat = Mat4();
const _axisEnd = Vec3();
const _yAxis = Vec3.create(0, 1, 0);
const _zAxis = Vec3.create(0, 0, 1);
const _indicatorPos = Vec3();
export class ClipObjectHelper implements DebugHelper<ClipObjectHelperProps> {
readonly scene: Scene;
private readonly parent: Scene;
private _props: ClipObjectHelperProps;
private objectsData = new Map<string, ClipObjectData>();
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ClipObjectHelperProps>) {
this.scene = Scene.create(ctx, 'blended');
this.parent = parent;
this._props = { ...PD.getDefaultValues(ClipObjectHelperParams), ...props };
}
update() {
const currentKeys = new Set<string>();
const sceneRadius = this.parent.boundingSphereVisible.radius || 50;
this.parent.forEach((r, ro) => {
if (!ro.state.visible) return;
const count = ro.values.dClipObjectCount.ref.value;
if (count === 0) return;
const types = ro.values.uClipObjectType.ref.value;
const inverts = ro.values.uClipObjectInvert.ref.value;
const positions = ro.values.uClipObjectPosition.ref.value;
const rotations = ro.values.uClipObjectRotation.ref.value;
const scales = ro.values.uClipObjectScale.ref.value;
const transforms = ro.values.uClipObjectTransform.ref.value;
for (let i = 0; i < count; ++i) {
const type = types[i];
if (type === Clip.Type.none) continue;
const key = clipObjectKey(
type, inverts[i],
positions, i * 3,
rotations, i * 4,
scales, i * 3,
transforms, i * 16
);
currentKeys.add(key);
if (this.objectsData.has(key)) continue;
// Extract per-object params
Vec3.fromArray(_position, positions, i * 3);
Quat.fromArray(_rotation, rotations, i * 4);
Quat.normalize(_rotation, _rotation); // ensure unit quaternion for proper rotation
Vec3.fromArray(_scale, scales, i * 3);
Mat4.fromArray(_clipTransform, transforms, i * 16);
// Build base transform (translate * rotate) without scale,
// so each shape can insert pre-rotations before scale.
Mat4.fromQuat(_rotMat, _rotation);
Mat4.fromTranslation(_translateMat, _position);
Mat4.mul(_baseMat, _translateMat, _rotMat);
// apply inverse of clip transform
if (!Mat4.isIdentity(_clipTransform)) {
Mat4.invert(_invClipTransform, _clipTransform);
Mat4.mul(_baseMat, _invClipTransform, _baseMat);
}
const mesh = createClipObjectMesh(type, _baseMat, _scale, sceneRadius);
const color = clipObjectColors[type] || ColorNames.white;
const renderObject = createClipObjectRenderObject(mesh, color, clipMaterialId, type);
// Create position/rotation indicator mesh
const invert = inverts[i];
const indicatorMesh = createIndicatorMesh(_position, _rotation, _clipTransform, _scale, type, invert);
const indicatorRenderObject = createIndicatorRenderObject(indicatorMesh, indicatorMaterialId);
this.scene.add(renderObject);
this.scene.add(indicatorRenderObject);
this.objectsData.set(key, { key, renderObject, indicatorRenderObject, mesh });
}
});
// Remove clip objects no longer present
this.objectsData.forEach((data, key) => {
if (!currentKeys.has(key)) {
this.scene.remove(data.renderObject);
this.scene.remove(data.indicatorRenderObject);
this.objectsData.delete(key);
}
});
this.scene.update(void 0, false);
this.scene.commit();
}
syncVisibility() {
const visible = this._props.clipObjects;
this.objectsData.forEach(data => {
data.renderObject.state.visible = visible;
data.indicatorRenderObject.state.visible = visible;
});
}
clear() {
this.objectsData.clear();
this.scene.clear();
}
get isEnabled() {
return this._props.clipObjects;
}
get props() { return this._props as Readonly<ClipObjectHelperProps>; }
setProps(props: Partial<ClipObjectHelperProps>) {
Object.assign(this._props, props);
if (this.isEnabled) this.update();
}
}
//
function createClipObjectMesh(type: number, baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
switch (type) {
case Clip.Type.plane: return createPlaneMesh(baseMat, sceneRadius);
case Clip.Type.sphere: return createSphereMesh(baseMat, scale);
case Clip.Type.cube: return createCubeMesh(baseMat, scale);
case Clip.Type.cylinder: return createCylinderMesh(baseMat, scale);
case Clip.Type.infiniteCone: return createConeMesh(baseMat, scale, sceneRadius);
default: return createSphereMesh(baseMat, scale); // fallback
}
}
/**
* Plane: GLSL normal is quaternionTransform(rotation, vec3(0,1,0)) — Y-up default.
* Plane() primitive lies in XY with normal (0,0,1) along Z.
* Pre-rotate Rx(-90°) to align primitive Z-normal to GLSL Y-normal.
* Sized to cover the scene bounding sphere. Clip scale is ignored (plane is infinite in GLSL).
*/
function createPlaneMesh(baseMat: Mat4, sceneRadius: number): Mesh {
const size = Math.max(sceneRadius * 2, 10);
// baseMat * preRotPlane * uniformScale(size)
Mat4.mul(_tmpMat, baseMat, preRotPlaneMat);
Mat4.scale(_tmpMat, _tmpMat, Vec3.create(size, size, 1));
const plane = Plane();
const builderState = MeshBuilder.createState(256, 128);
MeshBuilder.addPrimitive(builderState, _tmpMat, plane);
// Add flipped backface for double-sided visibility
MeshBuilder.addPrimitiveFlipped(builderState, _tmpMat, plane);
return MeshBuilder.getMesh(builderState);
}
/**
* Sphere: SDF uses scale * 0.5 as the radii (ellipsoid).
* Sphere primitive has radius 1.
* Transform: baseMat * scale * 0.5
*/
function createSphereMesh(baseMat: Mat4, scale: Vec3): Mesh {
const detail = 2;
const sphere = getSphereForHelper(detail);
// baseMat * scale(scale * 0.5)
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1] * 0.5, scale[2] * 0.5));
const vertexCount = 10 * Math.pow(2, 2 * detail) + 2;
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
MeshBuilder.addPrimitive(builderState, _tmpMat, sphere);
return MeshBuilder.getMesh(builderState);
}
let _helperSphere: ReturnType<typeof Sphere> | undefined;
function getSphereForHelper(detail: number) {
if (!_helperSphere) _helperSphere = Sphere(detail);
return _helperSphere;
}
/**
* Cube: SDF uses scale * 0.5 as half-extents.
* Box() primitive is ±0.5 (unit cube), so scaling by `scale` gives half-extents of scale*0.5.
*/
function createCubeMesh(baseMat: Mat4, scale: Vec3): Mesh {
// baseMat * scale(scale)
Mat4.scale(_tmpMat, baseMat, scale);
const box = Box();
const builderState = MeshBuilder.createState(256, 128);
MeshBuilder.addPrimitive(builderState, _tmpMat, box);
return MeshBuilder.getMesh(builderState);
}
/**
* Cylinder: SDF axis along Y, radius = scale.x * 0.5, half-height = scale.y * 0.5.
* Cylinder primitive: axis along Y, radius=1 in XZ, half-height=0.5 in Y.
* Need: X/Z *= scale.x * 0.5 (radius 1 → scale.x*0.5), Y *= scale.y (half-height 0.5 → scale.y*0.5).
*/
function createCylinderMesh(baseMat: Mat4, scale: Vec3): Mesh {
const cyl = Cylinder({ radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: true, bottomCap: true });
// baseMat * scale(scale.x * 0.5, scale.y, scale.x * 0.5) — use scale.x for both radial axes
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1], scale[0] * 0.5));
const vertexCount = cyl.vertices.length / 3;
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
MeshBuilder.addPrimitive(builderState, _tmpMat, cyl);
return MeshBuilder.getMesh(builderState);
}
/**
* InfiniteCone: GLSL SDF axis along Z, radial in XY.
* surface: size.x * length(t.xy) + size.y * t.z = 0 (size = scale * 0.5)
* half-angle: tan(θ) = scale.y / scale.x
* apex at clip position (origin), opens in -Z direction.
*
* Cylinder primitive (radiusTop=0, radiusBottom=1, height=1):
* axis along Y, tip at Y=+0.5, base at Y=-0.5, base radius=1.
*
* Transform chain (right-to-left):
* 1. Scale(baseRadius, coneLength, baseRadius): stretch primitive to correct proportions
* 2. Translate(0, -0.5*coneLength, 0): move tip from Y=+0.5·cL to Y=0 (apex at origin)
* (after scale, tip is at Y=+0.5·cL; shifting by -0.5·cL puts it at Y=0)
* 3. preRotCone Rx(+90°): map prim-Y→Z, so cone axis becomes Z, opening in -Z
* 4. baseMat: position + rotation of clip object
*/
function createConeMesh(baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
const cone = Cylinder({ radiusTop: 0, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: false, bottomCap: true });
// Visible length of the (infinite) cone, and base radius matching the GLSL half-angle
const coneLength = Math.max(sceneRadius * 2, 10);
const tanHalfAngle = (scale[1] || 1) / (scale[0] || 1); // tan(θ) = scaleY / scaleX
const baseRadius = coneLength * tanHalfAngle;
// baseMat * preRotCone * Translate(0, -coneLength/2, 0) * Scale(baseRadius, coneLength, baseRadius)
const scaleMat = Mat4.fromScaling(Mat4(), Vec3.create(baseRadius, coneLength, baseRadius));
const translateMat = Mat4.fromTranslation(Mat4(), Vec3.create(0, -coneLength * 0.5, 0));
Mat4.mul(_tmpMat, translateMat, scaleMat);
Mat4.mul(_tmpMat, preRotConeMat, _tmpMat);
Mat4.mul(_tmpMat, baseMat, _tmpMat);
const vertexCount = cone.vertices.length / 3;
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
MeshBuilder.addPrimitive(builderState, _tmpMat, cone);
return MeshBuilder.getMesh(builderState);
}
function createClipObjectRenderObject(mesh: Mesh, color: Color, materialId: number, type: number) {
const alpha = type === Clip.Type.plane ? 0.25 : 0.15;
const values = Mesh.Utils.createValuesSimple(mesh, { alpha, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1);
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
}
/**
* Create a mesh with a sphere at the clip object position and a cylinder
* along the characteristic axis to indicate orientation.
*
* - Plane/sphere/cube/cylinder: axis = rotated Y (matches GLSL normal/axis direction)
* - InfiniteCone: axis = rotated Z (cone axis is Z in local frame)
* - Plane with invert: direction is flipped
*/
function createIndicatorMesh(position: Vec3, rotation: Quat, clipTransform: Mat4, scale: Vec3, type: number, invert: boolean): Mesh {
const objectSize = Math.max(scale[0], scale[1], scale[2]);
const sphereRadius = Math.max(objectSize * 0.004, 0.01);
const cylinderRadius = sphereRadius * 0.4;
const axisLength = Math.max(objectSize * 0.1, 2);
// Transform position by inverse of clipTransform if non-identity
Vec3.copy(_indicatorPos, position);
if (!Mat4.isIdentity(clipTransform)) {
Mat4.invert(_invClipTransform, clipTransform);
Vec3.transformMat4(_indicatorPos, _indicatorPos, _invClipTransform);
}
// Choose the local-frame axis based on clip type
const localAxis = type === Clip.Type.infiniteCone ? _zAxis : _yAxis;
Vec3.transformQuat(_axisEnd, localAxis, rotation);
// Cone opens in -Z locally, so negate to point along the cone opening
if (type === Clip.Type.infiniteCone) {
Vec3.negate(_axisEnd, _axisEnd);
}
// For planes, the normal points toward the clipped (removed) side.
// Flip so the indicator points toward the non-clipped (kept) geometry.
// When inverted, the kept side is the normal side, so don't flip.
if (type === Clip.Type.plane && !invert) {
Vec3.negate(_axisEnd, _axisEnd);
}
// If clipTransform is non-identity, also transform the axis direction
if (!Mat4.isIdentity(clipTransform)) {
// Transform direction (not position) by inverse clipTransform
const endWorld = Vec3();
Vec3.add(endWorld, position, Vec3.scale(Vec3(), _axisEnd, axisLength));
Vec3.transformMat4(endWorld, endWorld, _invClipTransform);
Vec3.sub(_axisEnd, endWorld, _indicatorPos);
Vec3.normalize(_axisEnd, _axisEnd);
}
// Axis cylinder endpoint
const axisEndPoint = Vec3();
Vec3.scaleAndAdd(axisEndPoint, _indicatorPos, _axisEnd, axisLength);
const builderState = MeshBuilder.createState(512, 256);
// Position sphere
addSphere(builderState, _indicatorPos, sphereRadius, 1);
// Rotation axis cylinder
addCylinder(builderState, _indicatorPos, axisEndPoint, 1, { radiusTop: cylinderRadius, radiusBottom: cylinderRadius, radialSegments: 8 });
// Small sphere at tip of axis
addSphere(builderState, axisEndPoint, cylinderRadius * 1.5, 1);
return MeshBuilder.getMesh(builderState);
}
function createIndicatorRenderObject(mesh: Mesh, materialId: number) {
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.7, doubleSided: false, cellSize: 0, batchSize: 0 }, ColorNames.white, 1);
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
}

View File

@@ -1,145 +0,0 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { ColorNames } from '../../mol-util/color/names';
import { Lines } from '../../mol-geo/geometry/lines/lines';
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { DirectVolumeValues } from '../../mol-gl/renderable/direct-volume';
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
export const DirectVolumeHelperParams = {
directVolumeEdges: PD.Boolean(false, { description: 'Show edges of visible direct-volume render objects.' }),
};
export type DirectVolumeHelperParams = typeof DirectVolumeHelperParams;
export type DirectVolumeHelperProps = PD.Values<DirectVolumeHelperParams>;
const directVolumeMaterialId = getNextMaterialId();
type TrackedEntry = { ro: GraphicsRenderObject, version: number };
export class DirectVolumeHelper implements DebugHelper<DirectVolumeHelperProps> {
readonly scene: Scene;
private readonly parent: Scene;
private _props: DirectVolumeHelperProps;
private renderObjects = new Map<number, TrackedEntry>();
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DirectVolumeHelperProps>) {
this.scene = Scene.create(ctx, 'blended');
this.parent = parent;
this._props = { ...PD.getDefaultValues(DirectVolumeHelperParams), ...props };
}
update() {
const previousIds = new Set(this.renderObjects.keys());
this.parent.forEach((r, ro) => {
if (!ro.state.visible) return;
if (ro.type !== 'direct-volume') return;
const values = ro.values as DirectVolumeValues;
const version = values.uUnitToCartn.ref.version + values.uGridDim.ref.version + values.aTransform.ref.version;
const existing = this.renderObjects.get(ro.id);
if (existing && existing.version === version) {
previousIds.delete(ro.id);
return;
}
// Remove old entry if version changed
if (existing) {
this.scene.remove(existing.ro);
this.renderObjects.delete(ro.id);
}
const lines = createVolumeEdgeLines(values);
if (!lines) return;
const linesRO = createLinesRenderObject(lines, directVolumeMaterialId);
this.scene.add(linesRO);
this.renderObjects.set(ro.id, { ro: linesRO, version });
previousIds.delete(ro.id);
});
for (const id of previousIds) {
const entry = this.renderObjects.get(id);
if (entry) {
this.scene.remove(entry.ro);
this.renderObjects.delete(id);
}
}
this.scene.update(void 0, false);
this.scene.commit();
}
syncVisibility() {
const visible = this._props.directVolumeEdges;
this.renderObjects.forEach(entry => {
entry.ro.state.visible = visible;
});
}
clear() {
this.renderObjects.clear();
this.scene.clear();
}
get isEnabled() {
return this._props.directVolumeEdges;
}
get props() { return this._props as Readonly<DirectVolumeHelperProps>; }
setProps(props: Partial<DirectVolumeHelperProps>) {
Object.assign(this._props, props);
if (this.isEnabled) this.update();
}
}
//
/**
* The volume proxy box in the shader uses aPosition in [-0.5, 0.5]^3,
* shifted to [0,1]^3 (unitCoord = aPosition + 0.5), then transformed by:
* uUnitToCartn → Cartesian space
* aTransform → instance space
*
* We replicate this pipeline to get the correct world-space edges.
* Grid ticks are placed at 1/gridDim intervals along each edge.
*/
function createVolumeEdgeLines(values: DirectVolumeValues): Lines | undefined {
const unitToCartn = values.uUnitToCartn.ref.value;
const transforms = values.aTransform.ref.value;
const instanceCount = values.uInstanceCount.ref.value;
const bs = values.boundingSphere.ref.value;
if (bs.radius < 1e-6) return undefined;
const builder = LinesBuilder.create(128 * instanceCount);
for (let inst = 0; inst < instanceCount; ++inst) {
const instTransform = Mat4();
Mat4.fromArray(instTransform, transforms, inst * 16);
// Combined transform: aTransform * uUnitToCartn
const combined = Mat4.mul(Mat4(), instTransform, unitToCartn);
addBox(builder, combined, 0);
}
return builder.getLines();
}
function createLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.8 };
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.orange, 1);
const state = Lines.Utils.createRenderableState(props);
state.pickable = false;
return createRenderObject('lines', values, state, materialId);
}

View File

@@ -1,266 +0,0 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { ColorNames } from '../../mol-util/color/names';
import { Color } from '../../mol-util/color';
import { Lines } from '../../mol-geo/geometry/lines/lines';
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
import { ImageValues } from '../../mol-gl/renderable/image';
import { Clip } from '../../mol-util/clip';
import { addSphere as addLinesSphere } from '../../mol-geo/geometry/lines/builder/sphere';
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
import { addPlane } from '../../mol-geo/geometry/lines/builder/plane';
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
export const ImageHelperParams = {
imageEdges: PD.Boolean(false, { description: 'Show edges of visible image render objects.' }),
};
export type ImageHelperParams = typeof ImageHelperParams;
export type ImageHelperProps = PD.Values<ImageHelperParams>;
const imageEdgeMaterialId = getNextMaterialId();
const imageTrimMaterialId = getNextMaterialId();
// Temp vectors
const _trimPos = Vec3();
const _trimScale = Vec3();
const _trimRot = Quat();
const _trimTransform = Mat4();
const _tmpMat = Mat4();
export class ImageHelper implements DebugHelper<ImageHelperProps> {
readonly scene: Scene;
private readonly parent: Scene;
private _props: ImageHelperProps;
private renderObjects = new Map<number, { roList: GraphicsRenderObject[], version: number }>();
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ImageHelperProps>) {
this.scene = Scene.create(ctx, 'blended');
this.parent = parent;
this._props = { ...PD.getDefaultValues(ImageHelperParams), ...props };
}
update() {
const previousIds = new Set(this.renderObjects.keys());
this.parent.forEach((r, ro) => {
if (!ro.state.visible) return;
if (ro.type !== 'image') return;
const values = ro.values as ImageValues;
const version = values.aPosition.ref.version
+ values.uTrimType.ref.version + values.uTrimCenter.ref.version
+ values.uTrimRotation.ref.version + values.uTrimScale.ref.version
+ values.uTrimTransform.ref.version + values.aTransform.ref.version;
const existing = this.renderObjects.get(ro.id);
if (existing && existing.version === version) {
previousIds.delete(ro.id);
return;
}
// Remove old entries if version changed
if (existing) {
for (const oldRO of existing.roList) this.scene.remove(oldRO);
this.renderObjects.delete(ro.id);
}
const roList: GraphicsRenderObject[] = [];
const edgeLines = createImageEdgeLines(values);
if (edgeLines) {
const edgeRO = createLinesRenderObject(edgeLines, imageEdgeMaterialId, ColorNames.cyan, 0.8);
this.scene.add(edgeRO);
roList.push(edgeRO);
}
const trimLines = createTrimEdgeLines(values);
if (trimLines) {
const trimRO = createLinesRenderObject(trimLines, imageTrimMaterialId, ColorNames.yellow, 0.7);
this.scene.add(trimRO);
roList.push(trimRO);
}
if (roList.length > 0) {
this.renderObjects.set(ro.id, { roList, version });
}
previousIds.delete(ro.id);
});
for (const id of previousIds) {
const entry = this.renderObjects.get(id);
if (entry) {
for (const ro of entry.roList) this.scene.remove(ro);
this.renderObjects.delete(id);
}
}
this.scene.update(void 0, false);
this.scene.commit();
}
syncVisibility() {
const visible = this._props.imageEdges;
this.renderObjects.forEach(entry => {
for (const ro of entry.roList) ro.state.visible = visible;
});
}
clear() {
this.renderObjects.clear();
this.scene.clear();
}
get isEnabled() {
return this._props.imageEdges;
}
get props() { return this._props as Readonly<ImageHelperProps>; }
setProps(props: Partial<ImageHelperProps>) {
Object.assign(this._props, props);
if (this.isEnabled) this.update();
}
}
//
/**
* Image quad vertex layout (from image.ts):
* Vertex 0: UV (0,1) — top-left
* Vertex 1: UV (0,0) — bottom-left
* Vertex 2: UV (1,1) — top-right
* Vertex 3: UV (1,0) — bottom-right
*
* addPlane expects corners in winding order (0→1→2→3→0),
* so we reorder to: top-left, bottom-left, bottom-right, top-right.
*/
const _planeCorners = new Float32Array(12);
function createImageEdgeLines(values: ImageValues): Lines | undefined {
const positions = values.aPosition.ref.value;
const transforms = values.aTransform.ref.value;
const instanceCount = values.uInstanceCount.ref.value;
if (positions.length < 12) return undefined; // need 4 vertices × 3 components
// Reorder from [TL, BL, TR, BR] to winding order [TL, BL, BR, TR]
// V0 (TL) → slot 0
_planeCorners[0] = positions[0]; _planeCorners[1] = positions[1]; _planeCorners[2] = positions[2];
// V1 (BL) → slot 1
_planeCorners[3] = positions[3]; _planeCorners[4] = positions[4]; _planeCorners[5] = positions[5];
// V3 (BR) → slot 2
_planeCorners[6] = positions[9]; _planeCorners[7] = positions[10]; _planeCorners[8] = positions[11];
// V2 (TR) → slot 3
_planeCorners[9] = positions[6]; _planeCorners[10] = positions[7]; _planeCorners[11] = positions[8];
const builder = LinesBuilder.create(4 * instanceCount);
for (let inst = 0; inst < instanceCount; ++inst) {
const transform = Mat4();
Mat4.fromArray(transform, transforms, inst * 16);
addPlane(builder, _planeCorners, transform, 0);
}
return builder.getLines();
}
function createTrimEdgeLines(values: ImageValues): Lines | undefined {
const trimType = values.uTrimType.ref.value as number;
if (trimType === 0) return undefined; // no trim
const transforms = values.aTransform.ref.value;
const instanceCount = values.uInstanceCount.ref.value;
Vec3.copy(_trimPos, values.uTrimCenter.ref.value);
Quat.copy(_trimRot, values.uTrimRotation.ref.value);
Vec3.copy(_trimScale, values.uTrimScale.ref.value);
Mat4.copy(_trimTransform, values.uTrimTransform.ref.value);
if (trimType === Clip.Type.cube) {
return createCubeTrimLines(transforms, instanceCount);
} else if (trimType === Clip.Type.sphere) {
return createSphereTrimLines(transforms, instanceCount);
}
// For other trim types (plane, cylinder, cone), draw a cube outline as a fallback
// using the trim center/scale/rotation
return createCubeTrimLines(transforms, instanceCount);
}
function createCubeTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
// Build cube transform: translate * rotate * scale
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
const scaleMat = Mat4.fromScaling(Mat4(), _trimScale);
Mat4.mul(_tmpMat, translateMat, rotMat);
Mat4.mul(_tmpMat, _tmpMat, scaleMat);
// Apply inverse of trim transform
if (!Mat4.isIdentity(_trimTransform)) {
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
}
// addBox uses [0,1]^3, trim cube uses [-0.5,0.5]^3 — prepend offset
const offset = Mat4.fromTranslation(Mat4(), Vec3.create(-0.5, -0.5, -0.5));
Mat4.mul(_tmpMat, _tmpMat, offset);
const builder = LinesBuilder.create(12 * instanceCount);
for (let inst = 0; inst < instanceCount; ++inst) {
const instTransform = Mat4();
Mat4.fromArray(instTransform, transforms, inst * 16);
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
addBox(builder, combined, 0);
}
return builder.getLines();
}
function createSphereTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
const radius = Math.max(_trimScale[0] * 0.5, _trimScale[1] * 0.5, _trimScale[2] * 0.5);
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
Mat4.mul(_tmpMat, translateMat, rotMat);
if (!Mat4.isIdentity(_trimTransform)) {
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
}
const segments = 32;
const circlesPerDimension = 3;
const builder = LinesBuilder.create(segments * 3 * circlesPerDimension * instanceCount);
for (let inst = 0; inst < instanceCount; ++inst) {
const instTransform = Mat4();
Mat4.fromArray(instTransform, transforms, inst * 16);
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
addLinesSphere(builder, radius, combined, 0, { segments, circlesPerDimension });
}
return builder.getLines();
}
function createLinesRenderObject(lines: Lines, materialId: number, color: Color, alpha: number): GraphicsRenderObject {
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha };
const values = Lines.Utils.createValuesSimple(lines, props, color, 1);
const state = Lines.Utils.createRenderableState(props);
state.pickable = false;
return createRenderObject('lines', values, state, materialId);
}

View File

@@ -1,71 +0,0 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { BoundingSphereHelper, BoundingSphereHelperParams } from './bounding-sphere-helper';
import { ClipObjectHelper, ClipObjectHelperParams } from './clip-object-helper';
import { DirectVolumeHelper, DirectVolumeHelperParams } from './direct-volume-helper';
import { ImageHelper, ImageHelperParams } from './image-helper';
import { MeshHelper, MeshHelperParams } from './mesh-helper';
const DebugHelpersParams = {
...BoundingSphereHelperParams,
...ClipObjectHelperParams,
...MeshHelperParams,
...ImageHelperParams,
...DirectVolumeHelperParams,
};
type DebugHelpersParams = typeof DebugHelpersParams;
type DebugHelpersProps = PD.Values<DebugHelpersParams>;
export const DebugHelpers = PluginBehavior.create<DebugHelpersProps>({
name: 'extension-debug-helpers',
category: 'misc',
display: {
name: 'Debug Helpers'
},
ctor: class extends PluginBehavior.Handler<DebugHelpersProps> {
async register(): Promise<void> {
await this.ctx.canvas3dInitialized;
const canvas3d = this.ctx.canvas3d;
if (!canvas3d) return;
const dr = canvas3d.debugRegistry;
const { ctx, parent } = dr;
dr.register('bounding-sphere', new BoundingSphereHelper(ctx, parent, this.params));
dr.register('clip-object', new ClipObjectHelper(ctx, parent, this.params));
dr.register('mesh', new MeshHelper(ctx, parent, this.params));
dr.register('image', new ImageHelper(ctx, parent, this.params));
dr.register('direct-volume', new DirectVolumeHelper(ctx, parent, this.params));
}
update(params: DebugHelpersProps) {
const changed = super.update(params);
const canvas3d = this.ctx.canvas3d;
if (changed && canvas3d) {
canvas3d.debugRegistry.setProps(params);
canvas3d.requestDraw();
}
return changed;
}
unregister() {
const canvas3d = this.ctx.canvas3d;
if (!canvas3d) return;
const dr = canvas3d.debugRegistry;
dr.unregister('bounding-sphere');
dr.unregister('clip-object');
dr.unregister('mesh');
dr.unregister('image');
dr.unregister('direct-volume');
}
},
params: () => DebugHelpersParams,
canAutoUpdate: () => true,
});

View File

@@ -1,164 +0,0 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { ColorNames } from '../../mol-util/color/names';
import { Lines } from '../../mol-geo/geometry/lines/lines';
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { MeshValues } from '../../mol-gl/renderable/mesh';
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
export const MeshHelperParams = {
meshNormals: PD.Boolean(false, { description: 'Show normals of visible mesh render objects.' }),
};
export type MeshHelperParams = typeof MeshHelperParams;
export type MeshHelperProps = PD.Values<MeshHelperParams>;
const meshHelperMaterialId = getNextMaterialId();
const _v = Vec3();
const _n = Vec3();
const _start = Vec3();
const _end = Vec3();
export class MeshHelper implements DebugHelper<MeshHelperProps> {
readonly scene: Scene;
private readonly parent: Scene;
private _props: MeshHelperProps;
private renderObjects = new Map<number, GraphicsRenderObject>();
constructor(ctx: WebGLContext, parent: Scene, props: Partial<MeshHelperProps>) {
this.scene = Scene.create(ctx, 'blended');
this.parent = parent;
this._props = { ...PD.getDefaultValues(MeshHelperParams), ...props };
}
update() {
const previousIds = new Set(this.renderObjects.keys());
const currentIds = new Set<number>();
this.parent.forEach((r, ro) => {
if (!ro.state.visible) return;
if (ro.type !== 'mesh') return;
currentIds.add(ro.id);
// Skip if we already have normals for this render object
if (this.renderObjects.has(ro.id)) {
previousIds.delete(ro.id);
return;
}
const values = ro.values as MeshValues;
const lines = createNormalLines(values);
if (!lines) return;
const linesRO = createNormalLinesRenderObject(lines, meshHelperMaterialId);
this.scene.add(linesRO);
this.renderObjects.set(ro.id, linesRO);
});
// Remove normals for render objects no longer present
for (const id of previousIds) {
const linesRO = this.renderObjects.get(id);
if (linesRO) {
this.scene.remove(linesRO);
this.renderObjects.delete(id);
}
}
this.scene.update(void 0, false);
this.scene.commit();
}
syncVisibility() {
const visible = this._props.meshNormals;
this.renderObjects.forEach(ro => {
ro.state.visible = visible;
});
}
clear() {
this.renderObjects.clear();
this.scene.clear();
}
get isEnabled() {
return this._props.meshNormals;
}
get props() { return this._props as Readonly<MeshHelperProps>; }
setProps(props: Partial<MeshHelperProps>) {
Object.assign(this._props, props);
if (this.isEnabled) this.update();
}
}
//
function createNormalLines(values: MeshValues): Lines | undefined {
const positions = values.aPosition.ref.value;
const normals = values.aNormal.ref.value;
const indices = values.elements.ref.value;
const transforms = values.aTransform.ref.value;
const instanceCount = values.uInstanceCount.ref.value;
const vertexCount = positions.length / 3;
if (vertexCount === 0) return undefined;
// Determine normal line length: proportional to bounding sphere radius
const bs = values.boundingSphere.ref.value;
const normalLength = Math.max(bs.radius * 0.01, 0.1);
// Count unique vertices referenced by indices
const indexCount = values.drawCount.ref.value;
const builder = LinesBuilder.create(indexCount * instanceCount);
for (let inst = 0; inst < instanceCount; ++inst) {
const tOffset = inst * 16;
const transform = Mat4();
Mat4.fromArray(transform, transforms, tOffset);
// Use a set to avoid drawing duplicate normals for shared vertices
const visited = new Set<number>();
for (let i = 0; i < indexCount; ++i) {
const vi = indices[i];
if (visited.has(vi)) continue;
visited.add(vi);
const vo = vi * 3;
Vec3.set(_v, positions[vo], positions[vo + 1], positions[vo + 2]);
Vec3.set(_n, normals[vo], normals[vo + 1], normals[vo + 2]);
// Transform vertex position and normal direction by instance transform
Vec3.transformMat4(_start, _v, transform);
Vec3.transformDirection(_end, _n, transform);
Vec3.normalize(_end, _end);
Vec3.scaleAndAdd(_end, _start, _end, normalLength);
builder.addVec(_start, _end, 0);
}
}
return builder.getLines();
}
function createNormalLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.7 };
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.magenta, 1);
const state = Lines.Utils.createRenderableState(props);
state.pickable = false;
return createRenderObject('lines', values, state, materialId);
}

View File

@@ -59,7 +59,7 @@ export async function getG3dDataBlock(ctx: PluginContext, header: G3dHeader, url
async function getRawData(ctx: PluginContext, urlOrData: string | Uint8Array, range: { offset: number, size: number }) {
if (typeof urlOrData === 'string') {
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: { 'Range': `bytes=${range.offset}-${range.offset + range.size - 1}` }, type: 'binary' }));
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: [['Range', `bytes=${range.offset}-${range.offset + range.size - 1}`]], type: 'binary' }));
} else {
return urlOrData.slice(range.offset, range.offset + range.size);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -56,7 +56,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
private materialMap = new Map<string, number>();
private accessors: Record<string, any>[] = [];
private bufferViews: Record<string, any>[] = [];
private binaryBuffer: ArrayBufferLike[] = [];
private binaryBuffer: ArrayBuffer[] = [];
private byteOffset = 0;
private centerTransform: Mat4;
@@ -72,7 +72,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
return [min, max];
}
private addBuffer(buffer: ArrayBufferLike, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
private addBuffer(buffer: ArrayBuffer, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
this.binaryBuffer.push(buffer);
const bufferViewOffset = this.bufferViews.length;
@@ -241,7 +241,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
// create a glTF mesh if needed
if (instanceIndex === 0 || !sameGeometryBuffers || !sameColorBuffer) {
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = GlbExporter.getInstance(input, instanceIndex);
const { vertices, normals, indices, groups, vertexCount, drawCount } = GlbExporter.getInstance(input, instanceIndex);
// create geometry buffers if needed
if (instanceIndex === 0 || !sameGeometryBuffers) {
@@ -253,7 +253,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
// create a color buffer if needed
if (instanceIndex === 0 || !sameColorBuffer) {
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
}
// glTF mesh
@@ -304,7 +304,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
materials: this.materials
};
const createChunk = (chunkType: number, data: ArrayBufferLike[], byteLength: number, padChar: number): [ArrayBufferLike[], number] => {
const createChunk = (chunkType: number, data: ArrayBuffer[], byteLength: number, padChar: number): [ArrayBuffer[], number] => {
let padding = null;
if (byteLength % 4 !== 0) {
const pad = 4 - (byteLength % 4);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -30,10 +30,6 @@ import { RenderObjectExporter, RenderObjectExportData } from './render-object-ex
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { ValueCell } from '../../mol-util/value-cell';
import { ColorTheme } from '../../mol-theme/color';
import { computeFrenetFrames } from '../../mol-math/linear-algebra/3d/frenet-frames';
import { addTube } from '../../mol-geo/geometry/mesh/builder/tube';
import { arrayCopyOffset } from '../../mol-util/array';
const GeoExportName = 'geo-export';
@@ -53,32 +49,22 @@ export interface AddMeshInput {
groups: Float32Array | Uint8Array
vertexCount: number
drawCount: number
vertexMapping?: number[]
} | undefined
meshes: Mesh[] | undefined
values: BaseValues & {
readonly uDoubleSided?: ValueCell<boolean>
readonly aGroup?: ValueCell<Float32Array>
readonly tPositionGroup?: ValueCell<TextureImage<Float32Array>>
}
values: BaseValues & { readonly uDoubleSided?: ValueCell<any> }
isGeoTexture: boolean
mode: MeshMode
webgl: WebGLContext | undefined
ctx: RuntimeContext
vertexMapping?: number[]
}
export type MeshGeoData = {
values: BaseValues & {
readonly aGroup?: ValueCell<Float32Array>
readonly tPositionGroup?: ValueCell<TextureImage<Float32Array>>
}
groups?: Float32Array | Uint8Array,
vertexCount: number
instanceIndex: number
isGeoTexture: boolean
values: BaseValues,
groups: Float32Array | Uint8Array,
vertexCount: number,
instanceIndex: number,
isGeoTexture: boolean,
mode: MeshMode
vertexMapping?: number[]
}
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
@@ -91,7 +77,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
return unpackRGBToInt(r, g, b) / sizeDataFactor;
}
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number, vertexIndex: number): number {
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
const tSize = values.tSize.ref.value;
let size = 0;
switch (values.dSizeType.ref.value) {
@@ -108,13 +94,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const groupCount = values.uGroupCount.ref.value;
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex * groupCount + group);
break;
case 'vertex':
size = MeshExporter.getSizeFromTexture(tSize, vertexIndex);
break;
case 'vertexInstance':
const vertexCount = values.uVertexCount.ref.value;
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex * vertexCount + vertexIndex);
break;
}
return size * values.uSizeFactor.ref.value;
}
@@ -246,14 +225,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
indices: mesh.indexBuffer.ref.value,
groups: mesh.groupBuffer.ref.value,
vertexCount: mesh.vertexCount,
drawCount: mesh.triangleCount * 3,
vertexMapping: input.vertexMapping,
drawCount: mesh.triangleCount * 3
};
}
}
protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
const { values, groups, instanceIndex, isGeoTexture, mode } = geoData;
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
const groupCount = values.uGroupCount.ref.value;
const colorType = values.dColorType.ref.value;
const uColor = values.uColor.ref.value;
@@ -261,23 +239,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const overpaintType = values.dOverpaintType.ref.value;
const dOverpaint = values.dOverpaint.ref.value;
const tOverpaint = values.tOverpaint.ref.value.array;
const usePalette = values.dUsePalette.ref.value;
let vertexCount = geoData.vertexCount;
if (geoData.vertexMapping) {
vertexIndex = geoData.vertexMapping[vertexIndex];
vertexCount = values.uVertexCount.ref.value;
} else if (mode === 'lines') {
if (mode === 'lines') {
vertexIndex *= 2;
vertexCount *= 2;
}
const group = isGeoTexture
? MeshExporter.getGroup(groups!, vertexIndex)
: values.dGeometryType.ref.value === 'spheres'
? values.tPositionGroup!.ref.value.array[vertexIndex * 4 + 3]
: values.aGroup!.ref.value[vertexIndex];
let color: Color;
switch (colorType) {
case 'uniform':
@@ -287,10 +255,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
color = Color.fromArray(tColor, instanceIndex * 3);
break;
case 'group': {
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
color = Color.fromArray(tColor, group * 3);
break;
}
case 'groupInstance': {
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
break;
}
@@ -309,31 +279,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
default: throw new Error('Unsupported color type.');
}
if (usePalette) {
const palette = values.tPalette.ref.value;
const paletteArray = palette.array;
const paletteLength = paletteArray.length / 3;
const [r, g, b] = Color.toRgb(color);
const paletteValue = ((r * 256 * 256 + g * 256 + b) - 1) / ColorTheme.PaletteScale;
const fIndex = paletteValue * (paletteLength - 1);
if (palette.filter === 'nearest') {
const index = Math.round(fIndex);
color = Color.fromArray(paletteArray, index * 3);
} else { // linear
const index0 = Math.floor(fIndex);
const index1 = index0 + 1;
const t = fIndex - index0;
const color0 = Color.fromArray(paletteArray, index0 * 3);
const color1 = Color.fromArray(paletteArray, index1 * 3);
color = Color.interpolate(color0, color1, t);
}
}
if (dOverpaint) {
let overpaintColor: Color;
let overpaintAlpha: number;
switch (overpaintType) {
case 'groupInstance': {
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
const idx = (instanceIndex * groupCount + group) * 4;
overpaintColor = Color.fromArray(tOverpaint, idx);
overpaintAlpha = tOverpaint[idx + 3] / 255;
@@ -369,24 +320,16 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const transparencyType = values.dTransparencyType.ref.value;
let vertexCount = geoData.vertexCount;
if (geoData.vertexMapping) {
vertexIndex = geoData.vertexMapping[vertexIndex];
vertexCount = values.uVertexCount.ref.value;
} else if (mode === 'lines') {
if (mode === 'lines') {
vertexIndex *= 2;
vertexCount *= 2;
}
const group = isGeoTexture
? MeshExporter.getGroup(groups!, vertexIndex)
: values.dGeometryType.ref.value === 'spheres'
? values.tPositionGroup!.ref.value.array[vertexIndex * 4 + 3]
: values.aGroup!.ref.value[vertexIndex];
let transparency: number = 0;
if (dTransparency) {
switch (transparencyType) {
case 'groupInstance': {
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
const idx = (instanceIndex * groupCount + group);
transparency = tTransparency[idx] / 255;
break;
@@ -430,133 +373,10 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
}
private async addLineStrips(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aStart = values.aStart.ref.value;
const aEnd = values.aEnd.ref.value;
const aGroup = values.aGroup.ref.value;
const stripCount = values.stripCount.ref.value;
const stripOffsets = values.stripOffsets.ref.value;
const aMapping = values.aMapping.ref.value;
if (this.options.linesAsTriangles) {
const instanceCount = values.instanceCount.ref.value;
const meshes: Mesh[] = [];
const radialSegments = 6;
const vertexMapping: number[] = [];
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
for (let s = 0; s < stripCount; ++s) {
const stripStart = stripOffsets[s];
const stripEnd = stripOffsets[s + 1];
// Collect segments for this strip (only end-side vertices)
const segmentIndices: number[] = [];
for (let v = stripStart; v < stripEnd; v += 2) {
const mappingY = aMapping[v * 2 + 1];
if (mappingY < 0) continue;
segmentIndices.push(v);
}
if (segmentIndices.length === 0) continue;
const nPoints = segmentIndices.length + 1;
const linearSegments = nPoints - 1;
const curvePoints = new Float32Array(nPoints * 3);
const curveOrigIndices: number[] = [];
const widthValues = new Float32Array(nPoints);
const heightValues = new Float32Array(nPoints);
// First point: start of first segment
const v0 = segmentIndices[0];
arrayCopyOffset(curvePoints, aStart, 0, v0 * 3, 3);
curveOrigIndices.push(v0);
const radius0 = MeshExporter.getSize(values, instanceIndex, aGroup[v0], v0) * 0.03;
widthValues[0] = radius0;
heightValues[0] = radius0;
// Subsequent points: end of each segment
for (let j = 0; j < segmentIndices.length; ++j) {
const v = segmentIndices[j];
arrayCopyOffset(curvePoints, aEnd, (j + 1) * 3, v * 3, 3);
curveOrigIndices.push(v);
const radius = MeshExporter.getSize(values, instanceIndex, aGroup[v], v) * 0.03;
widthValues[j + 1] = radius;
heightValues[j + 1] = radius;
}
const normalVectors = new Float32Array(nPoints * 3);
const binormalVectors = new Float32Array(nPoints * 3);
computeFrenetFrames(curvePoints, normalVectors, binormalVectors, nPoints);
addTube(state, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, true, true, 'elliptical');
// Build vertex mapping
if (instanceIndex === 0) {
for (let i = 0; i <= linearSegments; ++i) {
for (let j = 0; j < radialSegments; ++j) {
vertexMapping.push(curveOrigIndices[i]);
}
}
for (let j = 0; j <= radialSegments; ++j) {
vertexMapping.push(curveOrigIndices[0]);
}
for (let j = 0; j <= radialSegments; ++j) {
vertexMapping.push(curveOrigIndices[linearSegments]);
}
}
}
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
} else {
// Decompose strips into individual line segments
let nLineSegments = 0;
for (let s = 0; s < stripCount; ++s) {
const stripStart = stripOffsets[s];
const stripEnd = stripOffsets[s + 1];
for (let v = stripStart; v < stripEnd; v += 2) {
const mappingY = aMapping[v * 2 + 1];
if (mappingY < 0) continue;
nLineSegments++;
}
}
const vertexCount = nLineSegments * 2;
const drawCount = nLineSegments;
const vertices = new Float32Array(vertexCount * 3);
const vertexMapping: number[] = [];
let vertexIndex = 0;
for (let s = 0; s < stripCount; ++s) {
const stripStart = stripOffsets[s];
const stripEnd = stripOffsets[s + 1];
for (let v = stripStart; v < stripEnd; v += 2) {
const mappingY = aMapping[v * 2 + 1];
if (mappingY < 0) continue;
arrayCopyOffset(vertices, aStart, vertexIndex * 3, v * 3, 3);
vertexMapping[vertexIndex] = v;
vertexIndex++;
arrayCopyOffset(vertices, aEnd, vertexIndex * 3, v * 3, 3);
vertexMapping[vertexIndex] = v;
vertexIndex++;
}
}
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount, vertexMapping }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
}
}
private async addLineSegments(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aStart = values.aStart.ref.value;
const aEnd = values.aEnd.ref.value;
const aGroup = values.aGroup.ref.value;
const vertexCount = (values.uVertexCount.ref.value / 4) * 2;
const drawCount = values.drawCount.ref.value / (2 * 3);
@@ -571,8 +391,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const topCap = true;
const bottomCap = true;
const vertexMapping: number[] = [];
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
@@ -580,44 +398,35 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
v3fromArray(start, aStart, i * 3);
v3fromArray(end, aEnd, i * 3);
const group = aGroup[i / 4];
const radius = MeshExporter.getSize(values, instanceIndex, group, i / 4) * 0.03;
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
const vertexOffset = state.vertices.elementCount;
state.currentGroup = aGroup[i];
addCylinder(state, start, end, 1, cylinderProps);
if (instanceIndex === 0) {
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
vertexMapping.push(i);
}
}
}
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
} else {
const n = vertexCount / 2;
const vertices = new Float32Array(n * 2 * 3);
for (let i = 0; i < n; ++i) {
arrayCopyOffset(vertices, aStart, i * 6, i * 4 * 3, 3);
arrayCopyOffset(vertices, aEnd, i * 6 + 3, i * 4 * 3, 3);
vertices[i * 6] = aStart[i * 4 * 3];
vertices[i * 6 + 1] = aStart[i * 4 * 3 + 1];
vertices[i * 6 + 2] = aStart[i * 4 * 3 + 2];
vertices[i * 6 + 3] = aEnd[i * 4 * 3];
vertices[i * 6 + 4] = aEnd[i * 4 * 3 + 1];
vertices[i * 6 + 5] = aEnd[i * 4 * 3 + 2];
}
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
}
}
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
if (values.stripCount.ref.value !== 0) {
await this.addLineStrips(values, webgl, ctx);
} else {
await this.addLineSegments(values, webgl, ctx);
}
}
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aPosition = values.aPosition.ref.value;
const aGroup = values.aGroup.ref.value;
@@ -631,7 +440,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const meshes: Mesh[] = [];
const detail = 0;
const vertexMapping: number[] = [];
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
@@ -640,21 +448,15 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
v3fromArray(center, aPosition, i * 3);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group, i) * 0.03;
const vertexOffset = state.vertices.elementCount;
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
state.currentGroup = group;
addSphere(state, center, radius, detail);
if (instanceIndex === 0) {
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
vertexMapping.push(i);
}
}
}
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
} else {
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'points', webgl, ctx });
}
@@ -669,7 +471,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const vertexCount = values.uVertexCount.ref.value;
const meshes: Mesh[] = [];
const sphereCount = (vertexCount / 6) * instanceCount;
const sphereCount = vertexCount / 6 * instanceCount;
let detail: number;
switch (this.options.primitivesQuality) {
case 'auto':
@@ -690,8 +492,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
assertUnreachable(this.options.primitivesQuality);
}
const vertexMapping: number[] = [];
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
@@ -699,21 +499,15 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
v3fromArray(center, aPosition, i * 3);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group, i);
const vertexOffset = state.vertices.elementCount;
const radius = MeshExporter.getSize(values, instanceIndex, group);
state.currentGroup = group;
addSphere(state, center, radius, detail);
if (instanceIndex === 0) {
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
vertexMapping.push(i);
}
}
}
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
}
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
@@ -751,8 +545,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
assertUnreachable(this.options.primitivesQuality);
}
const vertexMapping: number[] = [];
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
@@ -762,7 +554,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
v3sub(dir, end, start);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group, i) * aScale[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
const cap = aCap[i];
let topCap = cap === 1 || cap === 3;
let bottomCap = cap >= 2;
@@ -770,20 +562,14 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
[bottomCap, topCap] = [topCap, bottomCap];
}
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
const vertexOffset = state.vertices.elementCount;
state.currentGroup = aGroup[i];
addCylinder(state, start, end, 1, cylinderProps);
if (instanceIndex === 0) {
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
vertexMapping.push(i);
}
}
}
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
}
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {

View File

@@ -107,7 +107,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = ObjExporter.getInstance(input, instanceIndex);
const { vertices, normals, indices, groups, vertexCount, drawCount } = ObjExporter.getInstance(input, instanceIndex);
Mat4.fromArray(t, aTransform, instanceIndex * 16);
Mat4.mul(t, this.centerTransform, t);
@@ -137,7 +137,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
StringBuilder.newline(obj);
}
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping };
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
// color
const quantizedColors = new Uint8Array(drawCount * 3);

View File

@@ -100,7 +100,7 @@ def Material "material${materialKey}"
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = UsdzExporter.getInstance(input, instanceIndex);
const { vertices, normals, indices, groups, vertexCount, drawCount } = UsdzExporter.getInstance(input, instanceIndex);
Mat4.fromArray(t, aTransform, instanceIndex * 16);
Mat4.mul(t, this.centerTransform, t);
@@ -134,7 +134,7 @@ def Material "material${materialKey}"
StringBuilder.writeSafe(normalBuilder, ')');
}
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping };
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
// face
for (let i = 0; i < drawCount; ++i) {

View File

@@ -305,7 +305,7 @@ function PlotInteractivity({ drawing, interactity }: { drawing: MAPairwiseMetric
let labelNode: ReactNode | undefined;
if (label) {
const labelStyle: CSSProperties | undefined = label ? { fontSize: '45px', fill: 'black', fontWeight: 'bold', pointerEvents: 'none', userSelect: 'none' } : undefined;
let x: number, y: number, anchor: 'start' | 'end';
let x: number, y: number, anchor: string;
if (crosshairOffset![0] < PlotSize / 2) {
x = PlotOffset + crosshairOffset![0] + 20;
anchor = 'start';

View File

@@ -39,7 +39,7 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
const format = options?.format ?? 'cif';
const { structures } = plugin.managers.structure.hierarchy.current;
const files: [name: string, data: string | Uint8Array<ArrayBuffer>][] = [];
const files: [name: string, data: string | Uint8Array][] = [];
const entryMap = new Map<string, number>();
for (const _s of structures) {
@@ -80,7 +80,7 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
if (files.length === 1) {
download(new Blob([files[0][1]]), files[0][0]);
} else if (files.length > 1) {
const zipData: Record<string, Uint8Array<ArrayBuffer>> = {};
const zipData: Record<string, Uint8Array> = {};
for (const [fn, data] of files) {
if (data instanceof Uint8Array) {
zipData[fn] = data;

View File

@@ -25,7 +25,7 @@ export interface Mp4EncoderParams<A extends PluginStateAnimation = PluginStateAn
quantizationParameter?: number
}
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>): Promise<Uint8Array<ArrayBuffer>> {
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>) {
await ctx.update({ message: 'Initializing...', isIndeterminate: true });
validateViewport(params);
@@ -88,7 +88,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
stoppedAnimation = true;
encoder.finalize();
finalized = true;
return encoder.FS.readFile(encoder.outputFilename) as Uint8Array<ArrayBuffer>;
return encoder.FS.readFile(encoder.outputFilename);
} finally {
if (finalized) encoder.delete();
if (params.customBackground !== void 0) {

View File

@@ -15,7 +15,7 @@ import { Mp4AnimationParams, Mp4Controls } from './controls';
interface State {
busy?: boolean,
data?: { movie: Uint8Array<ArrayBuffer>, filename: string };
data?: { movie: Uint8Array, filename: string };
}
export class Mp4EncoderUI extends CollapsableControls<{}, State> {

View File

@@ -1 +1 @@
Please refer to the standalone documentation [here](https://molstar.org/mol-view-spec-docs/).
Find the MVS extension documentation [here](../../../docs/extensions/mvs/README.md).

View File

@@ -12,7 +12,7 @@ import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
import { PluginContext } from '../../mol-plugin/context';
import { StructureRepresentationProvider } from '../../mol-repr/structure/representation';
import { StateAction, StateObject, StateObjectCell, StateTree } from '../../mol-state';
import { StateAction, StateObjectCell, StateTree } from '../../mol-state';
import { Task } from '../../mol-task';
import { ColorTheme } from '../../mol-theme/color';
import { fileToDataUri } from '../../mol-util/file';
@@ -111,19 +111,6 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
this.ctx.state.data.actions.add(action);
}
this.ctx.state.data.registerRefResolver('mvs', (state, ref) => {
const tagSearch = StateTree.doPreOrder(state.tree, state.tree.root, { ref, ret: undefined as StateObject | undefined }, (n, _, s) => {
if (!n.tags) return;
for (const t of n.tags) {
if (t.startsWith('mvs-ref:') && t.substring(8) === ref) {
s.ret = state.cells.get(n.ref)?.obj?.data;
return false;
}
}
});
return tagSearch.ret;
});
this.ctx.managers.markdownExtensions.registerRefResolver('mvs', (plugin, refs) => {
const mvsRefs = new Set(refs.map(ref => `mvs-ref:${ref}`));
return StateTree.doPreOrder(
@@ -193,7 +180,6 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
for (const action of this.registrables.actions ?? []) {
this.ctx.state.data.actions.remove(action);
}
this.ctx.state.data.removeRefResolver('mvs');
this.ctx.managers.markdownExtensions.removeRefResolver('mvs');
}
},

View File

@@ -1,37 +1,39 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Camera } from '../../mol-canvas3d/camera';
import { CameraFogParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
import { CameraFogParams, Canvas3DParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
import { TrackballControlsParams } from '../../mol-canvas3d/controls/trackball';
import { BackgroundParams } from '../../mol-canvas3d/passes/background';
import { BloomParams } from '../../mol-canvas3d/passes/bloom';
import { DofParams } from '../../mol-canvas3d/passes/dof';
import { OutlineParams } from '../../mol-canvas3d/passes/outline';
import { ShadowParams } from '../../mol-canvas3d/passes/shadow';
import { SsaoParams } from '../../mol-canvas3d/passes/ssao';
import { Vec3 } from '../../mol-math/linear-algebra';
import { getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
import { getFocusSnapshot, getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { PluginState } from '../../mol-plugin/state';
import { StateObjectSelector, StateTransform } from '../../mol-state';
import { StateObjectSelector } from '../../mol-state';
import { fovAdjustedPosition } from '../../mol-util/camera';
import { ColorNames } from '../../mol-util/color/names';
import { deepClone } from '../../mol-util/object';
import { ParamDefinition } from '../../mol-util/param-definition';
import { decodeColor } from './helpers/utils';
import { MolstarLoadingContext } from './load';
import { SnapshotMetadata } from './mvs-data';
import { MVSAnimationNode } from './tree/animation/animation-tree';
import { MolstarNode, MolstarNodeParams } from './tree/molstar/molstar-tree';
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
import { Vector3 } from './tree/mvs/param-types';
const DefaultFocusOptions = {
minRadius: 5,
extraRadius: 0,
};
const DefaultCanvasBackgroundColor = ColorNames.white;
@@ -58,32 +60,35 @@ export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: Mols
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
const up = Vec3.create(...params.up);
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
const snapshot: Partial<Camera.Snapshot> = {
target,
position,
up,
radius,
radiusMax: radius,
minNear: params.near ?? undefined,
};
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius, radiusMax: radius };
return snapshot;
}
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector | undefined, params: MolstarNodeParams<'focus'> & { center?: Vector3 } }[], ignoreOrientation: boolean): PluginState.SnapshotFocusInfo {
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is undefined).
* Orient the camera based on a focus node params. **/
export async function setFocus(plugin: PluginContext, focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]) {
const snapshot = getFocusSnapshot(plugin, {
...snapshotFocusInfoFromMvsFocuses(focuses),
minRadius: DefaultFocusOptions.minRadius,
});
if (!snapshot) return;
resetSceneRadiusFactor(plugin);
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
}
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]): PluginState.SnapshotFocusInfo {
const lastFocus = (focuses.length > 0) ? focuses[focuses.length - 1] : undefined;
const direction = lastFocus?.params.direction ?? MVSTreeSchema.nodes.focus.params.fields.direction.default;
const up = lastFocus?.params.up ?? MVSTreeSchema.nodes.focus.params.fields.up.default;
return {
targets: focuses.map<PluginState.SnapshotFocusTargetInfo>(f => ({
targetRef: f.target?.ref === StateTransform.RootRef ? undefined : f.target?.ref, // need to treat root separately so it does not include invisible structure parts etc.
center: f.params.center ? Vec3.create(...f.params.center) : undefined,
targetRef: f.target.ref === '-=root=-' ? undefined : f.target.ref, // need to treat root separately so it does not include invisible structure parts etc.
radius: f.params.radius ?? undefined,
radiusFactor: f.params.radius_factor,
extraRadius: f.params.radius_extent,
})),
direction: ignoreOrientation ? undefined : Vec3.create(...direction),
up: ignoreOrientation ? undefined : Vec3.create(...up),
direction: Vec3.create(...direction),
up: Vec3.create(...up),
};
}
@@ -96,34 +101,24 @@ function adjustSceneRadiusFactor(plugin: PluginContext, cameraTarget: Vec3 | und
plugin.canvas3d?.setProps({ sceneRadiusFactor });
}
/** Reset `sceneRadiusFactor` property to the default value */
function resetSceneRadiusFactor(plugin: PluginContext) {
const sceneRadiusFactor = Canvas3DParams.sceneRadiusFactor.defaultValue;
plugin.canvas3d?.setProps({ sceneRadiusFactor });
}
/** Create object for PluginState.Snapshot.camera based on tree loading context and MVS snapshot metadata */
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, options: { previousTransitionDurationMs?: number, ignoreCameraOrientation?: boolean }): PluginState.Snapshot['camera'] {
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }): PluginState.Snapshot['camera'] {
const camera: PluginState.Snapshot['camera'] = {
transitionStyle: 'animate',
transitionDurationInMs: options.previousTransitionDurationMs ?? 0,
transitionDurationInMs: metadata.previousTransitionDurationMs ?? 0,
};
if (context.camera.cameraParams !== undefined) {
const cam = context.camera.cameraParams;
if (options.ignoreCameraOrientation) {
camera.focus = snapshotFocusInfoFromMvsFocuses([{
target: undefined,
params: {
center: cam.target,
radius: Vec3.distance(cam.target as number[] as Vec3, cam.position as number[] as Vec3) / 2,
direction: MVSTreeSchema.nodes.focus.params.fields.direction.default, // will be ignored
up: MVSTreeSchema.nodes.focus.params.fields.up.default, // will be ignored
radius_factor: 1, // will be ignored
radius_extent: 0, // will be ignored
},
}], true);
// This will not work exactly when viewport height>width because of how focusing works (could be solved by adjusting radius by aspect ration, but that would mess up cropping, and wouldn't work properly when aspect ration changes after loading)
} else {
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, cam);
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
}
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, context.camera.cameraParams);
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
} else {
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses, options.ignoreCameraOrientation ?? false);
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses);
}
return camera;
}
@@ -137,11 +132,6 @@ function optionalParams(enable: boolean | undefined, values: any, params: ParamD
return fallback;
}
function normalizeBackground(variant: any, prev: any): any {
if (!variant) return prev;
return ParamDefinition.normalizeParams(BackgroundParams, { variant }, 'children');
}
/** Create a deep copy of `oldCanvasProps` with values modified according to a canvas node params. */
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, animationNode: MVSAnimationNode<'animation'> | undefined): Canvas3DProps {
const params = canvasNode?.params;
@@ -167,8 +157,6 @@ export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: Mol
const bloom = molstar_postprocessing?.enable_bloom;
const bloomParams = molstar_postprocessing?.bloom_params;
const background = molstar_postprocessing?.background;
const trackballAnimation = animationNode?.custom?.molstar_trackball;
const trackballAnimationName = trackballAnimation?.name;
const trackballAnimationParams = trackballAnimation?.params ?? {};
@@ -182,7 +170,6 @@ export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: Mol
occlusion: optionalParams(occlusion, occlusionParams, SsaoParams, oldCanvasProps.postprocessing.occlusion),
dof: optionalParams(dof, dofParams, DofParams, oldCanvasProps.postprocessing.dof),
bloom: optionalParams(bloom, bloomParams, BloomParams, oldCanvasProps.postprocessing.bloom),
background: normalizeBackground(background, oldCanvasProps.postprocessing.background),
},
cameraFog: optionalParams(fog, fogParams, CameraFogParams, oldCanvasProps.cameraFog),
renderer: {
@@ -213,14 +200,13 @@ export function resetCanvasProps(plugin: PluginContext) {
...old,
postprocessing: {
...old,
outline: deepClone(DefaultCanvas3DParams.postprocessing.outline),
shadow: deepClone(DefaultCanvas3DParams.postprocessing.shadow),
occlusion: deepClone(DefaultCanvas3DParams.postprocessing.occlusion),
dof: deepClone(DefaultCanvas3DParams.postprocessing.dof),
bloom: deepClone(DefaultCanvas3DParams.postprocessing.bloom),
background: deepClone(DefaultCanvas3DParams.postprocessing.background),
outline: DefaultCanvas3DParams.postprocessing.outline,
shadow: DefaultCanvas3DParams.postprocessing.shadow,
occlusion: DefaultCanvas3DParams.postprocessing.occlusion,
dof: DefaultCanvas3DParams.postprocessing.dof,
bloom: DefaultCanvas3DParams.postprocessing.bloom,
},
cameraFog: deepClone(DefaultCanvas3DParams.cameraFog),
cameraFog: DefaultCanvas3DParams.cameraFog,
trackball: {
...old?.trackball,
animate: { name: 'off', params: {} },

View File

@@ -117,7 +117,7 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
return {
factory: MVSAnnotationColorTheme,
granularity: 'groupInstance',
preferSmoothing: false,
preferSmoothing: true,
color: color,
props: props,
description: 'Assigns colors based on custom MolViewSpec annotation data.',

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -9,19 +9,16 @@ import { TextBuilder } from '../../../../mol-geo/geometry/text/text-builder';
import { Structure } from '../../../../mol-model/structure';
import { ComplexTextVisual, ComplexVisual } from '../../../../mol-repr/structure/complex-visual';
import * as Original from '../../../../mol-repr/structure/visual/label-text';
import { eachSerialElement, ElementIterator, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
import { ElementIterator, eachSerialElement, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
import { VisualUpdateState } from '../../../../mol-repr/util';
import { VisualContext } from '../../../../mol-repr/visual';
import { Theme } from '../../../../mol-theme/theme';
import { arrayEqual } from '../../../../mol-util';
import { ColorNames } from '../../../../mol-util/color/names';
import { omitObjectKeys } from '../../../../mol-util/object';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { FormatTemplate } from '../../../../mol-util/string-format';
import { textPropsForSelection } from '../../helpers/label-text';
import { MVSAnnotationRow } from '../../helpers/schemas';
import { GroupedArray } from '../../helpers/utils';
import { getMVSAnnotationForStructure, MVSAnnotation } from '../annotation-prop';
import { groupRows } from '../../helpers/selections';
import { getMVSAnnotationForStructure } from '../annotation-prop';
/** Parameter definition for "label-text" visual in "MVS Annotation Label" representation */
@@ -29,8 +26,6 @@ export type MVSAnnotationLabelTextParams = typeof MVSAnnotationLabelTextParams
export const MVSAnnotationLabelTextParams = {
annotationId: PD.Text('', { description: 'Reference to "Annotation" custom model property', isEssential: true }),
fieldName: PD.Text('label', { description: 'Annotation field (column) from which to take label contents', isEssential: true }),
textFormat: PD.Text('{}', { description: 'Formatting template for the label text. Supports simplified f-string syntax. May reference multiple annotation fields. If value in any field is not defined, label will not be displayed.', isEssential: true }),
groupByFields: PD.ObjectList({ fieldName: PD.Text(), }, obj => obj.fieldName, { defaultValue: [{ fieldName: 'group_id' }], description: 'Set of annotation fields for grouping annotation rows into label instances (i.e. annotation rows with the same values in all group-by fields will yield one label instance). Annotation row with undefined value in any group-by field is considered a separate label instance.', isEssential: true }),
...omitObjectKeys(Original.LabelTextParams, ['level', 'chainScale', 'residueScale', 'elementScale']),
borderColor: { ...Original.LabelTextParams.borderColor, defaultValue: ColorNames.black },
};
@@ -47,11 +42,7 @@ export function MVSAnnotationLabelTextVisual(materialId: number): ComplexVisual<
getLoci: getSerialElementLoci,
eachLocation: eachSerialElement,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<MVSAnnotationLabelTextParams>, currentProps: PD.Values<MVSAnnotationLabelTextParams>) => {
state.createGeometry =
newProps.annotationId !== currentProps.annotationId
|| newProps.fieldName !== currentProps.fieldName
|| newProps.textFormat !== currentProps.textFormat
|| !arrayEqual(newProps.groupByFields, currentProps.groupByFields);
state.createGeometry = newProps.annotationId !== currentProps.annotationId || newProps.fieldName !== currentProps.fieldName;
}
}, materialId);
}
@@ -59,32 +50,16 @@ export function MVSAnnotationLabelTextVisual(materialId: number): ComplexVisual<
function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme, props: MVSAnnotationLabelTextProps, text?: Text): Text {
const { annotation, model } = getMVSAnnotationForStructure(structure, props.annotationId);
const rows = annotation?.getRows() ?? [];
const groups = GroupedArray.groupIndices(rows, rowGroupingFunction(annotation!, props.groupByFields.map(x => x.fieldName)));
const builder = TextBuilder.create(props, groups.count, groups.count / 2, text);
const template = FormatTemplate(props.textFormat);
for (let iGroup = 0; iGroup < groups.count; iGroup++) {
const rowIndicesInGroup = GroupedArray.getGroup(groups, iGroup);
const labelText = template.format(field => annotation!.getValueForRow(rowIndicesInGroup[0], field || props.fieldName));
const { count, offsets, grouped } = groupRows(rows);
const builder = TextBuilder.create(props, count, count / 2, text);
for (let iGroup = 0; iGroup < count; iGroup++) {
const iFirstRowInGroup = grouped[offsets[iGroup]];
const labelText = annotation!.getValueForRow(iFirstRowInGroup, props.fieldName);
if (!labelText) continue;
const rowsInGroup = rowIndicesInGroup.map(i => rows[i]);
const p = textPropsForSelection(structure, rowsInGroup, model);
const rowsInGroup = grouped.slice(offsets[iGroup], offsets[iGroup + 1]).map(j => rows[j]);
const p = textPropsForSelection(structure, theme.size.size, rowsInGroup, model);
if (!p) continue;
builder.add(labelText, p.center[0], p.center[1], p.center[2], p.depth, p.scale, p.group);
}
return builder.getText();
}
function rowGroupingFunction(annotation: MVSAnnotation, groupByFields: string[]): (row: MVSAnnotationRow, i: number) => string | undefined {
if (groupByFields.length === 1) {
const groupByField = groupByFields[0];
return (row, i) => annotation.getValueForRow(i, groupByField);
}
if (groupByFields.length === 0) {
return () => '';
}
return (row, i) => {
const values = groupByFields.map(field => annotation.getValueForRow(i, field));
if (values.includes(undefined)) return undefined;
return values.join('\t');
};
}

View File

@@ -12,17 +12,17 @@ import { CustomModelProperty } from '../../../mol-model-props/common/custom-mode
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Model } from '../../../mol-model/structure';
import { Structure, StructureElement, Unit } from '../../../mol-model/structure/structure';
import { Structure, StructureElement } from '../../../mol-model/structure/structure';
import { Asset } from '../../../mol-util/assets';
import { Jsonable, canonicalJsonString } from '../../../mol-util/json';
import { objectOfArraysToArrayOfObjects, pickObjectKeysWithRemapping, promiseAllObj } from '../../../mol-util/object';
import { Choice } from '../../../mol-util/param-choice';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { ElementRanges } from '../helpers/element-ranges';
import { AtomRanges } from '../helpers/atom-ranges';
import { IndicesAndSortings } from '../helpers/indexing';
import { MaybeStringParamDefinition } from '../helpers/param-definition';
import { MVSAnnotationRow, MVSAnnotationSchema, getCifAnnotationSchema } from '../helpers/schemas';
import { getAtomRangesForRow, getGaussianRangesForRow, getSphereRangesForRow } from '../helpers/selections';
import { getAtomRangesForRow } from '../helpers/selections';
import { Maybe, isDefined, safePromise } from '../helpers/utils';
@@ -141,25 +141,10 @@ export function getMVSAnnotationForStructure(structure: Structure, annotationId:
type FieldRemapping = Record<string, string | null>;
/** Mapping `ElementIndex` -> annotation row index for all elements of one kind (atoms, spheres, gaussians) in a `Model`.
/** Mapping `ElementIndex` -> annotation row index for all elements in a `Model`.
* `-1` means no row applies to the element.
* `null` means no row applies to any element. */
type IndexedElements = number[] | null;
/** Mapping `ElementIndex` -> annotation row index for atoms, spheres, and gaussians in a `Model`. */
type IndexedModel = {
atoms: IndexedElements,
spheres: IndexedElements,
gaussians: IndexedElements,
};
function getIndexedElementsForUnitKind(indexedModel: IndexedModel, unitKind: Unit.Kind): IndexedElements {
if (unitKind === Unit.Kind.Atomic) return indexedModel.atoms;
if (unitKind === Unit.Kind.Spheres) return indexedModel.spheres;
if (unitKind === Unit.Kind.Gaussians) return indexedModel.gaussians;
console.warn(`Unknown Unit.Kind value: ${unitKind}`);
return null;
}
type IndexedModel = number[] | null;
/** Main class for processing MVS annotation */
export class MVSAnnotation {
@@ -217,8 +202,7 @@ export class MVSAnnotation {
/** Return value of field `fieldName` assigned to location `loc`, if any */
getValueForLocation(loc: StructureElement.Location, fieldName: string): string | undefined {
const indexedModel = this.getIndexedModel(loc.unit.model, loc.unit.conformation.operator.instanceId);
const indexedElements = getIndexedElementsForUnitKind(indexedModel, loc.unit.kind);
const iRow = indexedElements ? indexedElements[loc.element] : -1;
const iRow = (indexedModel !== null) ? indexedModel[loc.element] : -1;
return this.getValueForRow(iRow, fieldName);
}
/** Return value of field `fieldName` assigned to `i`-th annotation row, if any */
@@ -251,22 +235,16 @@ export class MVSAnnotation {
private getRowForEachAtom(model: Model, instanceId: string): IndexedModel {
const indices = IndicesAndSortings.get(model);
const nAtoms = model.atomicHierarchy.atoms._rowCount;
const nSpheres = model.coarseHierarchy.spheres.count;
const nGaussians = model.coarseHierarchy.gaussians.count;
let indexedAtoms: IndexedElements = null;
let indexedSpheres: IndexedElements = null;
let indexedGaussians: IndexedElements = null;
let result: IndexedModel = null;
const rows = this.getRows();
for (let iRow = 0, nRows = rows.length; iRow < nRows; iRow++) {
const row = rows[iRow];
for (let i = 0, nRows = rows.length; i < nRows; i++) {
const row = rows[i];
const atomRanges = getAtomRangesForRow(row, model, instanceId, indices);
indexedAtoms = fillValueOnRanges(indexedAtoms, nAtoms, atomRanges, iRow);
const sphereRanges = getSphereRangesForRow(row, model, instanceId, indices);
indexedSpheres = fillValueOnRanges(indexedSpheres, nSpheres, sphereRanges, iRow);
const gaussianRanges = getGaussianRangesForRow(row, model, instanceId, indices);
indexedGaussians = fillValueOnRanges(indexedGaussians, nGaussians, gaussianRanges, iRow);
if (AtomRanges.count(atomRanges) === 0) continue;
result ??= Array(nAtoms).fill(-1);
AtomRanges.foreach(atomRanges, (from, to) => result!.fill(i, from, to));
}
return { atoms: indexedAtoms, spheres: indexedSpheres, gaussians: indexedGaussians };
return result;
}
/** Parse and return all annotation rows in this annotation, or return cached result if available */
@@ -377,7 +355,6 @@ function getRowsFromCif(data: CifCategory, schema: MVSAnnotationSchema, fieldRem
const columnArray = getArrayFromCifCategory(data, srcKey, cifSchema[key]); // Avoiding `column.toArray` as it replaces . and ? fields by 0 or ''
if (columnArray) columns[key] = columnArray;
}
if (Object.keys(columns).length === 0) return new Array(data.rowCount).fill({});
return objectOfArraysToArrayOfObjects(columns);
}
@@ -460,11 +437,3 @@ function annotationSourceFromSpec(s: MVSAnnotationSpec): MVSAnnotationSource {
return { kind: 'source-cif' };
}
}
/** In `array`, set value `fillValue` to all positions described by `fillRanges`. In case `array` is `null`, initialize it with length `n` prefilled with -1. */
function fillValueOnRanges(array: IndexedElements, n: number, fillRanges: ElementRanges | undefined, fillValue: number): IndexedElements {
if (!fillRanges || ElementRanges.count(fillRanges) === 0) return array;
const out = array ?? Array(n).fill(-1);
ElementRanges.foreach(fillRanges, (from, to) => out.fill(fillValue, from, to));
return out;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -45,11 +45,11 @@ export const MVSAnnotationStructureComponent = MVSTransform({
to: SO.Molecule.Structure,
params: MVSAnnotationStructureComponentParams,
})({
apply({ a, params, cache }) {
return createMVSAnnotationStructureComponent(a.data, params, cache as MVSComponentCache);
apply({ a, params }) {
return createMVSAnnotationStructureComponent(a.data, params);
},
update: ({ a, b, oldParams, newParams, cache }) => {
return updateMVSAnnotationStructureComponent(a.data, b, oldParams, newParams, cache as MVSComponentCache);
update: ({ a, b, oldParams, newParams }) => {
return updateMVSAnnotationStructureComponent(a.data, b, oldParams, newParams);
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
@@ -75,11 +75,8 @@ export function createMVSAnnotationSubstructure(structure: Structure, params: MV
}
}
interface MVSComponentCache { source?: Structure }
/** Create a substructure PSO based on `MVSAnnotationStructureComponentProps` */
export function createMVSAnnotationStructureComponent(structure: Structure, params: MVSAnnotationStructureComponentProps, cache: MVSComponentCache) {
cache.source = structure;
export function createMVSAnnotationStructureComponent(structure: Structure, params: MVSAnnotationStructureComponentProps) {
const component = createMVSAnnotationSubstructure(structure, params);
if (params.nullIfEmpty && component.elementCount === 0) return StateObject.Null;
@@ -105,23 +102,15 @@ export function createMVSAnnotationStructureComponent(structure: Structure, para
}
/** Update a substructure PSO based on `MVSAnnotationStructureComponentProps` */
export function updateMVSAnnotationStructureComponent(a: Structure, b: SO.Molecule.Structure, oldParams: MVSAnnotationStructureComponentProps, newParams: MVSAnnotationStructureComponentProps, cache: MVSComponentCache) {
const structureChanged = !cache.source || !Structure.areEquivalent(a, cache.source);
cache.source = a;
if (structureChanged) {
return StateTransformer.UpdateResult.Recreate;
export function updateMVSAnnotationStructureComponent(a: Structure, b: SO.Molecule.Structure, oldParams: MVSAnnotationStructureComponentProps, newParams: MVSAnnotationStructureComponentProps) {
const change = !deepEqual(newParams, oldParams);
const needsRecreate = !deepEqual(omitObjectKeys(newParams, ['label']), omitObjectKeys(oldParams, ['label']));
if (!change) {
return StateTransformer.UpdateResult.Unchanged;
}
const coreParamsChanged = !deepEqual(omitObjectKeys(newParams, ['label']), omitObjectKeys(oldParams, ['label']));
if (coreParamsChanged) {
return StateTransformer.UpdateResult.Recreate;
}
const labelChanged = newParams.label !== oldParams.label;
if (labelChanged) {
if (!needsRecreate) {
b.label = newParams.label || b.label;
return StateTransformer.UpdateResult.Updated;
}
return StateTransformer.UpdateResult.Unchanged;
return StateTransformer.UpdateResult.Recreate;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -11,7 +11,6 @@ import { Loci } from '../../../mol-model/loci';
import { Structure, StructureElement } from '../../../mol-model/structure';
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { FormatTemplate } from '../../../mol-util/string-format';
import { filterDefined } from '../helpers/utils';
import { MVSAnnotationsProvider } from './annotation-prop';
@@ -22,7 +21,6 @@ export const MVSAnnotationTooltipsParams = {
{
annotationId: PD.Text('', { description: 'Reference to "MVS Annotation" custom model property' }),
fieldName: PD.Text('tooltip', { description: 'Annotation field (column) from which to take color values' }),
textFormat: PD.Text('{}', { description: 'Formatting template for tooltip text. Supports simplified f-string syntax. May reference multiple annotation fields. If value in any field is not defined, tooltip will not be displayed.' }),
},
obj => `${obj.annotationId}:${obj.fieldName}`
),
@@ -61,9 +59,7 @@ export const MVSAnnotationTooltipsLabelProvider = {
const tooltipProps = MVSAnnotationTooltipsProvider.get(location.structure).value;
if (!tooltipProps || tooltipProps.tooltips.length === 0) return undefined;
const annotations = MVSAnnotationsProvider.get(location.unit.model).value;
const texts = tooltipProps.tooltips.map(p =>
FormatTemplate(p.textFormat).format(field => annotations?.getAnnotation(p.annotationId)?.getValueForLocation(location, field || p.fieldName))
);
const texts = tooltipProps.tooltips.map(p => annotations?.getAnnotation(p.annotationId)?.getValueForLocation(location, p.fieldName));
return filterDefined(texts).join(' | ');
default:
return undefined;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -75,7 +75,7 @@ function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme,
break;
case 'selection':
const substructure = substructureFromSelector(structure, item.position.params.selector);
const p = textPropsForSelection(substructure, [{}]);
const p = textPropsForSelection(substructure, theme.size.size, [{}]);
const group = serialIndexOfSubstructure(structure, substructure) ?? 0;
if (p) builder.add(item.text, p.center[0], p.center[1], p.center[2], p.depth, p.scale, group);
break;

View File

@@ -61,7 +61,6 @@ export const ParseMVSX = MVSTransform({
export const LoadMvsDataParams = {
appendSnapshots: PD.Boolean(false, { description: 'If true, add snapshots from MVS into current snapshot list; if false, replace the snapshot list.' }),
keepCamera: PD.Boolean(false, { description: 'If true, any camera positioning from the MVS state will be ignored and the current camera position will be kept.' }),
keepCameraOrientation: PD.Boolean(false, { description: 'If true, any camera orientation from the MVS state will be ignored and the current camera orientation will be kept (camera target position will be loaded from MVS). keepCamera option overrides this.' }),
applyExtensions: PD.Boolean(true, { description: 'If true, apply builtin MVS-loading extensions (not a part of standard MVS specification).' }),
};
@@ -72,7 +71,7 @@ export const LoadMvsData = StateAction.build({
params: LoadMvsDataParams,
})(({ a, params }, plugin: PluginContext) => Task.create('Load MVS Data', async () => {
const { mvsData, sourceUrl } = a.data;
await loadMVS(plugin, mvsData, { appendSnapshots: params.appendSnapshots, keepCamera: params.keepCamera, keepCameraOrientation: params.keepCameraOrientation, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
await loadMVS(plugin, mvsData, { appendSnapshots: params.appendSnapshots, keepCamera: params.keepCamera, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
}));
@@ -113,23 +112,16 @@ export const MVSXFormatProvider: DataFormatProvider<{}, StateObjectRef<Mvs>, any
* add all contained files to `plugin`'s asset manager,
* and parse the main file in the archive as MVSJ.
* Return parsed MVS data and `sourceUrl` for resolution of relative URIs. */
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array<ArrayBuffer>, mainFilePathOrOptions?: string | { mainFilePath?: string, doNotClearAssets?: boolean }): Promise<{ mvsData: MVSData, sourceUrl: string }> {
// TODO: on next major version, streamline mainFilePathOrOptions
if (typeof mainFilePathOrOptions === 'string') {
mainFilePathOrOptions = { mainFilePath: mainFilePathOrOptions };
}
const mainFilePath = mainFilePathOrOptions?.mainFilePath ?? 'index.mvsj';
const doNotClearAssets = mainFilePathOrOptions?.doNotClearAssets ?? false;
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array, mainFilePath: string = 'index.mvsj'): Promise<{ mvsData: MVSData, sourceUrl: string }> {
// Ensure at most one generation of MVSX file assets exists in the asset manager.
// Hopefully, this is a reasonable compromise to ensure MVSX files work in multi-snapshot
// states.
if (!doNotClearAssets) clearMVSXFileAssets(plugin);
clearMVSXFileAssets(plugin);
const archiveId = `ni,MurmurHash3_128;${murmurHash3_128_fromBytes(data, 42)}`;
let files: { [path: string]: Uint8Array<ArrayBuffer> };
let files: { [path: string]: Uint8Array };
try {
files = await unzip(runtimeCtx, data.buffer) as typeof files;
files = await unzip(runtimeCtx, data) as typeof files;
} catch (err) {
plugin.log.error('Invalid MVSX file');
throw err;
@@ -146,7 +138,7 @@ export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext
return { mvsData, sourceUrl };
}
export async function loadMVSData(plugin: PluginContext, data: MVSData | StringLike | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: MVSLoadOptions) {
export async function loadMVSData(plugin: PluginContext, data: MVSData | StringLike | Uint8Array, format: 'mvsj' | 'mvsx', options?: MVSLoadOptions) {
if (typeof data === 'string' && data.startsWith('base64')) {
data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array
}
@@ -168,7 +160,7 @@ export async function loadMVSData(plugin: PluginContext, data: MVSData | StringL
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
}
await plugin.runTask(Task.create('Load MVSX file', async ctx => {
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array<ArrayBuffer>, { doNotClearAssets: options?.appendSnapshots });
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array);
await loadMVS(plugin, parsed.mvsData, { sanityChecks: true, ...options, sourceUrl: parsed.sourceUrl });
}));
} else {
@@ -198,7 +190,7 @@ function arcpUri(archiveId: string, path: string): string {
/** Add a URL asset to asset manager.
* Skip if an asset with the same URL already exists. */
function ensureUrlAsset(manager: AssetManager, url: string, data: Uint8Array<ArrayBuffer>, options?: { isFile?: boolean }) {
function ensureUrlAsset(manager: AssetManager, url: string, data: Uint8Array, options?: { isFile?: boolean }) {
const asset = Asset.getUrlAsset(manager, url);
if (!manager.has(asset)) {
const filename = url.split('/').pop() ?? 'file';

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -72,7 +72,7 @@ export const DefaultMultilayerColorThemeProps: MultilayerColorThemeProps = { lay
* If a nested theme provider has `ensureCustomProperties` methods, these will not be called automatically
* (the caller must ensure that any required custom properties be attached). */
function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry): ColorTheme<MultilayerColorThemeParams> {
const { colorLayers, granularity, preferSmoothing } = makeLayers(ctx, props, colorThemeRegistry);
const { colorLayers, granularity } = makeLayers(ctx, props, colorThemeRegistry);
function structureElementColor(loc: StructureElement.Location, isSecondary: boolean): Color {
for (const layer of colorLayers) {
@@ -101,7 +101,7 @@ function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorT
return {
factory: (ctx_, props_) => makeMultilayerColorTheme(ctx_, props_, colorThemeRegistry),
granularity,
preferSmoothing,
preferSmoothing: true,
color: color,
props: props,
description: 'Combines colors from multiple color themes.',
@@ -136,7 +136,6 @@ interface ColorLayer {
function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry) {
const colorLayers: ColorLayer[] = [];
let granularityFlags = 0;
let preferSmoothing = false;
for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from the end to get top layer first, bottom layer last
const layer = props.layers[i];
const themeProvider = colorThemeRegistry.get(layer.theme.name);
@@ -176,9 +175,8 @@ function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, col
default:
console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`);
}
if (theme.preferSmoothing) preferSmoothing = true;
}
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags), preferSmoothing };
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags) };
}

View File

@@ -33,7 +33,6 @@ import { Task } from '../../../mol-task';
import { round } from '../../../mol-util';
import { range } from '../../../mol-util/array';
import { Asset } from '../../../mol-util/assets';
import { Clip } from '../../../mol-util/clip';
import { Color } from '../../../mol-util/color';
import { MarkerActions } from '../../../mol-util/marker-action';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -41,7 +40,6 @@ import { capitalize } from '../../../mol-util/string';
import { rowsToExpression, rowToExpression } from '../helpers/selections';
import { collectMVSReferences, decodeColor, isDefined } from '../helpers/utils';
import { addParamDefaults } from '../tree/generic/params-schema';
import { treeValidationIssues } from '../tree/generic/tree-validation';
import { MolstarNode, MolstarNodeParams, MolstarSubtree } from '../tree/molstar/molstar-tree';
import { MVSNode, MVSTreeSchema } from '../tree/mvs/mvs-tree';
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
@@ -67,12 +65,6 @@ 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',
@@ -89,35 +81,15 @@ export const MVSDownloadPrimitiveData = MVSTransform({
const url = Asset.getUrlAsset(plugin.managers.asset, params.uri);
const asset = await plugin.managers.asset.resolve(url, 'string').runInContext(ctx);
const node = JSON.parse(StringLike.toString(asset.data)) as MolstarSubtree<'primitives'>;
const validationIssues = treeValidationIssues(MVSTreeSchema, node, { anyRoot: true });
if (validationIssues) {
throw new Error(`Invalid primitive data from ${params.uri}:\n${validationIssues.join('\n')}`);
}
if (node.kind !== 'primitives') {
throw new Error(`Expected primitives node from ${params.uri}, got ${node.kind}`);
}
const nodeWithDefaults: MolstarSubtree<'primitives'> = {
...node,
params: addParamDefaults(MVSTreeSchema.nodes.primitives.params, node.params || {}),
children: node.children?.map((child: any) => {
if (child.kind === 'primitive') {
return {
...child,
params: addParamDefaults(MVSTreeSchema.nodes.primitive.params, child.params || {})
};
}
return child;
})
};
(cache as any).asset = asset;
return new MVSPrimitivesData({
node: nodeWithDefaults,
node,
defaultStructure: SO.Molecule.Structure.is(a) ? a.data : undefined,
structureRefs: {},
primitives: getPrimitives(nodeWithDefaults),
options: { ...nodeWithDefaults.params },
primitives: getPrimitives(node),
options: { ...node.params },
positionCache: new Map(),
instances: getInstances(nodeWithDefaults.params),
instances: getInstances(node.params),
}, { label: 'Primitive Data' });
});
},
@@ -169,8 +141,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
from: MVSPrimitivesData,
to: SO.Shape.Provider,
params: {
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh'),
clip: PD.Value<Clip.Props | undefined>(undefined, { isHidden: true })
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh')
}
})({
apply({ a, params, dependencies }) {
@@ -189,7 +160,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
label,
data: context,
params: {
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customMeshParams }),
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, ...customMeshParams }),
...snapshotKey,
...markdownCommands,
},
@@ -213,7 +184,6 @@ export const MVSBuildPrimitiveShape = MVSTransform({
tetherLength: options?.label_tether_length ?? 1,
background: isDefined(bgColor),
backgroundColor: isDefined(bgColor) ? decodeColor(bgColor) : undefined,
clip: params.clip,
...customLabelParams,
}),
...snapshotKey,
@@ -230,7 +200,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
label,
data: context,
params: {
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customLineParams }),
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, ...customLineParams }),
...snapshotKey,
...markdownCommands,
},
@@ -611,7 +581,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,
@@ -644,7 +614,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,
@@ -679,7 +649,7 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev: Text | und
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
} satisfies MVSPrimitiveShapeSourceData,
},
labelsBuilder.getText(),
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,

View File

@@ -73,7 +73,7 @@ export const ElementSet = {
/** Return a substructure of `structure` defined by `selector` */
export function substructureFromSelector(structure: Structure, selector: Selector): Structure {
const pso = (selector.name === 'annotation') ?
createMVSAnnotationStructureComponent(structure, { ...selector.params, label: '', nullIfEmpty: false }, {})
createMVSAnnotationStructureComponent(structure, { ...selector.params, label: '', nullIfEmpty: false })
: createStructureComponent(structure, { type: selector, label: '', nullIfEmpty: false }, { source: structure });
return PluginStateObject.Molecule.Structure.is(pso) ? pso.data : Structure.Empty;
}

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