Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Rose
3667092327 tweak cube clamp param 2022-11-08 22:28:47 -08:00
Alexander Rose
842824057b add param to clamp cube values 2022-11-08 22:17:23 -08:00
648 changed files with 28674 additions and 146651 deletions

View File

@@ -18,7 +18,7 @@
],
"brace-style": "off",
"comma-spacing": "off",
"space-infix-ops": "off",
"space-infix-ops": "error",
"comma-dangle": "off",
"eqeqeq": [
"error",
@@ -55,8 +55,7 @@
"block-spacing": "error",
"keyword-spacing": "off",
"space-before-blocks": "error",
"semi-spacing": "error",
"no-constant-binary-expression": "error"
"semi-spacing": "error"
},
"overrides": [
{
@@ -108,7 +107,6 @@
"1tbs", { "allowSingleLine": true }
],
"@typescript-eslint/comma-spacing": "error",
"@typescript-eslint/space-infix-ops": "error",
"@typescript-eslint/space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",

View File

@@ -1,5 +1,3 @@
name: Build
on:
push:
pull_request:
@@ -11,12 +9,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18
node-version: 16
- run: npm ci
- run: sudo apt-get install xvfb
- name: Lint
run: npm run lint
- name: Test
run: npm install --no-save "gl@^6.0.2" && xvfb-run --auto-servernum npm run jest
run: npm install --no-save "gl@^5.0.0" && xvfb-run --auto-servernum npm run jest
- name: Build
run: npm run build

18
.travis.yml Normal file
View File

@@ -0,0 +1,18 @@
language: node_js
os: linux
sudo: required
dist: trusty
before_install:
- sudo apt-get install -y mesa-utils
- sudo apt-get install -y xvfb
- sudo apt-get install -y libgl1-mesa-dri
- sudo apt-get install -y libglapi-mesa
- sudo apt-get install -y libosmesa6
- sudo apt-get install -y gcc-4.9
- sudo apt-get install -y libstdc++6
- sudo apt-get install -y libxi-dev
node_js:
- "12"
- "10"
before_script:
- export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start

View File

@@ -3,430 +3,21 @@ 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]
## [v4.1.0] - 2023-03-31
- Add `VolumeTransform` to translate/rotate a volume like in a structure superposition
- Fix BinaryCIF encoder edge cases caused by re-encoding an existing BinaryCIF file
- Fix edge-case where width/height in InputObserver are not correct
- Fix transparency rendering fallback (#1058)
- Fix SSAO broken when `OES_texture_float_linear` is unavailable
- Add `normalOffset` to `external-volume` color theme
- This can give results similar to pymol's surface_ramp_above_mode=1
- Add `rotation` parameter to skybox background
## [v4.0.1] - 2023-02-19
- Fix BinaryCIF decoder edge cases. Fixes mmCIF model export from data provided by ModelServer.
- MolViewSpec extension: support for MVSX file format
- Revert "require WEBGL_depth_texture extension" & "remove renderbuffer use"
## [v4.0.0] - 2023-02-04
- Add Mesoscale Explorer app for investigating large systems
- [Breaking] Remove `cellpack` extension (superseded by Mesoscale Explorer app)
- [Breaking] Set minimal node.js version to 18
- [Breaking] Generalize rcsb/assembly-symmetry/ extension
- Move to assembly-symmetry/
- Remove RCSB specific dependencies and prefixes
- [Breaking] Require `WEBGL_depth_texture` webgl extension
- Remove `renderbuffer` use
- [Breaking] Change build target to ES2018
- Custom builds only require ES6 for dependencies like immer.js
- [Breaking] Changed `createPluginUI`
- The function now takes a single `options` argument
- The caller must specify a `render` method that mounts the Mol* react component to DOM
- A default `renderReact18` method is provided, but needs to be imported separately
- To support React 16 and 17, `ReactDOM.render` can be passed
- Improve `SetUtils` performance using ES6 features
- [Breaking] Reduce memory usage of `SymmetryOperator.ArrayMapping`
- Requires calling methods from instance
- [Breaking] Fix `mol-model/structure/model/properties/seconday-structure.ts` file name (#938)
- [Breaking] Add `Canvas3DContext` runtime props
- Props: pixelScale, pickScale, transparency (blended, wboit, dpoit)
- Replaces instantiation-time attribs
- [Breaking] Change default compile target to ES2018
- [Breaking] Add culling & LOD support
- Cull per-object and per-instance
- Cull based on frustum and camera distance
- LOD visibility based on camera distance
- Special LOD mode for spheres with automatic levels
- Occlusion culling (only WebGL2)
- Hi-Z pass
- Cull based on previous frame's Hi-Z buffer
- Add stochastic/dithered transparency to fade overlapping LODs in and out
- Add "Automatic Detail" preset that shows surface/cartoon/ball & stick based on camera distance
## [v3.45.0] - 2023-02-03
- Add color interpolation to impostor cylinders
- MolViewSpec components are applicable only when the model has been loaded from MolViewSpec
- Add `snapshotKey` and `tooltip` params to loci `LabelRepresentation`
- Update `FocusLoci` behavior to support `snapshotKey` param
- Clicking a visual with `snapshotKey` will trigger that snapshot
- Render multiline loci label tooltips as Markdown
- `ParamDefinition.Text` updates:
- Support `multiline` inputs
- Support `placeholder` parameter
- Support `disableInteractiveUpdates` to only trigger updates once the control loses focus
- Move dependencies related to the headless context from optional deps to optional peer deps
## [v3.44.0] - 2023-01-06
- Add new `cartoon` visuals to support atomic nucleotide base with sugar
- Add `thicknessFactor` to `cartoon` representation for scaling nucleotide block/ring/atomic-fill visuals
- Use bonds from `_struct_conn` in mmCIF files that use `label_seq_id`
- Fix measurement label `offsetZ` default: not needed when `scaleByRadius` is enbaled
- Support for label rendering in HeadlessPluginContext
- MolViewSpec extension
- Support all X11 colors
- Support relative URIs
- CLI tools: mvs-validate, mvs-render, mvs-print-schema
- Labels applied in one node
- ModelServer SDF/MOL2 ligand export: fix atom indices when additional atoms are present
- Avoid showing (and calculating) inter-unit bonds for huge structures
- Fixed `DragOverlay` on WebKit/Safari browsers
## [v3.43.1] - 2023-12-04
- Fix `react-markdown` dependency
## [v3.43.0] - 2023-12-02
- Fix `State.tryGetCellData` (return type & data check)
- Don't change camera.target unless flyMode or pointerLock are enabled
- Handle empty CIF files
- Snapshot improvements:
- Add `key` property
- Ability to existing snapshot name, key, and description
- Support markdown in descriptions (ignores all HTML tags)
- Ability to link to snapshots by key from descriptions
- Separate UI control showing description of the current snapshot
- Do not activate drag overlay for non-file content
- Add `structure-element-sphere` visual to `spacefill` representation
- Fix missing `await` in `HeadlessPluginContext.saveStateSnapshot`
- Added support for providing custom sequence viewers to the plugin spec
- MolViewSpec extension (MVS)
- Add URL parameters `mvs-url`, `mvs-data`, `mvs-format`
- Add drag&drop for `.mvsj` files
- Fix `bumpiness` scaling with `ignoreLight` enabled
- Add `transforms` & `label` params to `ShapeFromPly`
- Optimize `LociSelectManager.selectOnly` to avoid superfluous loci set operations
- Dispose of viewer on `unload` event to aid GC
## [v3.42.0] - 2023-11-05
- Fix handling of PDB files with insertion codes (#945)
- Fix de-/saturate of colors with no hue
- Improve `distinctColors` function
- Add `sort` and `sampleCountFactor` parameters
- Fix clustering issues
- Add `clipPrimitive` option to spheres geometry, clipping whole spheres instead of cutting them
- Add `DragAndDropManager`
- Add `options` support for default bond labels
## [v3.41.0] - 2023-10-15
- Add `PluginContext.initialized` promise & support for it in the `Plugin` UI component.
- Fix undesired interaction between settings panel and the panel on the right.
- Add ability to customize server parameters for `RCSBAssemblySymmetry`.
## [v3.40.1] - 2023-09-30
- Do not call `updateFocusRepr` if default `StructureFocusRepresentation` isn't present.
- Treat "tap" as a click in `InputObserver`
- ModelServer ligand queries: fix atom count reported by SDF/MOL/MOL2 export
- CCD extension: Make visuals for aromatic bonds configurable
- Add optional `file?: CifFile` to `MmcifFormat.data`
- Add support for webgl extensions
- `WEBGL_clip_cull_distance`
- `EXT_conservative_depth`
- `WEBGL_stencil_texturing`
- `EXT_clip_control`
- Add `MultiSampleParams.reduceFlicker` (to be able to switch it off)
- Add `alphaThickness` parameter to adjust alpha of spheres for radius
- Ability to hide "right" panel from simplified viewport controls
- Add `blockIndex` parameter to TrajectoryFromMmCif
- Fix bounding sphere calculation for "element-like" visuals
- Fix RCSB PDB validation report URL
- Add sharpening postprocessing option
- Take pixel-ratio into account for outline scale
- Gracefully handle missing HTMLImageElement
- Fix pixel-ratio changes not applied to all render passes
## [v3.39.0] - 2023-09-02
- Add some elements support for `guessElementSymbolString` function
- Faster bounding rectangle calculation for imposter spheres
- Allow toggling of hydrogens as part of `LabelTextVisual`
## [v3.38.3] - 2023-07-29
- Fix imposter spheres not updating, e.g. in trajectories (broke in v3.38.0)
## [v3.38.2] - 2023-07-24
- Don't rely solely on `chem_comp_atom` when detecting CCD files (#877)
- Actually support non-physical keys in `Bindings.Trigger.code`
## [v3.38.1] - 2023-07-22
- Fix pixel-scale not updated in SSAO pass
## [v3.38.0] - 2023-07-18
- Fix display issue with SIFTS mapping
- Support non-physical keys in `Bindings.Trigger.code`
- Update `getStateSnapshot` to only overwrite current snapshot if it was created automatically
- Fix distinct palette's `getSamples` infinite loop
- Add 'NH2', 'FOR', 'FMT' to `CommonProteinCaps`
- Add `opened` event to `PluginStateSnapshotManager`
- Properly switch-off fog
- Add `approximate` option for spheres rendering
- Reduce `Spheres` memory usage
- Derive mapping from VertexID
- Pull position and group from texture
- Add `Euler` math primitive
- Add stride option to element sphere & point visuals
- Add `disabledExtensions` field to default viewer's options
- Add `LRUCache.remove`
- Add 'Chain Instance' and 'Uniform' options for 'Carbon Color' param (in Color Theme: Element Symbol)
## [v3.37.1] - 2023-06-20
- Fix issues with wboit/dpoit in large scenes
- Fix lines, text, points rendering (broken in v3.37.0)
## [v3.37.0] - 2023-06-17
- Add `inverted` option to `xrayShaded` parameter
- Model-export extension: Add ability to set a file name for structures
- Add `contextHash` to `SizeTheme`
- Add mipmap-based blur for image backgrounds
## [v3.36.1] - 2023-06-11
- Allow parsing of CCD ligand files
- Add dedicated wwPDB CCD extension to align and visualize ideal & model CCD coordinates
- Make operators in `IndexPairBonds` a directed property
- Remove erroneous bounding-box overlap test in `Structure.eachUnitPair`
- Fix `EdgeBuilder.addNextEdge` for loop edges
- Optimize inter unit bond compute
- Ensure consistent state for volume representation (#210)
- Improve SSAO for thin geometry (e.g. lines)
- Add snapshot support for structure selections
- Add `nucleicProfile` parameter to cartoon representation
- Add `cartoon` theme with separate colorings for for mainchain and sidechain visuals
## [v3.35.0] - 2023-05-14
- Enable odd dash count (1,3,5)
- Add principal axes spec and fix edge cases
- Add a uniform color theme for NtC tube that still paints residue and segment dividers in a different color
- Mesh exporter improvements
- Support points & lines in glTF export
- Set alphaMode and doubleSided in glTF export
- Fix flipped cylinder caps
- Fix bond assignments `struct_conn` records referencing waters
- Add StructConn extension providing functions for inspecting struct_conns
- Fix `PluginState.setSnapshot` triggering unnecessary state updates
- Fix an edge case in the `mol-state`'s `State` when trying to apply a transform to an existing Null object
- Add `SbNcbrPartialCharges` extension for coloring and labeling atoms and residues by partial atomic charges
- uses custom mmcif categories `_sb_ncbr_partial_atomic_charges_meta` and `_sb_ncbr_partial_atomic_charges` (more info in [README.md](./src/extensions/sb-ncbr/README.md))
- Parse HEADER record when reading PDB file
- Support `ignoreHydrogens` in interactions representation
- Add hydroxyproline (HYP) commonly present in collagen molecules to the list of amino acids
- Fix assemblies for Archive PDB files (do not generate unique `label_asym_id` if `REMARK 350` is present)
- Add additional functions to `core.math` in `mol-script`
- `cantorPairing`, `sortedCantorPairing`, `invertCantorPairing`,
- `trunc`, `sign`
## [v3.34.0] - 2023-04-16
- Avoid `renderMarkingDepth` for fully transparent renderables
- Remove `camera.far` doubling workaround
- Add `ModifiersKeys.areNone` helper function
- Do not render NtC tube segments unless all required atoms are present in the structure
- Fix rendering issues caused by VAO reuse
- Add "Zoom All", "Orient Axes", "Reset Axes" buttons to the "Reset Camera" button
- Improve trackball move-state handling when key bindings use modifiers
- Fix rendering with very small viewport and SSAO enabled
- Fix `.getAllLoci` for structure representations with `structure.child`
- Fix `readAllLinesAsync` refering to dom length property
- Make mol-util/file-info node compatible
- Add `eachLocation` to representation/visual interface
## [v3.33.0] - 2023-04-02
- Handle resizes of viewer element even when window remains the same size
- Throttle canvas resize events
- Selection toggle buttons hidden if selection mode is off
- Camera focus loci bindings allow reset on click-away to be overridden
- Input/controls improvements
- Move or fly around the scene using keys
- Pointer lock to look around scene
- Toggle spin/rock animation using keys
- Apply bumpiness as lightness variation with `ignoreLight`
- Remove `JSX` reference from `loci-labels.ts`
- Fix overpaint/transparency/substance smoothing not updated when geometry changes
- Fix camera project/unproject when using offset viewport
- Add support for loading all blocks from a mmcif file as a trajectory
- Add `Frustum3D` and `Plane3D` math primitives
- Include `occupancy` and `B_iso_or_equiv` when creating `Conformation` from `Model`
- Remove LazyImports (introduced in v3.31.1)
## [v3.32.0] - 2023-03-20
- Avoid rendering of fully transparent renderables
- Add occlusion color parameter
- Fix issue with outlines and orthographic camera
- Reduce over-blurring occlusion at larger view distances
- Fix occlusion artefact with non-canvas viewport and pixel-ratio > 1
- Update nodejs-shims conditionals to handle polyfilled document object in NodeJS environment.
- Ensure marking edges are at least one pixel wide
- Add exposure parameter to renderer
- Only trigger marking when mouse is directly over canvas
- Fix blurry occlusion in screenshots
- [Breaking] Add `setFSModule` to `mol-util/data-source` instead of trying to trick WebPack
## [v3.31.4] - 2023-02-24
- Allow link cylinder/line `dashCount` set to '0'
- Stop animation loop when disposing `PluginContext` (thanks @gfrn for identifying the issue)
## [v3.31.3] - 2023-02-22
- Fix impostor bond visuals not correctly updating on `sizeFactor` changes
- Fix degenerate case in PCA
- Fix near clipping avoidance in impostor shaders
- Update `fs` import in `data-source.ts`
## [v3.31.2] - 2023-02-12
- Fix exit code of volume pack executable (pack.ts). Now exits with non-0 status when an error happens
- Remove pca transform from components ui focus (too distracting)
- Fix artefacts with opaque outlines behind transparent objects
- Fix polymer trace visual not updating
- Fix use of `WEBGL_provoking_vertex`
## [v3.31.1] - 2023-02-05
- Improve Component camera focus based on the PCA of the structure and the following rules:
- The first residue should be in first quadrant if there is only one chain
- The average position of the residues of the first chain should be in the first quadrant if there is more than one chain
- Add `HeadlessPluginContext` and `HeadlessScreenshotHelper` to be used in Node.js
- Add example `image-renderer`
- Fix wrong offset when rendering text with orthographic projection
- Update camera/handle helper when `devicePixelRatio` changes
- Add various options to customize the axes camera-helper
- Fix issue with texture-mesh color smoothing when changing themes
- Add fast boundary helper and corresponding unit trait
- Add Observable for Canvas3D commits
## [v3.30.0] - 2023-01-29
- Improve `Dnatco` extension
- Factor out common code in `Dnatco` extension
- Add `NtC tube` visual. Applicable for structures with NtC annotation
- [Breaking] Rename `DnatcoConfalPyramids` to `DnatcoNtCs`
- Improve boundary calculation performance
- Add option to create & include images in state snapshots
- Fix SSAO artefacts with high bias values
- Fix SSAO resolution scale parameter handling
- Improve outlines, visually more stable at different view distances
## [v3.29.0] - 2023-01-15
- `meshes` extension: Fixed a bug in mesh visualization (show backfaces when opacity < 1)
- Add color quick select control to Volume controls
- Fix `dropFiles` bug
- Fix some cyclic imports and reduce the use of const enums. This should make it easier to use the library with the `isolatedModules: true` TS config.
- Fix `dropFiles` bug (#679)
- Add `input type='color'` picker to `CombinedColorControl`
- Set `ParameterMappingControl` disabled when state is updating
- Performance tweaks
- Update clip `defines` only when changed
- Check for identity in structure/unit areEqual methods
- Avoid cloning of structure representation parameters
- Make SymmetryOperator.createMapping monomorphic
- Improve bonding-sphere calculation
- Defer Scene properties calculation (markerAverage, opacityAverage, hasOpaque)
- Improve checks in in UnitsRepresentation setVisualState
- Add StructureElement.Loci.forEachLocation
- Add RepresentationRegistry.clear and ThemeRegistry.clear
- Add generic Loci support for overpaint, substance, clipping themes
- Add `.getCenter` and `.center` to `Camera`
- Add support to dim unmarked groups
- Add support for marker edge strength
## [v3.28.0] - 2022-12-20
- Show histogram in direct volume control point settings
- Add `solidInterior` parameter to sphere/cylinder impostors
- [Breaking] Tweak `ignoreHydrogens` non-polar handling (introduced in 3.27.0)
- Add `meshes` and `volumes-and-segmentations` extensions
- See https://molstarvolseg.ncbr.muni.cz/ for more info
- Fix missing support for info in `ParamDefinition.Converted`
- Add support for multi-visual volume representations
- Improve volume isosurface bounding-sphere
- Add basic volume segmentation support to core
- Add `Volume.Segment` model
- Add `Segmentation` custom volume property
- Add `SegmentRepresentation` representation
- Add `volume-segment` color theme
- Fix GPU marching cubes failing for large meshes with webgl2 (due to use of float16)
## [v3.27.0] - 2022-12-15
- Add an `includeTransparent` parameter to hide/show outlines of components that are transparent
- Fix 'once' for animations of systems with many frames
- Better guard against issue (black fringes) with bumpiness in impostors
- Improve impostor shaders
- Fix sphere near-clipping with orthographic projection
- Fix cylinder near-clipping
- Add interior cylinder caps
- Add per-pixel object clipping
- Fix `QualityAssessment` assignment bug for structures with different auth vs label sequence numbering
- Refresh `ApplyActionControl`'s param definition when toggling expanded state
- Fix `struct_conn` bond assignment for ions
- Ability to show only polar hydrogens
## [v3.26.0] - 2022-12-04
- Support for ``powerPreference`` webgl attribute. Add ``PluginConfig.General.PowerPreference`` and ``power-preference`` Viewer GET param.
- Excluded common protein caps `NME` and `ACE` from the ligand selection query
- Add screen-space shadow post-processing effect
- Add "Structure Molecular Surface" visual
- Add `external-volume` theme (coloring of arbitrary geometries by user-selected volume)
## [v3.25.1] - 2022-11-20
- Fix edge-case in `Structure.eachUnitPair` with single-element units
- Fix 'auto' structure-quality for coarse models
## [v3.25.0] - 2022-11-16
- Fix handling of gzipped assets (reverts #615)
## [v3.24.0] - 2022-11-13
- Make `PluginContext.initContainer` checkered canvas background optional
- Store URL of downloaded assets to detect zip/gzip based on extension (#615)
- Add optional `operator.key`; can be referenced in `IndexPairBonds`
- Add overpaint/transparency/substance theme strength to representations
- Fix viewport color for transparent background
## [v3.23.0] - 2022-10-19
- Add `PluginContext.initContainer/mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI
- Add `PluginContext.canvas3dInitialized`
- `createPluginUI` now resolves after the 3d canvas has been initialized
- Change EM Volume Streaming default from `Whole Structure` to `Auto`
- Change EM Volume Streaming default from `Whote Structure` to `Auto`
## [v3.22.0] - 2022-10-17
- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality`
- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality`
## [v3.21.0] - 2022-10-17

View File

@@ -1,6 +1,6 @@
[![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](./LICENSE)
[![npm version](https://badge.fury.io/js/molstar.svg)](https://www.npmjs.com/package/molstar)
[![Build](https://github.com/molstar/molstar/actions/workflows/node.yml/badge.svg)](https://github.com/molstar/molstar/actions/workflows/node.yml)
[![Build Status](https://travis-ci.org/molstar/molstar.svg?branch=master)](https://travis-ci.org/molstar/molstar)
[![Gitter](https://badges.gitter.im/molstar/Lobby.svg)](https://gitter.im/molstar/Lobby)
# Mol*
@@ -124,6 +124,10 @@ and navigate to `build/viewer`
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
**GraphQL schemas**
node node_modules/@graphql-codegen/cli/cjs/bin -c src/extensions/rcsb/graphql/codegen.yml
### Other scripts
**Create chem comp bond table**
@@ -163,12 +167,13 @@ If node complains about a missing acorn peer dependency, run the following comma
### Editor
To get syntax highlighting for shader files add the following to Visual Code's settings files and make sure relevant extensions are installed in the editor.
To get syntax highlighting for shader and graphql files add the following to Visual Code's settings files and make sure relevant extensions are installed in the editor.
"files.associations": {
"*.glsl.ts": "glsl",
"*.frag.ts": "glsl",
"*.vert.ts": "glsl"
"*.vert.ts": "glsl",
"*.gql.ts": "graphql"
},
## Publish

View File

@@ -1,86 +0,0 @@
# MolViewSpec extension in Mol\*
**MolViewSpec (MVS)** is a toolkit for standardized description of reproducible molecular visualizations shareable across software applications. MolViewSpec describes a 3D molecular scene by a MolViewSpec State, a viewer-independent representation that contains all information necessary to reproduce the scene. Thanks to its nested tree-based format, complex scenes can be composed from simple building blocks.
MolViewSpec documentation is available from <https://molstar.org/docs/extensions/mvs/>.
MolViewSpec extension in Mol\* provides functionality for building, validating, and visualizing MolViewSpec States.
## Graphical user interface
- **Drag&drop support:** The easiest way to load a MVS view into Mol* Viewer is to drag a `.mvsj` or `.mvsx` file and drop it in a browser window with Mol* Viewer.
- **Load via menu:** Another way to load a MVS view is to use "Download File" or "Open Files" action, available in the "Home" tab in the left panel. For these actions, the "Format" parameter must be set to "MVSJ" or "MVSX" (in the "Miscellaneous" category) or "Auto".
- **URL parameters:** Mol\* Viewer supports `mvs-url`, `mvs-data`, and `mvs-format` URL parameters to specify a MVS view to be loaded when the viewer is initialized.
- `mvs-url` specifies the address from which the MVS view should be retrieved.
- `mvs-data` specifies the MVS view data directly. Keep in mind that some characters must be escaped to be used in the URL. Also beware that URLs longer than 2000 character may not work in all browsers. Because of these limitations, the preferred method it to host the data somewhere and use `mvs-url` instead.
- `mvs-format` specifies the format of the MVS view data from `mvs-url` or `mvs-data`. Allowed values are `mvsj` and `mvsx` (default is `mvsj`).
Examples of URL parameter usage:
- https://molstar.org/viewer/?mvs-format=mvsj&mvs-url=https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj
- https://molstar.org/viewer/?mvs-format=mvsx&mvs-url=https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1h9t.mvsx
- https://molstar.org/viewer/?mvs-format=mvsj&mvs-data=%7B%22metadata%22%3A%7B%22title%22%3A%22Example%20MolViewSpec%20-%201cbs%20with%20labelled%20protein%20and%20ligand%22%2C%22version%22%3A%221%22%2C%22timestamp%22%3A%222023-11-24T10%3A38%3A17.483%22%7D%2C%22root%22%3A%7B%22kind%22%3A%22root%22%2C%22children%22%3A%5B%7B%22kind%22%3A%22download%22%2C%22params%22%3A%7B%22url%22%3A%22https%3A//www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22parse%22%2C%22params%22%3A%7B%22format%22%3A%22bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22structure%22%2C%22params%22%3A%7B%22type%22%3A%22model%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22polymer%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22cartoon%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22green%22%7D%7D%2C%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22selector%22%3A%7B%22label_asym_id%22%3A%22A%22%2C%22beg_label_seq_id%22%3A1%2C%22end_label_seq_id%22%3A50%7D%2C%22color%22%3A%22%236688ff%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Protein%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22ligand%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22ball_and_stick%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22%23cc3399%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Retinoic%20Acid%22%7D%7D%5D%7D%5D%7D%5D%7D%5D%7D%2C%7B%22kind%22%3A%22canvas%22%2C%22params%22%3A%7B%22background_color%22%3A%22%23ffffee%22%7D%7D%2C%7B%22kind%22%3A%22camera%22%2C%22params%22%3A%7B%22target%22%3A%5B17%2C21%2C27%5D%2C%22position%22%3A%5B41%2C34%2C69%5D%2C%22up%22%3A%5B-0.129%2C0.966%2C-0.224%5D%7D%7D%5D%7D%7D
- https://molstar.org/viewer/?mvs-format=mvsx&mvs-data=base64,UEsDBBQAAAAIADSFPlhDx8RXYwEAAGwFAAAUABwAYW5ub3RhdGlvbnMtMWg5dC5jaWZVVAkAA8MmuWVuvbtldXgLAAEE9gEAAAQUAAAAlZJNj4IwEIbv/RVNPPSkacGPckT8TDabPe6emkq72ARbF9iD/34LFBV1FSbh7YTh4Z1hELzgjOyDgnGtTcELZXQOQGrMkQEWm8PRaKmLfJTynUwZz08HpkSrck5BCD8yU0ilwfycRXDxHoJFpUu4NqkAm/pYwWifmkwJCdaXdHtJmy6uOrtt47q0kwmry7n8uatKLZ5UY2M9Hzi1byWZ+T2WbAhtBPaiPoRoIMR0ijGyqZ1yuFNaKJ0geI5RBdAxhJ5PSgBjSmsgjE/pMDIhbFEj+wFbDnHcAP85zO8dKH3mEFWHEzQgBGPOnQP8vHp347BoA6KKBvh6ACzbwLeNeoZy/XfPW2DTF1i1AYyFqIHmH0I3wLovsO0LlJuezJygTDZ9ozeVcC2aLcBcFSVLKmA6c9IRIB5x8hrwamDipDMQOOkKzKiT14BfAh72S+nY0rzf0OMa6D60A7oPXQF9ZhiDP1BLAwQUAAAACACagEFYhgn8CJECAAAbFQAACgAcAGluZGV4Lm12c2pVVAkAAyTBu2UlwbtldXgLAAEE9gEAAAQUAAAA7VjBjtowEL3vV0S5tiSBFnXhttpKPbWq1KqXqoqMPWwsHDuynaVoxb/XNhAIiUOEtK26m5zIzLzJG8+8gfB0EwShFEKH8+DJfDZ3K8qJudtZ3+5sOKOMSODG/tNZgn10DUHEmjOByB7lfAWSKFdV9r21lMwCMq0LNY/j9XodwYJGCEflKi7IAmLgWm5GS8pAxYe88Tib6bQsCNJAIkyXYZVze/LIFrJ1wjXShqCCE8Z+1s6zFDJH9rTCPK8xOGPRxaTJpsZIaVliXcpzVt3MnFdvCnDcBAEWNiK2LQk7OLbzrHHFIi8EN81Kl1LkaSlpC+nLxF2EBZuUUYw4FxppKrga2Za7VrenrXWkMwyboXkQcpNylEONuurITYGRJsIPWDCBV2kGiIC0EDexJ/X4kQpnkCP3mAxRfonTI2IlKE/T9pFfpdBgUnkifrXaW2bEJbswJ/bydfZ0qUAhQZkjdMfhLbLPvLiow8BjJLUQ3lK9ZbkkPUqzVxeRmiSYkJfkUKF6lekir5NHBe8nkyq8IZc+Q3x82JluzIlcBl2tnSrDUUNmyCgpIZWIP4B/Kuy17fD6fe3i8SHaolsn8nWv22fYgR+/3L2G/bdAjBmtkFRpilfDGvTC/+oaHPZRhRj20S7yk2CkS/b3mZENJd4ZeUlLS5VyiXCHHIZtNWwrT/Q124qhBbD/b1P1en2s/+J2lT7vW2qPGWnr5jV9M292TNNi6Ny/7VzDdq7LesSp9+g5WHcWe+cmIsxBI4I0Ov4P+QhS2a8bwziJxod/IgkoLGmh9547HsBvlBcMgjXVWfD5x7egeRyhpjkobeIsZpJM3o+SySgZfx+P59Pb+btZNPtwO5lO3yTJPEnskWxvtn8AUEsBAh4DFAAAAAgANIU+WEPHxFdjAQAAbAUAABQAGAAAAAAAAQAAAKSBAAAAAGFubm90YXRpb25zLTFoOXQuY2lmVVQFAAPDJrlldXgLAAEE9gEAAAQUAAAAUEsBAh4DFAAAAAgAmoBBWIYJ/AiRAgAAGxUAAAoAGAAAAAAAAQAAAKSBsQEAAGluZGV4Lm12c2pVVAUAAyTBu2V1eAsAAQT2AQAABBQAAABQSwUGAAAAAAIAAgCqAAAAhgQAAAAA
## Programming interface
Most functions for manipulation of MVS data (including parsing, encoding, validating, and building) are provided by the `MVSData` object (defined in [src/extensions/mvs/mvs-data.ts](/src/extensions/mvs/mvs-data.ts)). In TypeScript, `MVSData` is also the type for a MVS view.
The `loadMVS` function (defined in [src/extensions/mvs/load.ts](/src/extensions/mvs/load.ts)) can be used to load MVS view data into Mol\* Viewer.
Example usage:
```ts
// Fetch a MVS, validate, and load
const response = await fetch('https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj');
const rawData = await response.text();
const mvsData: MVSData = MVSData.fromMVSJ(rawData);
if (!MVSData.isValid(mvsData)) throw new Error(`Oh no: ${MVSData.validationIssues(mvsData)}`);
await loadMVS(this.plugin, mvsData, { replaceExisting: true });
console.log('Loaded this:', MVSData.toPrettyString(mvsData));
console.log('Loaded this:', MVSData.toMVSJ(mvsData));
// Build a MVS and load
const builder = MVSData.createBuilder();
const structure = builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og2_updated.cif' }).parse({ format: 'mmcif' }).modelStructure();
structure.component({ selector: 'polymer' }).representation({ type: 'cartoon' });
structure.component({ selector: 'ligand' }).representation({ type: 'ball_and_stick' }).color({ color: '#aa55ff' });
const mvsData2: MVSData = builder.getState();
await loadMVS(this.plugin, mvsData2, { replaceExisting: false });
```
When using the pre-built Mol\* plugin bundle, `MVSData` and `loadMVS` are exposed as `molstar.PluginExtensions.mvs.MVSData` and `molstar.PluginExtensions.mvs.loadMVS`. Furthermore, the `molstar.Viewer` class has `loadMvsFromUrl` and `loadMvsData` methods, providing the same functionality as `mvs-url` and `mvs-data` URL parameters. See the [integration examples](./integration-examples.html) page for a demonstration.
## Command-line utilities
The MVS extension in Mol\* provides a few command-line utilities, which can be executed via NodeJS:
- `mvs-validate` provides validation of MolViewSpec files
- `mvs-render` creates images based on MolViewSpec files
- `mvs-print-schema` prints MolViewSpec tree schema (i.e. currently supported node types and their parameters)
Example usage:
```sh
# Clone Mol* repo, install, and build
git clone https://github.com/molstar/molstar.git
cd molstar/
npm install && npm run build
# Validate a MolViewSpec file `examples/mvs/1cbs.mvsj`
node lib/commonjs/cli/mvs/mvs-validate.js examples/mvs/1cbs.mvsj
# Render a MolViewSpec file `examples/mvs/1cbs.mvsj` to `../outputs/1cbs.png`
node lib/commonjs/cli/mvs/mvs-render.js -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
# Print MolViewSpec tree schema formatted as markdown
node lib/commonjs/cli/mvs/mvs-print-schema.js --markdown
```
(An alternative to cloning the GitHub repository is to install Mol\* package from npm by `npm install molstar canvas gl jpeg-js pngjs`. Then you can type `npx mvs-validate ...` instead of `node lib/commonjs/cli/mvs/mvs-validate.js ...`)

View File

@@ -1,112 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Replace "latest" by the specific version you want to use, e.g. "4.0.0" -->
<script src="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.js"></script>
<!-- Replace "latest" by the specific version you want to use, e.g. "4.0.0" -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.css" />
</head>
<body>
<h1>Integration of Mol* with MolViewSpec Extension</h1>
<p>
This page demonstrates several methods to integrate Mol* Viewer in a web page and use MolViewSpec functionality.
See the source HTML to see the actual code.
</p>
<h2>Method 1: Get MVS view from a server and pass to the viewer</h2>
<p>
The recommended method is to serve the MVS view files by your server (either as static files or generated by the
server on-demand) and call the <code>loadMvsFromUrl</code> method to retrieve and load them.
This example uses a MVS view file from the address specified in the <code>sourceUrl</code> variable.
If the MVS view file contains relative references, they will be resolved as relative to <code>sourceUrl</code>.
</p>
<div id="viewer1" style="position: relative; width: 500px; height: 500px;"></div>
<script>
const sourceUrl = 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1h9t_domain_labels.mvsj';
molstar.Viewer.create('viewer1', { layoutIsExpanded: false, layoutShowControls: false })
.then(viewer => viewer.loadMvsFromUrl(sourceUrl, 'mvsj'));
</script>
<p>
A variation of this method uses <code>molstar.PluginExtensions.mvs.loadMVS</code> instead of
<code>loadMvsFromUrl</code> and allows replacing the MVS view after it has been loaded.
</p>
<div id="viewer1b" style="position: relative; width: 500px; height: 500px;"></div>
<button onclick="loadView1();">View 1</button>
<button onclick="loadView2();">View 2</button>
<script>
let theViewer;
function load(viewer, url, replace) {
fetch(url)
.then(response => response.text())
.then(text => molstar.PluginExtensions.mvs.MVSData.fromMVSJ(text))
.then(mvsData => molstar.PluginExtensions.mvs.loadMVS(viewer.plugin, mvsData, { sourceUrl: url, sanityChecks: true, replaceExisting: replace }));
}
function loadView1() {
load(theViewer, 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj', true);
}
function loadView2() {
load(theViewer, 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs-focus.mvsj', true);
}
molstar.Viewer.create('viewer1b', { layoutIsExpanded: false, layoutShowControls: false })
.then(viewer => {
theViewer = viewer;
loadView1();
});
</script>
<h2>Method 2: Construct MVS view on frontend and pass to the viewer</h2>
<p>
Another option is to utilize the MVS builder provided by the extension to build the view on frontend and then
pass it to the viewer. This example builds the view in plain JavaScript, directly in a &lt;script&gt; tag in
HTML. However, for a better developer experience consider writing the code in TypeScript.
If the built MVS view contains relative references, they will be resolved as relative to the URL of this HTML
page.
</p>
<div id="viewer2" style="position: relative; width: 500px; height: 500px;"></div>
<script>
// Build an ad-hoc MVS view
const builder = molstar.PluginExtensions.mvs.MVSData.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' })
.label({ text: 'Retinoic acid' })
.focus({})
.representation({ type: 'ball_and_stick' })
.color({ color: '#cc3399' });
const mvsData = builder.getState();
// Initialize viewer and load MVSJ
const mvsj = molstar.PluginExtensions.mvs.MVSData.toMVSJ(mvsData);
molstar.Viewer.create('viewer2', { layoutIsExpanded: false, layoutShowControls: false })
.then(viewer => viewer.loadMvsData(mvsj, 'mvsj'));
// // Alternative initialization and loading (avoids encoding and again decoding the data, allows changing the view by using `replaceExisting: true`):
// molstar.Viewer.create('viewer2', { layoutIsExpanded: false, layoutShowControls: false })
// .then(viewer => molstar.PluginExtensions.mvs.loadMVS(viewer.plugin, mvsData, { sourceUrl: undefined, sanityChecks: true, replaceExisting: false }));
</script>
<p>
Again, there is variation with using <code>molstar.PluginExtensions.mvs.loadMVS</code> instead of
<code>loadMvsData</code>.
</p>
</body>
</html>

View File

@@ -1,118 +0,0 @@
# wwPDB StructConn extension
The STRUCT_CONN category in the mmCIF file format contains details about the connections between portions of the structure. These can be hydrogen bonds, salt bridges, disulfide bridges and so on (see more at <https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v40.dic/Categories/struct_conn.html>).
**wwPDB StructConn extension** in Mol* provides functionality to retrieve and visualize these connections.
The extension exposes three functions, located in `src/extensions/wwpdb/struct-conn/index.ts`.
- `getStructConns` - to retrieve struct_conn records from a loaded structure
- `inspectStructConn` - to visualize a struct_conn
- `clearStructConnInspections` - to remove visulizations created by `inspectStructConn`
## Example 1
The following example is a minimal HTML using this functionality:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer</title>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<body style="margin: 0px;">
<div style="position: absolute; width: 100%; height: 10%; padding-block: 10px;">
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf1');">disulf1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf2');">disulf2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale1');">covale1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale2');">covale2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale3');">covale3</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale4');">covale4</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc1');">metalc1</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc2');">metalc2</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc3');">metalc3</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc4');">metalc4</button>
<button onclick="molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, '5elb');">CLEAR</button>
</div>
<div id="app" style="position: absolute; top: 10%; width: 100%; height: 90%;"></div>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
var molstarViewer;
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
molstarViewer = viewer;
viewer.loadPdb('5elb');
});
</script>
</body>
</html>
```
The PDB ID (`'5elb'`) can be replaced be `undefined`, in which case the functions will apply to the first loaded structure.
## Example 2
This is a more elaborated example, which automatically loads `5elb` (or any PDB entry given in the URL after `?pdb=`), retrieves the list of struct_conns, and creates a button for each struct_conn.
Be aware that some of the struct_conns may be present in the deposited model but not in the preferred assembly (default view). The presented example will raise a dialog window with error message in such cases, e.g. `disulf6` in entry `5elb`.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<title>Mol* Viewer - StructConn Extension Demo</title>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<style>
body { margin: 0px; }
#app { position: absolute; width: 85%; height: 100%; }
#controls { position: absolute; right: 0; width: 15%; height: 100%; display: flex; flex-direction: column; overflow-y: scroll; }
h1 { text-align: center; margin: 12px; font-weight: bold; font-size: 120%; }
button { margin: 4px; margin-top: 0px; }
</style>
<body>
<div id="app"></div>
<div id="controls">
<h1 id="pdb-id">Loading...</h1>
<button onclick="clearInspections();">CLEAR</button>
</div>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
var pdbId = window.location.search.match(/[?&]pdb=(\w+)/i)?.[1]?.toLowerCase() ?? '5elb';
var molstarViewer;
function inspect(structConnId) {
if (molstarViewer?.plugin) {
molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, pdbId, structConnId).then(nSelectedAtoms => {
if (nSelectedAtoms < 2) alert('Some of the interacting atoms were not found :(\n(maybe not present in the viewed assembly)');
});
}
}
function clearInspections() {
if (molstarViewer?.plugin) {
molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, pdbId);
}
}
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
molstarViewer = viewer;
return viewer.loadPdb(pdbId);
}).then(() => {
const structConns = molstar.PluginExtensions.wwPDBStructConn.getStructConns(molstarViewer.plugin, pdbId);
const controls = document.getElementById('controls');
for (const structConnId in structConns) {
const button = document.createElement('button');
button.innerText = structConnId;
button.addEventListener('click', () => inspect(structConnId));
controls.appendChild(button);
};
document.getElementById('pdb-id').innerHTML = pdbId;
});
</script>
</body>
</html>
```

View File

@@ -26,7 +26,6 @@
* Non-standard residues
* Protein (1BRR, 5Z6Y)
* DNA (5D3G)
* Collagen (6JEC)
* Multiple models with different sets of ligands or missing ligands (1J6T, 1VRC, 2ICY, 1O2F)
* Long linear sugar chain (4HG6)
* Anisotropic B-factors/Ellipsoids (1EJG)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +0,0 @@
{
"metadata": {
"title": "Example MolViewSpec - 1cbs with labelled and zoomed ligand",
"version": "1",
"timestamp": "2023-11-24T10:45:49.873Z"
},
"root": {
"kind": "root",
"children": [
{
"kind": "download",
"params": {
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif"
},
"children": [
{
"kind": "parse",
"params": {
"format": "bcif"
},
"children": [
{
"kind": "structure",
"params": {
"type": "model"
},
"children": [
{
"kind": "component",
"params": {
"selector": "polymer"
},
"children": [
{
"kind": "representation",
"params": {
"type": "cartoon"
},
"children": [
{
"kind": "color",
"params": {
"color": "green"
}
},
{
"kind": "color",
"params": {
"selector": {
"label_asym_id": "A",
"end_label_seq_id": 50
},
"color": "#6688ff"
}
}
]
},
{
"kind": "label",
"params": {
"text": "Protein"
}
}
]
},
{
"kind": "component",
"params": {
"selector": "ligand"
},
"children": [
{
"kind": "focus",
"params": {
"direction": [0.5, 0, -1],
"up": [0.365, 0.913, 0.183]
}
},
{
"kind": "representation",
"params": {
"type": "ball_and_stick"
},
"children": [
{
"kind": "color",
"params": {
"color": "#cc3399"
}
}
]
},
{
"kind": "label",
"params": {
"text": "Retinoic Acid"
}
}
]
}
]
}
]
}
]
},
{
"kind": "canvas",
"params": {
"background_color": "#ffffee"
}
}
]
}
}

View File

@@ -1,117 +0,0 @@
{
"metadata": {
"title": "Example MolViewSpec - 1cbs with labelled protein and ligand",
"version": "1",
"timestamp": "2023-11-24T10:38:17.483Z"
},
"root": {
"kind": "root",
"children": [
{
"kind": "download",
"params": {
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif"
},
"children": [
{
"kind": "parse",
"params": {
"format": "bcif"
},
"children": [
{
"kind": "structure",
"params": {
"type": "model"
},
"children": [
{
"kind": "component",
"params": {
"selector": "polymer"
},
"children": [
{
"kind": "representation",
"params": {
"type": "cartoon"
},
"children": [
{
"kind": "color",
"params": {
"color": "green"
}
},
{
"kind": "color",
"params": {
"selector": {
"label_asym_id": "A",
"beg_label_seq_id": 1,
"end_label_seq_id": 50
},
"color": "#6688ff"
}
}
]
},
{
"kind": "label",
"params": {
"text": "Protein"
}
}
]
},
{
"kind": "component",
"params": {
"selector": "ligand"
},
"children": [
{
"kind": "representation",
"params": {
"type": "ball_and_stick"
},
"children": [
{
"kind": "color",
"params": {
"color": "#cc3399"
}
}
]
},
{
"kind": "label",
"params": {
"text": "Retinoic Acid"
}
}
]
}
]
}
]
}
]
},
{
"kind": "canvas",
"params": {
"background_color": "#ffffee"
}
},
{
"kind": "camera",
"params": {
"target": [17, 21, 27],
"position": [41, 34, 69],
"up": [-0.129,0.966,-0.224]
}
}
]
}
}

Binary file not shown.

View File

@@ -1,67 +0,0 @@
{
"metadata": {
"title": "Example MolViewSpec - 1h9t colored by external annotation",
"version": "1",
"timestamp": "2023-11-24T10:47:33.182Z"
},
"root": {
"kind": "root",
"children": [
{
"kind": "download",
"params": {
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1h9t.bcif"
},
"children": [
{
"kind": "parse",
"params": {
"format": "bcif"
},
"children": [
{
"kind": "structure",
"params": {
"type": "model"
},
"children": [
{
"kind": "component",
"params": {
"selector": "polymer"
},
"children": [
{
"kind": "representation",
"params": {
"type": "cartoon"
},
"children": [
{
"kind": "color",
"params": {
"selector": "all",
"color": "white"
}
},
{
"kind": "color_from_uri",
"params": {
"uri": "./1h9t_domains.json",
"format": "json",
"schema": "all_atomic"
}
}
]
}
]
}
]
}
]
}
]
}
]
}
}

View File

@@ -1,583 +0,0 @@
{
"metadata": {
"title": "Example MolViewSpec - 1h9t colored and labelled by external annotation",
"version": "1",
"timestamp": "2023-11-24T10:48:28.677Z"
},
"root": {
"kind": "root",
"children": [
{
"kind": "download",
"params": {
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1h9t.bcif"
},
"children": [
{
"kind": "parse",
"params": {
"format": "bcif"
},
"children": [
{
"kind": "structure",
"params": {
"type": "model"
},
"children": [
{
"kind": "component",
"params": {
"selector": "protein"
},
"children": [
{
"kind": "representation",
"params": {
"type": "cartoon"
},
"children": [
{
"kind": "color",
"params": {
"selector": "all",
"color": "white"
}
},
{
"kind": "color_from_uri",
"params": {
"uri": "./1h9t_domains.json",
"format": "json",
"schema": "all_atomic"
}
}
]
}
]
},
{
"kind": "component",
"params": {
"selector": "nucleic"
},
"children": [
{
"kind": "representation",
"params": {
"type": "ball_and_stick"
},
"children": [
{
"kind": "color",
"params": {
"selector": "all",
"color": "white"
}
},
{
"kind": "color_from_uri",
"params": {
"uri": "./1h9t_domains.json",
"format": "json",
"schema": "all_atomic"
}
}
]
}
]
},
{
"kind": "component",
"params": {
"selector": "ion"
},
"children": [
{
"kind": "representation",
"params": {
"type": "surface"
},
"children": [
{
"kind": "color_from_uri",
"params": {
"uri": "./1h9t_domains.json",
"format": "json",
"schema": "all_atomic"
}
}
]
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"beg_label_seq_id": 9,
"end_label_seq_id": 83
}
},
"children": [
{
"kind": "label",
"params": {
"text": "DNA-binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"beg_label_seq_id": 9,
"end_label_seq_id": 83
}
},
"children": [
{
"kind": "label",
"params": {
"text": "DNA-binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"beg_label_seq_id": 84,
"end_label_seq_id": 231
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Acyl-CoA\nbinding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"beg_label_seq_id": 84,
"end_label_seq_id": 231
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Acyl-CoA binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "C"
}
},
"children": [
{
"kind": "label",
"params": {
"text": "DNA X"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "D"
}
},
"children": [
{
"kind": "label",
"params": {
"text": "DNA Y"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "D",
"atom_id": 4016
}
},
"children": [
{
"kind": "label",
"params": {
"text": "DNA Y O5'"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "D",
"atom_id": 4391
}
},
"children": [
{
"kind": "label",
"params": {
"text": "DNA Y O3'"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "E"
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Gold"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "H"
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Gold"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "F"
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Chloride"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "G"
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Chloride"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "I"
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Chloride"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"label_seq_id": 57
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"label_seq_id": 67
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"label_seq_id": 121
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"label_seq_id": 125
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"label_seq_id": 129
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"label_seq_id": 178
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "A",
"beg_label_seq_id": 203,
"end_label_seq_id": 205
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"label_seq_id": 67
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"label_seq_id": 121
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"label_seq_id": 125
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"label_seq_id": 129
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"label_seq_id": 178
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": {
"label_asym_id": "B",
"beg_label_seq_id": 203,
"end_label_seq_id": 205
}
},
"children": [
{
"kind": "label",
"params": {
"text": "Ligand binding"
}
}
]
},
{
"kind": "component",
"params": {
"selector": "all"
},
"children": [
{
"kind": "focus",
"params": {
"direction": [-0.3, -0.1, -1]
}
}
]
}
]
}
]
}
]
},
{
"kind": "canvas",
"params": {
"background_color": "#eeffee"
}
}
]
}
}

View File

@@ -1,155 +0,0 @@
[
{
"label_asym_id": "A",
"beg_label_seq_id": 9,
"end_label_seq_id": 83,
"color": "#dd6600",
"tooltip": "DNA-binding"
},
{
"label_asym_id": "A",
"beg_label_seq_id": 84,
"end_label_seq_id": 231,
"color": "#008800",
"tooltip": "Acyl-CoA binding"
},
{
"label_asym_id": "B",
"beg_label_seq_id": 9,
"end_label_seq_id": 83,
"color": "#cc8800",
"tooltip": "DNA-binding"
},
{
"label_asym_id": "B",
"beg_label_seq_id": 84,
"end_label_seq_id": 231,
"color": "#008888",
"tooltip": "Acyl-CoA binding"
},
{
"label_asym_id": "C",
"color": "#1100aa",
"tooltip": "DNA X"
},
{
"label_asym_id": "D",
"color": "#dddddd",
"tooltip": "DNA Y"
},
{
"label_asym_id": "D",
"atom_id": 4016,
"color": "#ff0044",
"tooltip": "DNA Y - O5'"
},
{
"label_asym_id": "D",
"atom_id": 4391,
"color": "#4400ff",
"tooltip": "DNA Y - O3'"
},
{
"label_asym_id": "E",
"color": "#ffff00",
"tooltip": "Gold"
},
{
"label_asym_id": "H",
"color": "#ffff00",
"tooltip": "Gold"
},
{
"label_asym_id": "F",
"color": "#00dd00",
"tooltip": "Chloride"
},
{
"label_asym_id": "G",
"color": "#00dd00",
"tooltip": "Chloride"
},
{
"label_asym_id": "I",
"color": "#00dd00",
"tooltip": "Chloride"
},
{
"label_asym_id": "A",
"label_seq_id": 57,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "A",
"label_seq_id": 67,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "A",
"label_seq_id": 121,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "A",
"label_seq_id": 125,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "A",
"label_seq_id": 129,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "A",
"label_seq_id": 178,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "A",
"beg_label_seq_id": 203,
"end_label_seq_id": 205,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "B",
"label_seq_id": 67,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "B",
"label_seq_id": 121,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "B",
"label_seq_id": 125,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "B",
"label_seq_id": 129,
"color": "#ff0000",
"tooltip": "Ligand binding site"
},
{
"label_asym_id": "B",
"beg_label_seq_id": 203,
"end_label_seq_id": 205,
"color": "#ff0000",
"tooltip": "Ligand binding site"
}
]

18235
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "4.1.0",
"version": "3.23.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -13,7 +13,7 @@
"scripts": {
"lint": "eslint .",
"lint-fix": "eslint . --fix",
"test": "npm install --no-save \"gl@^6.0.2\" && npm run lint && jest",
"test": "npm install --no-save \"gl@^5.0.0\" && npm run lint && jest",
"jest": "jest",
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
"clean": "node ./scripts/clean.js",
@@ -48,9 +48,6 @@
"bin": {
"cif2bcif": "lib/commonjs/cli/cif2bcif/index.js",
"cifschema": "lib/commonjs/cli/cifschema/index.js",
"mvs-validate": "lib/commonjs/cli/mvs/mvs-validate.js",
"mvs-render": "lib/commonjs/cli/mvs/mvs-render.js",
"mvs-print-schema": "lib/commonjs/cli/mvs/mvs-print-schema.js",
"model-server": "lib/commonjs/servers/model/server.js",
"model-server-query": "lib/commonjs/servers/model/query.js",
"model-server-preprocess": "lib/commonjs/servers/model/preprocess.js",
@@ -95,100 +92,76 @@
"Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
"Adam Midlik <midlik@gmail.com>",
"Koya Sakuma <koya.sakuma.work@gmail.com>",
"Gianluca Tomasello <giagitom@gmail.com>",
"Ke Ma <mark.ma@rcsb.org>",
"Jason Pattle <jpattle@exscientia.co.uk>",
"David Williams <dwilliams@nobiastx.com>",
"Zhenyu Zhang <jump2cn@gmail.com>",
"Russell Parker <russell@benchling.com>",
"Dominik Tichy <tichydominik451@gmail.com>",
"Yana Rose <yana.v.rose@gmail.com>",
"Yakov Pechersky <ffxen158@gmail.com>",
"Christian Dominguez <christian.99dominguez@gmail.com>",
"Cai Huiyu <szmun.caihy@gmail.com>"
"Gianluca Tomasello <giagitom@gmail.com>"
],
"license": "MIT",
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/gl": "^6.0.5",
"@types/pngjs": "^6.0.4",
"@types/jest": "^29.5.12",
"@types/react": "^18.2.73",
"@types/react-dom": "^18.2.23",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"@graphql-codegen/add": "^3.2.1",
"@graphql-codegen/cli": "^2.13.7",
"@graphql-codegen/time": "^3.2.1",
"@graphql-codegen/typescript": "^2.7.4",
"@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
"@graphql-codegen/typescript-graphql-request": "^4.5.6",
"@graphql-codegen/typescript-operations": "^2.5.4",
"@types/cors": "^2.8.12",
"@types/gl": "^4.1.1",
"@types/jest": "^29.1.2",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"benchmark": "^2.1.4",
"concurrently": "^8.2.2",
"cpx2": "^7.0.1",
"concurrently": "^7.4.0",
"cpx2": "^4.2.0",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.10.0",
"eslint": "^8.57.0",
"css-loader": "^6.7.1",
"eslint": "^8.25.0",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^6.2.0",
"fs-extra": "^11.2.0",
"fs-extra": "^10.1.0",
"graphql": "^16.6.0",
"http-server": "^14.1.1",
"jest": "^29.7.0",
"jpeg-js": "^0.4.4",
"mini-css-extract-plugin": "^2.8.1",
"jest": "^29.2.0",
"mini-css-extract-plugin": "^2.6.1",
"path-browserify": "^1.0.1",
"raw-loader": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.72.0",
"sass-loader": "^14.1.1",
"simple-git": "^3.24.0",
"sass": "^1.55.0",
"sass-loader": "^13.1.0",
"simple-git": "^3.14.1",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.4",
"ts-jest": "^29.1.2",
"typescript": "^5.4.3",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
"style-loader": "^3.3.1",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"dependencies": {
"@types/argparse": "^2.0.16",
"@types/benchmark": "^2.1.5",
"@types/compression": "1.7.5",
"@types/express": "^4.17.21",
"@types/node": "^18.19.28",
"@types/node-fetch": "^2.6.11",
"@types/swagger-ui-dist": "3.30.4",
"@types/argparse": "^2.0.10",
"@types/benchmark": "^2.1.2",
"@types/compression": "1.7.2",
"@types/express": "^4.17.14",
"@types/node": "^16.11.66",
"@types/node-fetch": "^2.6.2",
"@types/swagger-ui-dist": "3.30.1",
"argparse": "^2.0.1",
"body-parser": "^1.20.2",
"body-parser": "^1.20.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.19.2",
"express": "^4.18.2",
"h264-mp4-encoder": "^1.0.12",
"immer": "^10.0.4",
"immutable": "^4.3.5",
"io-ts": "^2.2.21",
"node-fetch": "^2.7.0",
"react-markdown": "^9.0.1",
"rxjs": "^7.8.1",
"swagger-ui-dist": "^5.13.0",
"tslib": "^2.6.2",
"util.promisify": "^1.1.2",
"immer": "^9.0.15",
"immutable": "^4.1.0",
"node-fetch": "^2.6.7",
"rxjs": "^7.5.7",
"swagger-ui-dist": "^4.14.3",
"tslib": "^2.4.0",
"util.promisify": "^1.1.1",
"xhr2": "^0.2.1"
},
"peerDependencies": {
"canvas": "^2.11.2",
"gl": "^6.0.2",
"jpeg-js": "^0.4.4",
"pngjs": "^6.0.0",
"react": "^18.1.0 || ^17.0.2 || ^16.14.0",
"react-dom": "^18.1.0 || ^17.0.2 || ^16.14.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
},
"gl": {
"optional": true
},
"jpeg-js": {
"optional": true
},
"pngjs": {
"optional": true
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -15,7 +15,7 @@ const deployDir = path.resolve(buildDir, 'deploy/');
const localPath = path.resolve(deployDir, 'molstar.github.io/');
const analyticsTag = /<!-- __MOLSTAR_ANALYTICS__ -->/g;
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics --><iframe src="https://web3dsurvey.com/collector-iframe.html" style="width: 1px; height: 1px;"></iframe>`;
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics -->`;
function log(command, stdout, stderr) {
if (command) {
@@ -39,14 +39,6 @@ function copyViewer() {
addAnalytics(path.resolve(viewerDeployPath, 'index.html'));
}
function copyMe() {
console.log('\n###', 'copy me files');
const meBuildPath = path.resolve(buildDir, '../build/mesoscale-explorer/');
const meDeployPath = path.resolve(localPath, 'me/');
fse.copySync(meBuildPath, meDeployPath, { overwrite: true });
addAnalytics(path.resolve(meDeployPath, 'index.html'));
}
function copyDemos() {
console.log('\n###', 'copy demos files');
const lightingBuildPath = path.resolve(buildDir, '../build/examples/lighting/');
@@ -62,7 +54,6 @@ function copyDemos() {
function copyFiles() {
copyViewer();
copyMe();
copyDemos();
}

View File

@@ -8,8 +8,7 @@
import { Structure } from '../../mol-model/structure';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { PluginStateObject as PSO, PluginStateTransform } from '../../mol-plugin-state/objects';
import { createPluginUI } from '../../mol-plugin-ui';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { createPluginUI } from '../../mol-plugin-ui/react18';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
@@ -129,7 +128,7 @@ class Viewer {
? document.getElementById(elementOrId)
: elementOrId;
if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
const plugin = await createPluginUI({ target: element, spec, render: renderReact18 });
const plugin = await createPluginUI(element, spec);
(plugin.customState as any) = {
colorPalette: {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -31,8 +31,7 @@ function shinyStyle(plugin: PluginContext) {
postprocessing: {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'off', params: {} },
shadow: { name: 'off', params: {} },
outline: { name: 'off', params: {} },
outline: { name: 'off', params: {} }
}
} });
}
@@ -45,21 +44,17 @@ function occlusionStyle(plugin: PluginContext) {
postprocessing: {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'on', params: {
blurKernelSize: 15,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
radius: 5,
samples: 32,
resolutionScale: 1,
color: Color(0x000000),
resolutionScale: 1
} },
outline: { name: 'on', params: {
scale: 1.0,
threshold: 0.33,
color: Color(0x0000),
includeTransparent: true,
} },
shadow: { name: 'off', params: {} },
} }
}
} });
}

View File

@@ -1,280 +0,0 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mp4Export } from '../../extensions/mp4-export';
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
import { createPluginUI } from '../../mol-plugin-ui';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { PluginSpec } from '../../mol-plugin/spec';
import '../../mol-util/polyfill';
import { ObjectKeys } from '../../mol-util/type-helpers';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { Backgrounds } from '../../extensions/backgrounds';
import { LeftPanel, RightPanel } from './ui/panels';
import { Color } from '../../mol-util/color';
import { SpacefillRepresentationProvider } from '../../mol-repr/structure/representation/spacefill';
import { PluginBehaviors } from '../../mol-plugin/behavior';
import { MesoFocusLoci } from './behavior/camera';
import { GraphicsMode, MesoscaleState } from './data/state';
import { MesoSelectLoci } from './behavior/select';
import { Transparency } from '../../mol-gl/webgl/render-item';
import { LoadModel, loadExampleEntry, loadPdb, loadPdbDev, loadUrl, openState } from './ui/states';
import { Asset } from '../../mol-util/assets';
import { AnimateCameraSpin } from '../../mol-plugin-state/animation/built-in/camera-spin';
import { AnimateCameraRock } from '../../mol-plugin-state/animation/built-in/camera-rock';
import { AnimateStateSnapshots } from '../../mol-plugin-state/animation/built-in/state-snapshots';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
export type ExampleEntry = {
id: string,
label: string,
url: string,
type: 'molx' | 'molj' | 'cif' | 'bcif',
description?: string,
link?: string,
}
export type MesoscaleExplorerState = {
examples?: ExampleEntry[],
graphicsMode: GraphicsMode,
stateRef?: string,
stateCache: { [k: string]: any },
}
//
const Extensions = {
'backgrounds': PluginSpec.Behavior(Backgrounds),
'mp4-export': PluginSpec.Behavior(Mp4Export),
};
const DefaultMesoscaleExplorerOptions = {
customFormats: [] as [string, DataFormatProvider][],
extensions: ObjectKeys(Extensions),
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: 'blended' as Transparency,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
viewportShowSelectionMode: false,
viewportShowAnimation: false,
viewportShowTrajectoryControls: false,
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,
graphicsMode: 'quality' as GraphicsMode
};
type MesoscaleExplorerOptions = typeof DefaultMesoscaleExplorerOptions;
export class MesoscaleExplorer {
constructor(public plugin: PluginUIContext) {
}
async loadExample(id: string) {
const entries = (this.plugin.customState as MesoscaleExplorerState).examples || [];
const entry = entries.find(e => e.id === id);
if (entry !== undefined) {
await loadExampleEntry(this.plugin, entry);
}
}
async loadUrl(url: string, type: 'molx' | 'molj' | 'cif' | 'bcif') {
await loadUrl(this.plugin, url, type);
}
async loadPdb(id: string) {
await loadPdb(this.plugin, id);
}
async loadPdbDev(id: string) {
await loadPdbDev(this.plugin, id);
}
static async create(elementOrId: string | HTMLElement, options: Partial<MesoscaleExplorerOptions> = {}) {
const definedOptions = {} as any;
// filter for defined properies only so the default values
// are property applied
for (const p of Object.keys(options) as (keyof MesoscaleExplorerOptions)[]) {
if (options[p] !== void 0) definedOptions[p] = options[p];
}
const o: MesoscaleExplorerOptions = { ...DefaultMesoscaleExplorerOptions, ...definedOptions };
const defaultSpec = DefaultPluginUISpec();
const spec: PluginUISpec = {
actions: defaultSpec.actions,
behaviors: [
PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
PluginSpec.Behavior(PluginBehaviors.Camera.CameraControls),
PluginSpec.Behavior(MesoFocusLoci),
PluginSpec.Behavior(MesoSelectLoci),
...o.extensions.map(e => Extensions[e]),
],
animations: [
AnimateCameraSpin,
AnimateCameraRock,
AnimateStateSnapshots,
],
customParamEditors: defaultSpec.customParamEditors,
customFormats: o?.customFormats,
layout: {
initial: {
isExpanded: o.layoutIsExpanded,
showControls: o.layoutShowControls,
controlsDisplay: o.layoutControlsDisplay,
regionState: {
bottom: 'full',
left: o.collapseLeftPanel ? 'collapsed' : 'full',
right: o.collapseRightPanel ? 'hidden' : 'full',
top: 'full',
}
},
},
components: {
...defaultSpec.components,
controls: {
...defaultSpec.components?.controls,
top: 'none',
bottom: 'none',
left: LeftPanel,
right: RightPanel,
},
remoteState: 'none',
},
config: [
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
[PluginConfig.General.PixelScale, o.pixelScale],
[PluginConfig.General.PickScale, o.pickScale],
[PluginConfig.General.Transparency, o.transparency],
[PluginConfig.General.PreferWebGl1, o.preferWebgl1],
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
[PluginConfig.General.PowerPreference, o.powerPreference],
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
[PluginConfig.Viewport.ShowTrajectoryControls, o.viewportShowTrajectoryControls],
[PluginConfig.State.DefaultServer, o.pluginStateServer],
[PluginConfig.State.CurrentServer, o.pluginStateServer],
[PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
[PluginConfig.VolumeStreaming.Enabled, !o.volumeStreamingDisabled],
[PluginConfig.Download.DefaultPdbProvider, o.pdbProvider],
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
]
};
const element = typeof elementOrId === 'string'
? document.getElementById(elementOrId)
: elementOrId;
if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
const plugin = await createPluginUI({
target: element,
spec,
render: renderReact18,
onBeforeUIRender: async plugin => {
let examples: MesoscaleExplorerState['examples'] = undefined;
try {
examples = await plugin.fetch({ url: './examples/list.json', type: 'json' }).run();
} catch (e) {
console.log(e);
}
(plugin.customState as MesoscaleExplorerState) = {
examples,
graphicsMode: o.graphicsMode,
stateCache: {},
};
await MesoscaleState.init(plugin);
}
});
plugin.canvas3d?.setProps({
renderer: {
backgroundColor: Color(0x101010),
},
cameraFog: { name: 'off', params: {} },
hiZ: { enabled: true },
});
plugin.representation.structure.registry.clear();
plugin.representation.structure.registry.add(SpacefillRepresentationProvider);
plugin.state.setSnapshotParams({
image: true,
componentManager: false,
structureSelection: true,
behavior: true,
});
plugin.managers.lociLabels.clearProviders();
plugin.managers.dragAndDrop.addHandler('mesoscale-explorer', (files) => {
const sessions = files.filter(f => {
const fn = f.name.toLowerCase();
return fn.endsWith('.molx') || fn.endsWith('.molj');
});
if (sessions.length > 0) {
openState(plugin, sessions[0]);
} else {
plugin.runTask(plugin.state.data.applyAction(LoadModel, {
files: files.map(f => Asset.File(f)),
}));
}
return true;
});
plugin.state.events.object.created.subscribe(e => {
(plugin.customState as MesoscaleExplorerState).stateCache = {};
});
plugin.state.events.object.removed.subscribe(e => {
(plugin.customState as MesoscaleExplorerState).stateCache = {};
});
return new MesoscaleExplorer(plugin);
}
handleResize() {
this.plugin.layout.events.updated.next(void 0);
}
dispose() {
this.plugin.dispose();
}
}

View File

@@ -1,75 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Loci } from '../../../mol-model/loci';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { PluginBehavior } from '../../../mol-plugin/behavior';
import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
import { Binding } from '../../../mol-util/binding';
import { PluginCommands } from '../../../mol-plugin/commands';
import { Sphere3D } from '../../../mol-math/geometry';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const DefaultMesoFocusLociBindings = {
clickCenter: Binding([
Trigger(B.Flag.Primary, M.create()),
], 'Camera center', 'Click element using ${triggers}'),
clickCenterFocus: Binding([
Trigger(B.Flag.Secondary, M.create()),
], 'Camera center and focus', 'Click element using ${triggers}'),
};
const MesoFocusLociParams = {
minRadius: PD.Numeric(8, { min: 1, max: 50, step: 1 }),
extraRadius: PD.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci' }),
durationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'Camera transition duration' }),
centerOnly: PD.Boolean(true, { description: 'Keep current camera distance' }),
bindings: PD.Value(DefaultMesoFocusLociBindings, { isHidden: true }),
};
type MesoFocusLociProps = PD.Values<typeof MesoFocusLociParams>
export const MesoFocusLoci = PluginBehavior.create<MesoFocusLociProps>({
name: 'camera-meso-focus-loci',
category: 'interaction',
ctor: class extends PluginBehavior.Handler<MesoFocusLociProps> {
register(): void {
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
const { canvas3d } = this.ctx;
if (!canvas3d) return;
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
const { clickCenter, clickCenterFocus } = this.params.bindings;
const { durationMs, extraRadius, minRadius } = this.params;
const radius = Math.max(sphere.radius + extraRadius, minRadius);
if (Binding.match(clickCenter, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
const snapshot = canvas3d.camera.getCenter(sphere.center);
canvas3d.requestCameraReset({ durationMs, snapshot });
} else if (Binding.match(clickCenterFocus, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
const snapshot = canvas3d.camera.getCenter(sphere.center, radius);
canvas3d.requestCameraReset({ durationMs, snapshot });
}
});
}
},
params: () => MesoFocusLociParams,
display: { name: 'Camera Meso Focus Loci on Canvas' }
});

View File

@@ -1,164 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { EveryLoci, Loci } from '../../../mol-model/loci';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { PluginBehavior } from '../../../mol-plugin/behavior';
import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
import { Binding } from '../../../mol-util/binding';
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
import { Structure, StructureElement } from '../../../mol-model/structure';
import { StateSelection } from '../../../mol-state';
import { StateTreeSpine } from '../../../mol-state/tree/spine';
import { Representation } from '../../../mol-repr/representation';
import { MarkerAction } from '../../../mol-util/marker-action';
import { PluginContext } from '../../../mol-plugin/context';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const DefaultMesoSelectLociBindings = {
clickToggleSelect: Binding([
Trigger(B.Flag.Primary, M.create({ shift: true })),
Trigger(B.Flag.Primary, M.create({ control: true })),
], 'Toggle select', 'Click element using ${triggers}'),
hoverHighlightOnly: Binding([
Trigger(B.Flag.None, M.create({ shift: true })),
Trigger(B.Flag.None, M.create({ control: true })),
], 'Highlight', 'Hover element using ${triggers}'),
};
const MesoSelectLociParams = {
bindings: PD.Value(DefaultMesoSelectLociBindings, { isHidden: true }),
};
type MesoSelectLociProps = PD.Values<typeof MesoSelectLociParams>
export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
name: 'camera-meso-select-loci',
category: 'interaction',
ctor: class extends PluginBehavior.Handler<MesoSelectLociProps> {
private spine: StateTreeSpine.Impl;
private lociMarkProvider = (interactionLoci: Representation.Loci, action: MarkerAction) => {
if (!this.ctx.canvas3d) return;
this.ctx.canvas3d.mark(interactionLoci, action);
};
private applySelectMark(ref: string, clear?: boolean) {
const cell = this.ctx.state.data.cells.get(ref);
if (cell && SO.isRepresentation3D(cell.obj)) {
this.spine.current = cell;
const so = this.spine.getRootOfType(SO.Molecule.Structure);
if (so) {
if (clear) {
this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect);
}
const loci = this.ctx.managers.structure.selection.getLoci(so.data);
this.lociMarkProvider({ loci }, MarkerAction.Select);
}
}
}
register(): void {
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
if (!this.ctx.canvas3d || this.ctx.isBusy) return;
const { clickToggleSelect } = this.params.bindings;
if (Binding.match(clickToggleSelect, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
this.ctx.managers.interactivity.lociSelects.deselectAll();
return;
}
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : 'chain');
this.ctx.managers.interactivity.lociSelects.toggle({ loci }, false);
}
});
this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, button, modifiers }) => {
if (!this.ctx.canvas3d || this.ctx.isBusy) return;
const pointerLock = !!this.ctx.canvas3dContext?.input.pointerLock;
const { hoverHighlightOnly } = this.params.bindings;
if (!pointerLock && Binding.match(hoverHighlightOnly, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
this.ctx.managers.interactivity.lociHighlights.clearHighlights();
return;
}
if (modifiers.control) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
} else {
const loci = Loci.normalize(current.loci, 'chain');
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
}
}
if (Loci.isEmpty(current.loci)) {
this.ctx.behaviors.labels.highlight.next({ labels: [] });
} else {
const labels: string[] = [];
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
labels.push(cell?.obj?.label || 'Unknown');
}
this.ctx.behaviors.labels.highlight.next({ labels });
}
});
this.ctx.managers.interactivity.lociHighlights.addProvider(this.lociMarkProvider);
let dimDisabled = false;
this.subscribeObservable(this.ctx.behaviors.interaction.keyReleased, ({ code, modifiers }) => {
if (!this.ctx.canvas3d) return;
if ((code.startsWith('Shift') && !modifiers.control) || (code.startsWith('Control') && !modifiers.shift)) {
if (dimDisabled) {
dimDisabled = false;
this.ctx.canvas3d?.setProps({ renderer: { dimStrength: 1 } }, true);
}
this.ctx.managers.interactivity.lociHighlights.clearHighlights();
}
});
this.subscribeObservable(this.ctx.behaviors.interaction.key, ({ modifiers }) => {
if (!this.ctx.canvas3d) return;
if (!dimDisabled && modifiers.control && modifiers.shift) {
dimDisabled = true;
this.ctx.canvas3d?.setProps({ renderer: { dimStrength: 0 } });
}
});
this.subscribeObservable(this.ctx.state.events.object.created, ({ ref }) => this.applySelectMark(ref));
// re-apply select-mark to all representation of an updated structure
this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref, obj, oldObj, oldData, action }) => {
const cell = this.ctx.state.data.cells.get(ref);
if (cell && SO.Molecule.Structure.is(cell.obj)) {
const structure: Structure = obj.data;
const oldStructure: Structure | undefined = action === 'recreate' ? oldObj?.data :
action === 'in-place' ? oldData : undefined;
if (oldStructure &&
Structure.areEquivalent(structure, oldStructure) &&
Structure.areHierarchiesEqual(structure, oldStructure)) return;
const reprs = this.ctx.state.data.select(StateSelection.children(ref).ofType(SO.Molecule.Structure.Representation3D));
for (const repr of reprs) this.applySelectMark(repr.transform.ref, true);
}
});
}
unregister() {
this.ctx.managers.interactivity.lociSelects.removeProvider(this.lociMarkProvider);
this.ctx.managers.interactivity.lociHighlights.removeProvider(this.lociMarkProvider);
}
constructor(ctx: PluginContext, params: MesoSelectLociProps) {
super(ctx, params);
this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
}
},
params: () => MesoSelectLociParams,
display: { name: 'Camera Meso Select Loci on Canvas' }
});

View File

@@ -1,178 +0,0 @@
/**
* Copyright (c) 2019-2023 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>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { SortedArray } from '../../../../mol-data/int';
import { SymmetryOperator } from '../../../../mol-math/geometry';
import { Mat4 } from '../../../../mol-math/linear-algebra';
import { ModelSymmetry } from '../../../../mol-model-formats/structure/property/symmetry';
import { CustomStructureProperty } from '../../../../mol-model-props/common/custom-structure-property';
import { ElementIndex, EntityIndex, Model, Structure, Unit } from '../../../../mol-model/structure';
import { Assembly, Symmetry } from '../../../../mol-model/structure/model/properties/symmetry';
import { PluginStateObject as PSO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
import { PluginContext } from '../../../../mol-plugin/context';
import { Task } from '../../../../mol-task';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
function createModelChainMap(model: Model) {
const builder = new Structure.StructureBuilder();
const units = new Map<string, Unit>();
const { label_asym_id, _rowCount } = model.atomicHierarchy.chains;
const { offsets } = model.atomicHierarchy.chainAtomSegments;
for (let i = 0; i < _rowCount; i++) {
const elements = SortedArray.ofBounds(offsets[i] as ElementIndex, offsets[i + 1] as ElementIndex);
const unit = builder.addUnit(Unit.Kind.Atomic, model, SymmetryOperator.Default, elements, Unit.Trait.FastBoundary);
units.set(label_asym_id.value(i), unit);
}
return units;
}
function buildCellpackAssembly(model: Model, assembly: Assembly) {
const coordinateSystem = SymmetryOperator.create(assembly.id, Mat4.identity(), { assembly: { id: assembly.id, operId: 0, operList: [] } });
const assembler = Structure.Builder({
coordinateSystem,
label: model.label,
});
const units = createModelChainMap(model);
for (const g of assembly.operatorGroups) {
for (const oper of g.operators) {
for (const id of g.asymIds!) {
const u = units.get(id);
if (u) {
assembler.addWithOperator(u, oper);
} else {
console.log(`missing asymId '${id}'`);
}
}
}
}
return assembler.getStructure();
}
export { CellpackAssembly };
type CellpackAssembly = typeof CellpackAssembly
const CellpackAssembly = PluginStateTransform.BuiltIn({
name: 'cellpack-assembly',
display: { name: 'Cellpack Assembly' },
from: PSO.Molecule.Model,
to: PSO.Molecule.Structure,
params: {
id: PD.Text('', { label: 'Asm Id', description: 'Assembly Id (use empty for the 1st assembly)' }),
}
})({
canAutoUpdate({ newParams }) {
return true;
},
apply({ a, params }, plugin: PluginContext) {
return Task.create('Build Structure', async ctx => {
const model = a.data;
let id = params.id;
let asm: Assembly | undefined = void 0;
const symmetry = ModelSymmetry.Provider.get(model);
// if no id is specified, use the 1st assembly.
if (!id && symmetry && symmetry.assemblies.length !== 0) {
id = symmetry.assemblies[0].id;
}
if (!symmetry || symmetry.assemblies.length === 0) {
plugin.log.warn(`Model '${model.entryId}' has no assembly, returning model structure.`);
} else {
asm = Symmetry.findAssembly(model, id || '');
if (!asm) {
plugin.log.warn(`Model '${model.entryId}' has no assembly called '${id}', returning model structure.`);
}
}
const base = Structure.ofModel(model);
if (!asm) {
const label = { label: 'Model', description: Structure.elementDescription(base) };
return new PSO.Molecule.Structure(base, label);
}
const s = buildCellpackAssembly(model, asm);
const objProps = { label: `Assembly ${id}`, description: Structure.elementDescription(s) };
return new PSO.Molecule.Structure(s, objProps);
});
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});
type UnitsByEntity = Map<EntityIndex, Unit[]>;
const UnitsByEntity = CustomStructureProperty.createSimple<UnitsByEntity>('units_by_entity', 'root');
function getUnitsByEntity(structure: Structure): UnitsByEntity {
if (UnitsByEntity.get(structure).value) {
return UnitsByEntity.get(structure).value!;
}
const atomicIndex = structure.model.atomicHierarchy.index;
const map: UnitsByEntity = new Map();
for (const ug of structure.unitSymmetryGroups) {
const u = ug.units[0] as Unit.Atomic;
const e = atomicIndex.getEntityFromChain(u.chainIndex[u.elements[0]]);
if (!map.has(e)) map.set(e, []);
const entityUnits = map.get(e)!;
for (let i = 0, il = ug.units.length; i < il; ++i) {
entityUnits.push(ug.units[i]);
}
}
UnitsByEntity.set(structure, { value: map }, map);
return map;
}
export { CellpackStructure };
type CellpackStructure = typeof CellpackStructure
const CellpackStructure = PluginStateTransform.BuiltIn({
name: 'cellpack-structure',
display: { name: 'Cellpack Structure' },
from: PSO.Root,
to: PSO.Molecule.Structure,
params: {
structureRef: PD.Text(''),
entityId: PD.Text('')
}
})({
canAutoUpdate({ newParams }) {
return true;
},
apply({ a, params, dependencies }) {
return Task.create('Build Structure', async ctx => {
const parent = dependencies![params.structureRef].data as Structure;
const { entities } = parent.model;
const idx = entities.getEntityIndex(params.entityId);
const unitsByEntity = getUnitsByEntity(parent);
const units = unitsByEntity.get(idx) || [];
// if (!unitsByEntity.get(idx)) {
// console.log(entities.data.pdbx_description.value(idx));
// }
const structure = Structure.create(units);
const description = entities.data.pdbx_description.value(idx)[0] || 'model';
const label = description.split('.').at(-1) || a.label;
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
});
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});

View File

@@ -1,227 +0,0 @@
/**
* Copyright (c) 2019-2023 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>
*/
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../../../mol-plugin/context';
import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/representation/spacefill';
import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../mol-state';
import { Color } from '../../../../mol-util/color';
import { ColorNames } from '../../../../mol-util/color/names';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
import { CellpackAssembly, CellpackStructure } from './model';
function getSpacefillParams(color: Color, sizeFactor: number, graphics: GraphicsMode, merge?: boolean) {
const gmp = getGraphicsModeProps(graphics === 'custom' ? 'quality' : graphics);
return {
type: {
name: 'spacefill',
params: {
...SpacefillRepresentationProvider.defaultValues,
ignoreHydrogens: false,
instanceGranularity: true,
ignoreLight: true,
lodLevels: gmp.lodLevels,
quality: 'lowest', // avoid 'auto', triggers boundary calc
sizeFactor,
clip: {
variant: merge ? 'pixel' : 'instance',
objects: [],
},
clipPrimitive: true,
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
visuals: [merge ? 'structure-element-sphere' : 'element-sphere'],
},
},
colorTheme: {
name: 'uniform',
params: {
value: color,
saturation: 0,
lightness: 0,
}
},
sizeTheme: {
name: 'physical',
params: {
value: 1,
}
},
};
}
function getSizeFactor(name: string): number {
switch (name) {
case 'dLDL':
return 2.5;
case 'iLDL':
return 5;
case 'NP_CA':
case 'POL_CA':
case 'FactorH1':
case 'iIgM_Antibody_5mer':
// case 'MG_271_272_273_274_192MER': // has a coarse and an atomic part
return 2;
default: return 1;
}
}
export async function createCellpackHierarchy(plugin: PluginContext, trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory>) {
const builder = plugin.builders.structure;
const state = plugin.state.data;
const model = await builder.createModel(trajectory, { modelIndex: 0 });
const entities = model.data!.entities.data;
const compGroups = new Map<string, StateObjectSelector>();
const compIds = new Map<string, { idx: number, members: Map<string, number> }>();
const compColors = new Map<string, Color[]>();
const funcGroups = new Map<string, StateObjectSelector>();
const funcIds = new Map<string, { idx: number, size: number }>();
const funcColors = new Map<string, Color[]>();
const graphicsMode = MesoscaleState.get(plugin).graphics;
const groupParams = getMesoscaleGroupParams(graphicsMode);
const base = await state.build()
.to(model)
.apply(CellpackAssembly, { id: '' })
.commit();
const compRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:comp:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `comp:`, label: 'compartment', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
const funcRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:func:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `func:`, label: 'function', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
if (entities._rowCount > 1) {
for (let i = 0; i < entities._rowCount; i++) {
const description = entities.pdbx_description.value(i)[0] || 'unknown compartment';
const d = description.split('.');
const n = d.slice(0, -1).join('.');
const l = d.at(-1)!;
if (!compIds.has(n)) {
compIds.set(n, { idx: compIds.size, members: new Map() });
}
const cm = compIds.get(n)!;
cm.members.set(l, cm.members.size);
const f = entities.details.value(i) || 'unknown function';
if (!funcIds.has(f)) {
funcIds.set(f, { idx: funcIds.size, size: 0 });
}
funcIds.get(f)!.size += 1;
}
//
const baseCompColors = getDistinctBaseColors(compIds.size, 0);
const compIdEntries = Array.from(compIds.entries());
for (let i = 0; i < compIdEntries.length; ++i) {
const [n, m] = compIdEntries[i];
const groupColors = getDistinctGroupColors(m.members.size, baseCompColors[i], 20, 0);
compColors.set(n, groupColors);
}
//
const baseFuncColors = getDistinctBaseColors(funcIds.size, 0);
const funcIdEntries = Array.from(funcIds.entries());
for (let i = 0; i < funcIdEntries.length; ++i) {
const [n, m] = funcIdEntries[i];
const groupColors = getDistinctGroupColors(m.size, baseFuncColors[i], 20, 0);
funcColors.set(n, groupColors);
}
//
for (let i = 0; i < entities._rowCount; i++) {
const description = entities.pdbx_description.value(i)[0] || 'unknown compartment';
const nodes = description.split('.');
for (let j = 0, jl = nodes.length - 1; j < jl; ++j) {
const n = nodes.slice(0, j + 1).join('.');
const p = nodes.slice(0, j).join('.');
if (!compGroups.has(n)) {
const colorIdx = compIds.get(n)?.idx;
const color = colorIdx !== undefined ? baseCompColors[colorIdx] : ColorNames.white;
const label = nodes[j];
const parent = compGroups.get(p) ?? compRoot;
parent.cell!.state.isCollapsed = false;
const group = await state.build()
.to(parent)
.applyOrUpdateTagged(`group:comp:${n}`, MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: `comp:${p}`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
compGroups.set(n, group);
}
}
const f = entities.details.value(i) || 'unknown function';
if (!funcGroups.has(f)) {
const colorIdx = funcIds.get(f)?.idx;
const color = colorIdx !== undefined ? baseFuncColors[colorIdx] : ColorNames.white;
const group = await state.build()
.to(funcRoot)
.applyOrUpdateTagged(`group:func:${f}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: 'func:', state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
funcGroups.set(f, group);
}
}
//
await state.transaction(async () => {
try {
const dependsOn = [base.ref];
plugin.animationLoop.stop({ noDraw: true });
let build: StateBuilder.Root | StateBuilder.To<any> = state.build();
for (let i = 0; i < entities._rowCount; i++) {
const description = entities.pdbx_description.value(i)[0] || 'model';
const d = description.split('.');
const n = d.slice(0, -1).join('.');
const l = d.at(-1)!;
const f = entities.details.value(i) || 'unknown function';
const color = compColors.get(n)![compIds.get(n)!.members.get(l)!];
const sizeFactor = getSizeFactor(l);
build = build
.toRoot()
.apply(CellpackStructure, { structureRef: base.ref, entityId: entities.id.value(i) }, { dependsOn })
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode), { tags: [`comp:${n}`, `func:${f}`] });
}
await build.commit();
} catch (e) {
console.error(e);
plugin.log.error(e);
} finally {
plugin.animationLoop.start();
}
}).run();
} else {
const dependsOn = [base.ref];
const merge = (
base.data &&
base.data.model.entities.data._rowCount === 1 &&
base.data.unitSymmetryGroups.length > 100 &&
base.data.unitSymmetryGroups.some(usg => usg.units.length > 1)
);
await state.build()
.toRoot()
.apply(CellpackStructure, { structureRef: base.ref, entityId: entities.id.value(0) }, { dependsOn })
.apply(StructureRepresentation3D, getSpacefillParams(ColorNames.lightgray, 1, graphicsMode, merge), { tags: [`comp:`, `func:`] })
.commit();
}
}

View File

@@ -1,139 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
import { ElementIndex, Model, Structure, Unit } from '../../../../mol-model/structure';
import { PluginStateObject as SO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
import { Task } from '../../../../mol-task';
import { StateObject, StateTransformer } from '../../../../mol-state';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { SymmetryOperator } from '../../../../mol-math/geometry';
import { mergeUnits, partitionUnits } from '../util';
import { Assembly, Symmetry } from '../../../../mol-model/structure/model/properties/symmetry';
import { ModelSymmetry } from '../../../../mol-model-formats/structure/property/symmetry';
import { SortedArray } from '../../../../mol-data/int';
import { GenericInstances, getTransforms } from './preset';
import { Asset } from '../../../../mol-util/assets';
import { PluginContext } from '../../../../mol-plugin/context';
import { deepEqual } from '../../../../mol-util';
function createModelChainMap(model: Model) {
const builder = new Structure.StructureBuilder();
const units = new Map<string, Unit>();
const { label_asym_id, _rowCount } = model.atomicHierarchy.chains;
const { offsets } = model.atomicHierarchy.chainAtomSegments;
for (let i = 0; i < _rowCount; i++) {
const elements = SortedArray.ofBounds(offsets[i] as ElementIndex, offsets[i + 1] as ElementIndex);
const unit = builder.addUnit(Unit.Kind.Atomic, model, SymmetryOperator.Default, elements, Unit.Trait.FastBoundary);
units.set(label_asym_id.value(i), unit);
}
return units;
}
function buildAssembly(model: Model, assembly: Assembly) {
const coordinateSystem = SymmetryOperator.create(assembly.id, Mat4.identity(), { assembly: { id: assembly.id, operId: 0, operList: [] } });
const assembler = Structure.Builder({
coordinateSystem,
label: model.label,
});
const units = createModelChainMap(model);
for (const g of assembly.operatorGroups) {
for (const oper of g.operators) {
for (const id of g.asymIds!) {
const u = units.get(id);
if (u) {
assembler.addWithOperator(u, oper);
} else {
console.log(`missing asymId '${id}'`);
}
}
}
}
return assembler.getStructure();
}
const EmptyInstances: GenericInstances<Asset> = {
positions: { data: [] },
rotations: { variant: 'euler', data: [] }
};
export { StructureFromGeneric };
type StructureFromGeneric = typeof StructureFromGeneric
const StructureFromGeneric = PluginStateTransform.BuiltIn({
name: 'structure-from-generic',
display: { name: 'Structure from Generic', description: 'Create a molecular structure from Generic models.' },
from: SO.Molecule.Model,
to: SO.Molecule.Structure,
params: {
instances: PD.Value<GenericInstances<Asset>>(EmptyInstances),
label: PD.Optional(PD.Text('')),
cellSize: PD.Numeric(500, { min: 0, max: 10000, step: 100 }),
}
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Build Structure', async ctx => {
const transforms = await getTransforms(plugin, params.instances);
if (transforms.length === 0) return StateObject.Null;
const model = a.data;
const label = params.label || model.label;
const base = Structure.ofModel(a.data);
let structure: Structure;
if (transforms.length === 1 && Mat4.isIdentity(transforms[0])) {
const symmetry = ModelSymmetry.Provider.get(model);
const id = symmetry?.assemblies[0]?.id;
const asm = Symmetry.findAssembly(model, id || '');
if (asm) {
structure = buildAssembly(model, asm);
} else {
const mergedUnits = partitionUnits(base.units, params.cellSize);
structure = Structure.create(mergedUnits, { label });
}
} else {
const assembler = Structure.Builder({ label });
const unit = mergeUnits(base.units, 0);
for (let i = 0, il = transforms.length; i < il; ++i) {
const t = transforms[i];
const op = SymmetryOperator.create(`op-${i}`, t);
assembler.addWithOperator(unit, op);
}
structure = assembler.getStructure();
}
const props = { label, description: Structure.elementDescription(structure) };
return new SO.Molecule.Structure(structure, props);
});
},
update({ newParams, oldParams }, plugin: PluginContext) {
if (deepEqual(newParams, oldParams)) {
return StateTransformer.UpdateResult.Unchanged;
}
if (oldParams.instances) releaseInstances(plugin, oldParams.instances);
return StateTransformer.UpdateResult.Recreate;
},
dispose({ b, params }, plugin: PluginContext) {
b?.data.customPropertyDescriptors.dispose();
if (params?.instances) releaseInstances(plugin, params.instances);
}
});
function releaseInstances(plugin: PluginContext, instances: GenericInstances<Asset>) {
if (!Array.isArray(instances.positions.data)) {
plugin.managers.asset.release(instances.positions.data.file);
}
if (!Array.isArray(instances.rotations.data)) {
plugin.managers.asset.release(instances.rotations.data.file);
}
}

View File

@@ -1,581 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
import { StateBuilder, StateObjectSelector } from '../../../../mol-state';
import { PluginContext } from '../../../../mol-plugin/context';
import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/representation/spacefill';
import { Color } from '../../../../mol-util/color';
import { utf8Read } from '../../../../mol-io/common/utf8';
import { Mat3, Quat, Vec3 } from '../../../../mol-math/linear-algebra';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
import { ColorNames } from '../../../../mol-util/color/names';
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
import { ParseCif, ParsePly, ReadFile } from '../../../../mol-plugin-state/transforms/data';
import { ModelFromTrajectory, ShapeFromPly, TrajectoryFromGRO, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromMmCif, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../../../mol-plugin-state/transforms/model';
import { Euler } from '../../../../mol-math/linear-algebra/3d/euler';
import { Asset } from '../../../../mol-util/assets';
import { Clip } from '../../../../mol-util/clip';
import { StructureFromGeneric } from './model';
import { getFileNameInfo } from '../../../../mol-util/file-info';
import { NumberArray } from '../../../../mol-util/type-helpers';
import { BaseGeometry } from '../../../../mol-geo/geometry/base';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
function getSpacefillParams(color: Color, sizeFactor: number, graphics: GraphicsMode, clipVariant: Clip.Variant) {
const gmp = getGraphicsModeProps(graphics === 'custom' ? 'quality' : graphics);
return {
type: {
name: 'spacefill',
params: {
...SpacefillRepresentationProvider.defaultValues,
ignoreHydrogens: true,
instanceGranularity: true,
ignoreLight: true,
lodLevels: gmp.lodLevels.map(l => {
return {
...l,
stride: Math.max(1, Math.round(l.stride / Math.pow(sizeFactor, l.scaleBias)))
};
}),
quality: 'lowest', // avoid 'auto', triggers boundary calc
sizeFactor,
clip: {
variant: clipVariant,
objects: [],
},
clipPrimitive: true,
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
},
},
colorTheme: {
name: 'uniform',
params: {
value: color,
saturation: 0,
lightness: 0,
}
},
sizeTheme: {
name: 'physical',
params: {
scale: 1,
}
},
};
}
function getPlyShapeParams(color: Color, clipVariant: Clip.Variant) {
return {
...PD.getDefaultValues(BaseGeometry.Params),
instanceGranularity: true,
ignoreLight: true,
clip: {
variant: clipVariant,
objects: [],
},
quality: 'custom',
doubleSided: true,
coloring: {
name: 'uniform',
params: { color }
},
grouping: {
name: 'none',
params: {}
},
material: {
metalness: 0.0,
roughness: 1.0,
bumpiness: 1.0,
},
bumpAmplitude: 0.1,
bumpFrequency: 0.1 / 10,
};
}
export async function createGenericHierarchy(plugin: PluginContext, file: Asset.File) {
const asset = await plugin.runTask(plugin.managers.asset.resolve(file, 'zip'));
let manifest: GenericManifest;
// TODO: remove special handling for martini prototype
if (asset.data['instanced_structure.json']) {
const d = asset.data['instanced_structure.json'];
const t = utf8Read(d, 0, d.length);
const martini = JSON.parse(t) as { model: string, positions: Vec3[], rotations: Vec3[], function: string }[];
console.log(martini);
manifest = martiniToGeneric(martini);
} else if (asset.data['manifest.json']) {
const d = asset.data['manifest.json'];
const t = utf8Read(d, 0, d.length);
manifest = JSON.parse(t) as GenericManifest;
} else {
throw new Error('no manifest found');
}
console.log(manifest);
const state = plugin.state.data;
const graphicsMode = MesoscaleState.get(plugin).graphics;
const groupParams = getMesoscaleGroupParams(graphicsMode);
async function addGroup(g: GenericGroup, cell: StateObjectSelector, parent: string) {
const group = await state.build()
.to(cell)
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `${g.root}:${g.id}`, label: g.label || g.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: [`group:${g.root}:${g.id}`, g.root === parent ? `${g.root}:` : `${g.root}:${parent}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit();
if (g.children) {
for (const c of g.children) {
await addGroup(c, group, g.id);
}
}
}
for (const r of manifest.roots) {
const root = await state.build()
.toRoot()
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `${r.id}:`, label: r.label || r.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: `group:${r.id}:`, state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
if (r.children) {
for (const c of r.children!) {
await addGroup(c, root, r.id);
}
}
}
const transformAssets = new Map<string, Asset>();
const getTransformAsset = (file: string) => {
if (!transformAssets.has(file)) {
const d = asset.data[file];
transformAssets.set(file, Asset.File(new File([d], file)));
}
return transformAssets.get(file)!;
};
const getAssetInstances = (instances: GenericInstances<string>): GenericInstances<Asset> => {
return {
positions: {
data: Array.isArray(instances.positions.data)
? instances.positions.data
: {
file: getTransformAsset(instances.positions.data.file),
view: instances.positions.data.view,
},
type: instances.positions.type,
},
rotations: {
data: Array.isArray(instances.rotations.data)
? instances.rotations.data
: {
file: getTransformAsset(instances.rotations.data.file),
view: instances.rotations.data.view,
},
variant: instances.rotations.variant,
type: instances.rotations.type,
}
};
};
await state.transaction(async () => {
try {
plugin.animationLoop.stop({ noDraw: true });
let build: StateBuilder.Root | StateBuilder.To<any> = state.build();
for (const ent of manifest.entities) {
const d = asset.data[ent.file];
const info = getFileNameInfo(ent.file);
const isBinary = ['bcif'].includes(info.ext);
const t = isBinary ? d : utf8Read(d, 0, d.length);
const file = Asset.File(new File([t], ent.file));
const color = ColorNames.skyblue;
const label = ent.label || ent.file.split('.')[0];
const sizeFactor = ent.sizeFactor || 1;
const tags = ent.groups.map(({ id, root }) => `${root}:${id}`);
const instances = ent.instances && getAssetInstances(ent.instances);
build = build
.toRoot()
.apply(ReadFile, { file, label, isBinary });
if (['gro', 'cif', 'mmcif', 'mcif', 'bcif', 'pdb', 'ent', 'xyz', 'mol', 'sdf', 'sd', 'mol2'].includes(info.ext)) {
if (['gro'].includes(info.ext)) {
build = build.apply(TrajectoryFromGRO);
} else if (['cif', 'mmcif', 'mcif', 'bcif'].includes(info.ext)) {
build = build.apply(ParseCif).apply(TrajectoryFromMmCif);
} else if (['pdb', 'ent'].includes(info.ext)) {
build = build.apply(TrajectoryFromPDB);
} else if (['xyz'].includes(info.ext)) {
build = build.apply(TrajectoryFromXYZ);
} else if (['mol'].includes(info.ext)) {
build = build.apply(TrajectoryFromMOL);
} else if (['sdf', 'sd'].includes(info.ext)) {
build = build.apply(TrajectoryFromSDF);
} else if (['mol2'].includes(info.ext)) {
build = build.apply(TrajectoryFromMOL2);
}
let clipVariant: Clip.Variant = 'pixel';
if (ent.instances) {
if (Array.isArray(ent.instances.positions.data)) {
clipVariant = ent.instances.positions.data.length <= 3 ? 'pixel' : 'instance';
} else {
const byteLength = ent.instances.positions.data.view
? ent.instances.positions.data.view.byteLength
: asset.data[ent.instances.positions.data.file].length;
clipVariant = byteLength <= 3 * 4 ? 'pixel' : 'instance';
}
}
build = build
.apply(ModelFromTrajectory, { modelIndex: 0 })
.apply(StructureFromGeneric, { instances, label })
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode, clipVariant), { tags });
} else if (['ply'].includes(info.ext)) {
if (['ply'].includes(info.ext)) {
const transforms = await getTransforms(plugin, instances);
const clipVariant = transforms.length === 1 ? 'pixel' : 'instance';
build = build
.apply(ParsePly)
.apply(ShapeFromPly, { label, transforms })
.apply(ShapeRepresentation3D, getPlyShapeParams(color, clipVariant), { tags });
}
} else {
console.warn(`unknown file format '${info.ext}'`);
}
}
await build.commit();
const rootId = `${manifest.roots[0].id}:`;
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
await updateColors(plugin, values, rootId, '');
} catch (e) {
console.error(e);
plugin.log.error(e);
} finally {
plugin.animationLoop.start();
}
}).run();
asset.dispose();
}
//
type GenericRoot = {
id: string
label?: string
description?: string
children: GenericGroup[]
}
type GenericGroup = {
id: string
/** reference to `${GenericRoot.id}` */
root: string
label?: string
description?: string
children?: GenericGroup[]
}
type GenericEntity = {
/**
* the entity file name
*
* the following extensions/formats are supported
*
* structures
* - gro
* - cif, mmcif, mcif, bcif
* - pdb, ent
* - xyz
* - mol
* - sdf, sd
* - mol2
*
* meshes
* - ply
*/
file: string
label?: string
description?: string
groups: {
/** reference to `${GenericGroup.id}` */
id: string,
/** reference to `${GenericGroup.root}` */
root: string
}[]
/**
* defaults to a single, untransformed instance
*/
instances?: GenericInstances<string>
/**
* defaults to 1 (assuming fully atomic structures)
* for C-alpha only structures set to 2
* for Martini coarse-grained set to 1.5
*/
sizeFactor?: number
}
type BinaryData<T extends string | Asset> = {
file: T,
view?: {
byteOffset: number,
byteLength: number
}
}
export type GenericInstances<T extends string | Asset> = {
/**
* translation vectors in Angstrom
* [x0, y0, z0, ..., xn, yn, zn] with n = count - 1
*/
positions: {
/**
* either the data itself or a pointer to binary data
*/
data: number[] | BinaryData<T>
/**
* how to interpret the data
* defaults to `{ kind: 'Array', type: 'Float32' }`
*/
type?: { kind: 'Array', type: 'Float32' }
// TODO: maybe worthwhile in the future, mirroring encoders from BinaryCIF
// | { kind: 'IntegerPackedFixedPoint', byteCount: number, srcSize: number, factor: number, srcType: 'Float32' }
}
/**
* euler angles in XYZ order
* [x0, y0, z0, ..., xn, yn, zn] with n = count - 1
*
* quaternion rotations in XYZW order
* [x0, y0, z0, w0, ..., xn, yn, zn, wn] with n = count - 1
*
* rotation matrices in row-major order
* [m00_0, m01_0, m02_0, ..., m20_n, m21_n, m22_n] with n = count - 1
*/
rotations: {
variant: 'euler' | 'quaternion' | 'matrix',
/**
* either the data itself or a pointer to binary data
*/
data: number[] | BinaryData<T>
/**
* how to interpret the data
* defaults to `{ kind: 'Array', type: 'Float32' }`
*/
type?: { kind: 'Array', type: 'Float32' }
}
}
type GenericFrame = {
time: number
entities: {
file: string
instances: GenericInstances<string>
}[]
}
type GenericTrajectory = {
label?: string
description?: string
frames: GenericFrame[]
}
type GenericManifest = {
label?: string
description?: string
roots: GenericRoot[]
entities: GenericEntity[]
trajectories?: GenericTrajectory[]
}
//
const p = Vec3();
const q = Quat();
const m = Mat3();
const e = Euler();
async function getPositions(plugin: PluginContext, p: GenericInstances<Asset>['positions']): Promise<NumberArray> {
if (Array.isArray(p.data)) {
return p.data;
} else {
const a = await plugin.runTask(plugin.managers.asset.resolve(p.data.file, 'binary'));
const o = p.data.view?.byteOffset || 0;
const l = p.data.view?.byteLength || a.data.byteLength;
return new Float32Array(a.data.buffer, o + a.data.byteOffset, l / 4);
}
};
async function getRotations(plugin: PluginContext, r: GenericInstances<Asset>['rotations']): Promise<NumberArray> {
if (Array.isArray(r.data)) {
return r.data;
} else {
const a = await plugin.runTask(plugin.managers.asset.resolve(r.data.file, 'binary'));
const o = r.data.view?.byteOffset || 0;
const l = r.data.view?.byteLength || a.data.byteLength;
return new Float32Array(a.data.buffer, o + a.data.byteOffset, l / 4);
}
};
export async function getTransforms(plugin: PluginContext, instances?: GenericInstances<Asset>) {
const transforms: Mat4[] = [];
if (instances) {
const positions = await getPositions(plugin, instances.positions);
const rotations = await getRotations(plugin, instances.rotations);
for (let i = 0, il = positions.length / 3; i < il; ++i) {
Vec3.fromArray(p, positions, i * 3);
if (instances.rotations.variant === 'matrix') {
Mat3.fromArray(m, rotations, i * 9);
const t = Mat4.fromMat3(Mat4(), m);
Mat4.setTranslation(t, p);
transforms.push(t);
} else if (instances.rotations.variant === 'quaternion') {
Quat.fromArray(q, rotations, i * 4);
const t = Mat4.fromQuat(Mat4(), q);
Mat4.setTranslation(t, p);
transforms.push(t);
} else if (instances.rotations.variant === 'euler') {
Euler.fromArray(e, rotations, i * 3);
Quat.fromEuler(q, e, 'XYZ');
const t = Mat4.fromQuat(Mat4(), q);
Mat4.setTranslation(t, p);
transforms.push(t);
}
}
} else {
transforms.push(Mat4.identity());
}
return transforms;
}
//
type MartiniManifest = {
model: string,
positions: Vec3[],
rotations: Vec3[],
function: string
}[]
function martiniToGeneric(martini: MartiniManifest): GenericManifest {
const functionRoot: GenericRoot = {
id: 'function',
label: 'Function',
description: 'Functional classification',
children: [],
};
const entities: GenericEntity[] = [];
const seenGroups = new Set<string>();
const membraneGroup = {
id: 'membane',
root: 'function',
label: 'Membrane',
children: [] as GenericGroup[],
};
functionRoot.children!.push(membraneGroup);
seenGroups.add(membraneGroup.id);
const lipidsGroup = {
id: 'lipid',
root: 'function',
label: 'Lipid',
children: [] as GenericGroup[],
};
membraneGroup.children!.push(lipidsGroup);
seenGroups.add(lipidsGroup.id);
const upperGroup = {
id: 'upper',
root: 'function',
label: 'Upper Leaflet',
};
lipidsGroup.children!.push(upperGroup);
seenGroups.add(upperGroup.id);
const lowerGroup = {
id: 'lower',
root: 'function',
label: 'Lower Leaflet',
};
lipidsGroup.children!.push(lowerGroup);
seenGroups.add(lowerGroup.id);
const memprotGroup = {
id: 'memprot',
root: 'function',
label: 'Transmembrane Protein',
};
membraneGroup.children!.push(memprotGroup);
seenGroups.add(memprotGroup.id);
for (const e of martini) {
const label = e.model.split('.')[0];
const group = e.function || 'Metabolite';
const positions = {
data: e.positions.flat().map(x => Math.round((x * 10) * 100) / 100)
};
const rotations = {
data: e.rotations.flat().map(x => Math.round(x * 100) / 100),
variant: 'euler' as const,
};
if (group.includes('lower leaflet')) {
entities.push({
file: e.model,
label: label.substring(15),
groups: [{ root: 'function', id: 'lower' }],
instances: { positions, rotations },
sizeFactor: 1.5,
});
} else if (group.includes('upper leaflet')) {
entities.push({
file: e.model,
label: label.substring(15),
groups: [{ root: 'function', id: 'upper' }],
instances: { positions, rotations },
sizeFactor: 1.5,
});
} else if (group.length === 4) {
entities.push({
file: e.model,
label: label.substring(17),
groups: [{ root: 'function', id: 'memprot' }],
instances: { positions, rotations },
sizeFactor: 1.5,
});
} else {
if (!seenGroups.has(group)) {
functionRoot.children!.push({
id: group,
root: 'function',
label: group,
});
seenGroups.add(group);
}
entities.push({
file: e.model,
label,
groups: [{ root: 'function', id: group }],
instances: { positions, rotations },
sizeFactor: 1.5,
});
}
}
return {
label: 'Martini',
description: 'Martini coarse-grained model',
roots: [functionRoot],
entities,
};
}

View File

@@ -1,202 +0,0 @@
/**
* Copyright (c) 2023 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 { SortedArray } from '../../../../mol-data/int';
import { SymmetryOperator } from '../../../../mol-math/geometry';
import { Mat4 } from '../../../../mol-math/linear-algebra';
import { ModelSymmetry } from '../../../../mol-model-formats/structure/property/symmetry';
import { CustomStructureProperty } from '../../../../mol-model-props/common/custom-structure-property';
import { ElementIndex, EntityIndex, Model, Structure, Unit } from '../../../../mol-model/structure';
import { Assembly, Symmetry } from '../../../../mol-model/structure/model/properties/symmetry';
import { PluginStateObject as PSO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
import { PluginContext } from '../../../../mol-plugin/context';
import { StateTransformer } from '../../../../mol-state/transformer';
import { Task } from '../../../../mol-task';
import { deepEqual } from '../../../../mol-util';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { partitionUnits } from '../util';
function createModelChainMap(model: Model) {
const builder = new Structure.StructureBuilder();
const units = new Map<string, Unit>();
const { label_asym_id, _rowCount } = model.atomicHierarchy.chains;
const { offsets } = model.atomicHierarchy.chainAtomSegments;
for (let i = 0; i < _rowCount; i++) {
const elements = SortedArray.ofBounds(offsets[i] as ElementIndex, offsets[i + 1] as ElementIndex);
const unit = builder.addUnit(Unit.Kind.Atomic, model, SymmetryOperator.Default, elements, Unit.Trait.FastBoundary);
units.set(label_asym_id.value(i), unit);
}
return units;
}
function buildAssembly(model: Model, assembly: Assembly) {
const coordinateSystem = SymmetryOperator.create(assembly.id, Mat4.identity(), { assembly: { id: assembly.id, operId: 0, operList: [] } });
const assembler = Structure.Builder({
coordinateSystem,
label: model.label,
});
const units = createModelChainMap(model);
for (const g of assembly.operatorGroups) {
for (const oper of g.operators) {
for (const id of g.asymIds!) {
const u = units.get(id);
if (u) {
assembler.addWithOperator(u, oper);
} else {
console.log(`missing asymId '${id}'`);
}
}
}
}
return assembler.getStructure();
}
export { MmcifAssembly };
type MmcifAssembly = typeof MmcifAssembly
const MmcifAssembly = PluginStateTransform.BuiltIn({
name: 'mmcif-assembly',
display: { name: 'Mmcif Assembly' },
from: PSO.Molecule.Model,
to: PSO.Molecule.Structure,
params: {
id: PD.Text('', { label: 'Asm Id', description: 'Assembly Id (use empty for the 1st assembly)' }),
}
})({
canAutoUpdate({ newParams }) {
return true;
},
apply({ a, params }, plugin: PluginContext) {
return Task.create('Build Structure', async ctx => {
const model = a.data;
let id = params.id;
let asm: Assembly | undefined = void 0;
const symmetry = ModelSymmetry.Provider.get(model);
// if no id is specified, use the 1st assembly.
if (!id && symmetry && symmetry.assemblies.length !== 0) {
id = symmetry.assemblies[0].id;
}
if (!symmetry || symmetry.assemblies.length === 0) {
plugin.log.warn(`Model '${model.entryId}' has no assembly, returning model structure.`);
} else {
asm = Symmetry.findAssembly(model, id || '');
if (!asm) {
plugin.log.warn(`Model '${model.entryId}' has no assembly called '${id}', returning model structure.`);
}
}
const base = Structure.ofModel(model);
if (!asm) {
const label = { label: 'Model', description: Structure.elementDescription(base) };
return new PSO.Molecule.Structure(base, label);
}
const s = buildAssembly(model, asm);
const objProps = { label: `Assembly ${id}`, description: Structure.elementDescription(s) };
return new PSO.Molecule.Structure(s, objProps);
});
},
update({ newParams, oldParams }) {
return deepEqual(newParams, oldParams)
? StateTransformer.UpdateResult.Unchanged
: StateTransformer.UpdateResult.Recreate;
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});
type UnitsByEntity = Map<EntityIndex, Unit[]>;
const UnitsByEntity = CustomStructureProperty.createSimple<UnitsByEntity>('units_by_entity', 'root');
function getUnitsByEntity(structure: Structure): UnitsByEntity {
if (UnitsByEntity.get(structure).value) {
return UnitsByEntity.get(structure).value!;
}
const atomicIndex = structure.model.atomicHierarchy.index;
const spheresIndex = structure.model.coarseHierarchy.spheres;
const map: UnitsByEntity = new Map();
for (const ug of structure.unitSymmetryGroups) {
const u = ug.units[0];
let e: EntityIndex;
if (Unit.isAtomic(u)) {
e = atomicIndex.getEntityFromChain(u.chainIndex[u.elements[0]]);
} else if (Unit.isSpheres(u)) {
e = spheresIndex.getEntityFromChain(u.coarseElements.chainElementSegments.index[u.elements[0]]);
} else {
continue;
}
if (!map.has(e)) map.set(e, []);
const entityUnits = map.get(e)!;
for (let i = 0, il = ug.units.length; i < il; ++i) {
entityUnits.push(ug.units[i]);
}
}
UnitsByEntity.set(structure, { value: map }, map);
return map;
}
export { MmcifStructure };
type MmcifStructure = typeof MmcifStructure
const MmcifStructure = PluginStateTransform.BuiltIn({
name: 'mmcif-structure',
display: { name: 'Mmcif Structure' },
from: PSO.Root,
to: PSO.Molecule.Structure,
params: {
structureRef: PD.Text(''),
entityId: PD.Text(''),
cellSize: PD.Numeric(500, { min: 0, max: 10000, step: 100 }),
}
})({
canAutoUpdate({ newParams }) {
return true;
},
apply({ a, params, dependencies }) {
return Task.create('Build Structure', async ctx => {
const parent = dependencies![params.structureRef].data as Structure;
const { entities } = parent.model;
const idx = entities.getEntityIndex(params.entityId);
const unitsByEntity = getUnitsByEntity(parent);
const units = unitsByEntity.get(idx) || [];
const unitCount = units.length;
let structure: Structure;
if (unitCount > 1 && units.every(u => u.conformation.operator.isIdentity)) {
const mergedUnits = partitionUnits(units, params.cellSize);
structure = Structure.create(mergedUnits);
} else {
structure = Structure.create(units);
}
const label = entities.data.pdbx_description.value(idx).join(', ') || 'model';
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
});
},
update({ newParams, oldParams }) {
return deepEqual(newParams, oldParams)
? StateTransformer.UpdateResult.Unchanged
: StateTransformer.UpdateResult.Recreate;
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});

View File

@@ -1,182 +0,0 @@
/**
* Copyright (c) 2023 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 { PluginStateObject } from '../../../../mol-plugin-state/objects';
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../../../mol-plugin/context';
import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/representation/spacefill';
import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../mol-state';
import { Clip } from '../../../../mol-util/clip';
import { Color } from '../../../../mol-util/color';
import { ColorNames } from '../../../../mol-util/color/names';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
import { MmcifAssembly, MmcifStructure } from './model';
function getSpacefillParams(color: Color, scaleFactor: number, graphics: GraphicsMode, clipVariant: Clip.Variant) {
const gmp = getGraphicsModeProps(graphics === 'custom' ? 'quality' : graphics);
return {
type: {
name: 'spacefill',
params: {
...SpacefillRepresentationProvider.defaultValues,
ignoreHydrogens: false,
instanceGranularity: false,
ignoreLight: true,
lodLevels: gmp.lodLevels.map(l => {
return {
...l,
stride: Math.max(1, Math.round(l.stride / Math.pow(scaleFactor, l.scaleBias)))
};
}),
quality: 'lowest', // avoid 'auto', triggers boundary calc
clip: {
variant: clipVariant,
objects: [],
},
clipPrimitive: true,
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
},
},
colorTheme: {
name: 'uniform',
params: {
value: color,
saturation: 0,
lightness: 0,
}
},
sizeTheme: {
name: 'physical',
params: {
value: 1,
}
},
};
}
export async function createMmcifHierarchy(plugin: PluginContext, trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory>) {
const builder = plugin.builders.structure;
const state = plugin.state.data;
const model = await builder.createModel(trajectory, { modelIndex: 0 });
const { data: entities, subtype } = model.data!.entities;
const sd = model.data?.sourceData;
if (MmcifFormat.is(sd)) {
const pdbId = sd.data.db.struct.entry_id.value(0);
MesoscaleState.set(plugin, {
description: sd.data.db.struct.title.value(0),
link: pdbId ? `https://www.rcsb.org/structure/${pdbId}` : ''
});
}
const spheresAvgRadius = new Map<string, number>();
if (model.data!.coarseHierarchy.isDefined) {
const spheresCount = new Map<string, number>();
const spheresEntity_id = model.data!.coarseHierarchy.spheres.entity_id;
const spheresRadius = model.data!.coarseConformation.spheres.radius;
for (let i = 0, il = spheresEntity_id.rowCount; i < il; ++i) {
const entitiId = spheresEntity_id.value(i);
const radius = spheresRadius[i];
if (!spheresCount.has(entitiId)) {
spheresCount.set(entitiId, 1);
spheresAvgRadius.set(entitiId, radius);
} else {
spheresCount.set(entitiId, spheresCount.get(entitiId)! + 1);
spheresAvgRadius.set(entitiId, spheresAvgRadius.get(entitiId)! + radius);
}
}
spheresAvgRadius.forEach((v, k) => {
spheresAvgRadius.set(k, v / spheresCount.get(k)!);
});
}
const entGroups = new Map<string, StateObjectSelector>();
const entIds = new Map<string, { idx: number, members: Map<number, number> }>();
const entColors = new Map<string, Color[]>();
const graphicsMode = MesoscaleState.get(plugin).graphics;
const groupParams = getMesoscaleGroupParams(graphicsMode);
const base = await state.build()
.to(model)
.apply(MmcifAssembly, { id: '' })
.commit();
const units = base.data!.units;
const willBeMerged = units.length > 1 && units.every(u => u.conformation.operator.isIdentity);
const clipVariant = willBeMerged ? 'pixel' : 'instance';
const entRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
const getEntityType = (i: number) => {
if (entities.type.value(i) === 'water') return 'water' as const;
return subtype.value(i) || 'unknown type';
};
for (let i = 0; i < entities._rowCount; i++) {
const t = getEntityType(i);
if (!entIds.has(t)) {
entIds.set(t, { idx: entIds.size, members: new Map() });
}
const cm = entIds.get(t)!;
cm.members.set(i, cm.members.size);
}
//
const baseEntColors = getDistinctBaseColors(entIds.size, 0);
const entIdEntries = Array.from(entIds.entries());
for (let i = 0; i < entIdEntries.length; ++i) {
const [t, m] = entIdEntries[i];
const groupColors = getDistinctGroupColors(m.members.size, baseEntColors[i], 20, 0);
entColors.set(t, groupColors);
}
for (let i = 0; i < entities._rowCount; i++) {
const t = getEntityType(i);
if (!entGroups.has(t)) {
const colorIdx = entIds.get(t)?.idx;
const color = colorIdx !== undefined ? baseEntColors[colorIdx] : ColorNames.white;
const group = await state.build()
.to(entRoot)
.applyOrUpdateTagged(`group:ent:${t}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `ent:${t}`, label: t, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
entGroups.set(t, group);
}
}
//
await state.transaction(async () => {
try {
const dependsOn = [base.ref];
plugin.animationLoop.stop({ noDraw: true });
let build: StateBuilder.Root | StateBuilder.To<any> = state.build();
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)) || 1;
build = build
.toRoot()
.apply(MmcifStructure, { structureRef: base.ref, entityId: entities.id.value(i) }, { dependsOn })
.apply(StructureRepresentation3D, getSpacefillParams(color, scaleFactor, graphicsMode, clipVariant), { tags: [`ent:${t}`] });
}
await build.commit();
} catch (e) {
console.error(e);
plugin.log.error(e);
} finally {
plugin.animationLoop.start();
}
}).run();
}

View File

@@ -1,138 +0,0 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
import { getMatrices, operatorGroupsProvider } from '../../../../mol-model-formats/structure/property/assembly';
import { Structure, StructureElement, StructureProperties, Trajectory, Unit } from '../../../../mol-model/structure';
import { Assembly } from '../../../../mol-model/structure/model/properties/symmetry';
import { PluginStateObject as SO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
import { Task } from '../../../../mol-task';
import { Table } from '../../../../mol-data/db';
import { mmCIF_Schema } from '../../../../mol-io/reader/cif/schema/mmcif';
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
import { arrayFind } from '../../../../mol-data/util';
import { StateObject, StateTransformer } from '../../../../mol-state';
import { CifField } from '../../../../mol-io/reader/cif';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { mergeUnits } from '../util';
import { deepEqual } from '../../../../mol-util';
export { StructureFromPetworld };
type StructureFromPetworld = typeof StructureFromPetworld
const StructureFromPetworld = PluginStateTransform.BuiltIn({
name: 'structure-from-petworld',
display: { name: 'Structure from PetWorld', description: 'Create a molecular structure from PetWorld models.' },
from: SO.Molecule.Trajectory,
to: SO.Molecule.Structure,
params: {
modelIndex: PD.Numeric(0),
entityIds: PD.Value<string[]>([]),
}
})({
apply({ a, params }) {
return Task.create('Build Structure', async ctx => {
const s = await buildModelsAssembly(a.data, '1', params.modelIndex, params.entityIds).runInContext(ctx);
if (!s || !MmcifFormat.is(s.model.sourceData)) return StateObject.Null;
const { frame } = s.model.sourceData.data;
const pdbx_model = frame.categories.pdbx_model.getField('name')!;
const label = pdbx_model.str(params.modelIndex);
const props = { label, description: Structure.elementDescription(s) };
return new SO.Molecule.Structure(s, props);
});
},
update({ newParams, oldParams }) {
return deepEqual(newParams, oldParams)
? StateTransformer.UpdateResult.Unchanged
: StateTransformer.UpdateResult.Recreate;
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});
function buildModelsAssembly(trajectory: Trajectory, asmName: string, modelIndex: number, entitiyIds: string[]) {
return Task.create('Build Models Assembly', async ctx => {
const model = await Task.resolveInContext(trajectory.getFrameAtIndex(modelIndex), ctx);
if (!MmcifFormat.is(model.sourceData)) return;
const { db, frame } = model.sourceData.data;
const PDB_model_num = frame.categories.pdbx_struct_assembly_gen.getField('PDB_model_num')!;
// hack to cache models assemblies
if (!(trajectory as any).__modelsAssemblies) {
(trajectory as any).__modelsAssemblies = createModelsAssemblies(db.pdbx_struct_assembly, db.pdbx_struct_assembly_gen as StructAssemblyGen, db.pdbx_struct_oper_list, PDB_model_num);
}
const modelsAssemblies = (trajectory as any).__modelsAssemblies as ModelsAssembly[];
const modelsAssembly = arrayFind(modelsAssemblies, ma => ma.assembly.id.toLowerCase() === asmName);
if (!modelsAssembly) throw new Error(`Models Assembly '${asmName}' is not defined.`);
const { assembly } = modelsAssembly;
const assembler = Structure.Builder();
const g = assembly.operatorGroups[modelIndex];
const structure = Structure.ofModel(model);
const l = StructureElement.Location.create(structure);
const units = structure.units.filter(u => {
l.unit = u;
l.element = u.elements[0];
return entitiyIds.includes(StructureProperties.entity.id(l));
});
const unit = mergeUnits(units, 0);
for (const oper of g.operators) {
assembler.addUnit(unit.kind, unit.model, oper, unit.elements, unit.traits | Unit.Trait.FastBoundary, unit.invariantId);
}
return assembler.getStructure();
});
}
//
type StructAssembly = Table<mmCIF_Schema['pdbx_struct_assembly']>
type StructAssemblyGen = Table<mmCIF_Schema['pdbx_struct_assembly_gen']>
type StructOperList = Table<mmCIF_Schema['pdbx_struct_oper_list']>
type ModelsAssembly = { assembly: Assembly, modelNums: number[] };
function createModelsAssemblies(pdbx_struct_assembly: StructAssembly, pdbx_struct_assembly_gen: StructAssemblyGen, pdbx_struct_oper_list: StructOperList, PDB_model_num: CifField): ReadonlyArray<ModelsAssembly> {
if (!pdbx_struct_assembly._rowCount) return [];
const matrices = getMatrices(pdbx_struct_oper_list);
const assemblies: ModelsAssembly[] = [];
for (let i = 0; i < pdbx_struct_assembly._rowCount; i++) {
assemblies[assemblies.length] = createModelsAssembly(pdbx_struct_assembly, pdbx_struct_assembly_gen, i, matrices, PDB_model_num);
}
return assemblies;
}
type Matrices = Map<string, Mat4>
type Generator = { assemblyId: string, expression: string, asymIds: string[] }
function createModelsAssembly(pdbx_struct_assembly: StructAssembly, pdbx_struct_assembly_gen: StructAssemblyGen, index: number, matrices: Matrices, PDB_model_num: CifField): ModelsAssembly {
const id = pdbx_struct_assembly.id.value(index);
const details = pdbx_struct_assembly.details.value(index);
const generators: Generator[] = [];
const modelNums: number[] = [];
const { assembly_id, oper_expression, asym_id_list } = pdbx_struct_assembly_gen;
for (let i = 0, _i = pdbx_struct_assembly_gen._rowCount; i < _i; i++) {
if (assembly_id.value(i) !== id) continue;
generators[generators.length] = {
assemblyId: id,
expression: oper_expression.value(i),
asymIds: asym_id_list.value(i)
};
modelNums[modelNums.length] = PDB_model_num.int(i);
}
const assembly = Assembly.create(id, details, operatorGroupsProvider(generators, matrices));
return { assembly, modelNums };
}

View File

@@ -1,134 +0,0 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { StateBuilder, StateObjectRef } from '../../../../mol-state';
import { StructureFromPetworld } from './model';
import { Color } from '../../../../mol-util/color';
import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/representation/spacefill';
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../../../mol-plugin/context';
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
import { ColorNames } from '../../../../mol-util/color/names';
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
import { Task } from '../../../../mol-task';
function getSpacefillParams(color: Color, graphics: GraphicsMode) {
const gmp = getGraphicsModeProps(graphics === 'custom' ? 'quality' : graphics);
return {
type: {
name: 'spacefill',
params: {
...SpacefillRepresentationProvider.defaultValues,
ignoreHydrogens: true,
instanceGranularity: true,
ignoreLight: true,
lodLevels: gmp.lodLevels,
quality: 'lowest', // avoid 'auto', triggers boundary calc
clip: {
variant: 'instance',
objects: [],
},
clipPrimitive: true,
approximate: gmp.approximate,
alphaThickness: gmp.alphaThickness,
},
},
colorTheme: {
name: 'uniform',
params: {
value: color,
saturation: 0,
lightness: 0,
}
},
sizeTheme: {
name: 'physical',
params: {
scale: 1,
}
},
};
}
export async function createPetworldHierarchy(plugin: PluginContext, trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory>) {
const cell = StateObjectRef.resolveAndCheck(plugin.state.data, trajectory);
const tr = cell?.obj?.data;
if (!cell || !tr) return;
if (!MmcifFormat.is(tr.representative.sourceData)) return;
const membrane: { modelIndex: number, entityIds: string[] }[] = [];
const other: { modelIndex: number, entityIds: string[] }[] = [];
for (let i = 0; i < tr.frameCount; ++i) {
const m = await Task.resolveInContext(tr.getFrameAtIndex(i));
// cannot use m.properties.structAsymMap because petworld models
// may assign the same asymId to multiple entities
const { label_asym_id, label_entity_id, _rowCount } = m.atomicHierarchy.chains;
const membraneIds: string[] = [];
const otherIds: string[] = [];
const seen = new Set<string>();
for (let i = 0; i < _rowCount; i ++) {
const entityId = label_entity_id.value(i);
if (seen.has(entityId)) continue;
const asymId = label_asym_id.value(i);
if (asymId.startsWith('MEM')) {
membraneIds.push(entityId);
} else {
otherIds.push(entityId);
}
seen.add(entityId);
}
if (membraneIds.length) {
membrane.push({ modelIndex: i, entityIds: membraneIds });
}
if (otherIds.length) {
other.push({ modelIndex: i, entityIds: otherIds });
}
}
const state = plugin.state.data;
const graphicsMode = MesoscaleState.get(plugin).graphics;
const groupParams = getMesoscaleGroupParams(graphicsMode);
const group = await state.build()
.toRoot()
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
await state.build()
.to(group)
.applyOrUpdateTagged(`group:ent:mem`, MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit();
const colors = getDistinctBaseColors(other.length, 0);
await state.transaction(async () => {
try {
plugin.animationLoop.stop({ noDraw: true });
let build: StateBuilder.Root | StateBuilder.To<any> = state.build();
for (let i = 0, il = membrane.length; i < il; ++i) {
build = build
.to(cell)
.apply(StructureFromPetworld, membrane[i])
.apply(StructureRepresentation3D, getSpacefillParams(ColorNames.lightgrey, graphicsMode), { tags: [`ent:mem`] });
}
for (let i = 0, il = other.length; i < il; ++i) {
build = build
.to(cell)
.apply(StructureFromPetworld, other[i])
.apply(StructureRepresentation3D, getSpacefillParams(colors[i], graphicsMode), { tags: [`ent:`] });
}
await build.commit();
} catch (e) {
console.error(e);
plugin.log.error(e);
} finally {
plugin.animationLoop.start();
}
}).run();
}

View File

@@ -1,592 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginStateObject as PSO, PluginStateTransform } from '../../../mol-plugin-state/objects';
import { PluginContext } from '../../../mol-plugin/context';
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 { Clip } from '../../../mol-util/clip';
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { ParamMapping } from '../../../mol-util/param-mapping';
import { EntityNode } from '../ui/entities';
import { DistinctColorsProps, distinctColors } from '../../../mol-util/color/distinct';
import { Sphere3D } from '../../../mol-math/geometry';
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';
function getHueRange(hue: number, variability: number) {
let min = hue - variability;
const minOverflow = (min < 0 ? -min : 0);
let max = hue + variability;
if (max > 360) min -= max - 360;
max += minOverflow;
return [Math.max(0, min), Math.min(360, max)] as [number, number];
}
function getGrayscaleColors(count: number, luminance: number, variability: number) {
const out: Color[] = [];
for (let i = 0; i < count; ++ i) {
const l = saturate(luminance / 100);
const v = saturate(variability / 180) * Math.random();
const s = Math.random() > 0.5 ? 1 : -1;
const d = Math.abs(l + s * v) % 1;
out[i] = Color.fromNormalizedRgb(d, d, d);
}
return out;
}
export function getDistinctGroupColors(count: number, color: Color, variability: number, shift: number, props?: Partial<DistinctColorsProps>) {
const hcl = Hcl.fromColor(Hcl(), color);
if (isNaN(hcl[0])) {
return getGrayscaleColors(count, hcl[2], variability);
}
if (count === 1) {
hcl[1] = 65;
hcl[2] = 55;
return [Hcl.toColor(hcl)];
}
const colors = distinctColors(count, {
hue: getHueRange(hcl[0], variability),
chroma: [30, 100],
luminance: [50, 100],
clusteringStepCount: 0,
minSampleCount: 1000,
sampleCountFactor: 100,
sort: 'none',
...props,
});
if (shift !== 0) {
const offset = Math.floor(shift / 100 * count);
return [...colors.slice(offset), ...colors.slice(0, offset)];
} else {
return colors;
}
}
const Colors = [0x377eb8, 0xe41a1c, 0x4daf4a, 0x984ea3, 0xff7f00, 0xffff33, 0xa65628, 0xf781bf] as Color[];
export function getDistinctBaseColors(count: number, shift: number, props?: Partial<DistinctColorsProps>): Color[] {
let colors: Color[];
if (count <= Colors.length) {
colors = Colors.slice(0, count).map(e => Array.isArray(e) ? e[0] : e);
} else {
colors = distinctColors(count, {
hue: [1, 360],
chroma: [25, 100],
luminance: [30, 100],
clusteringStepCount: 0,
minSampleCount: 1000,
sampleCountFactor: 100,
sort: 'none',
...props,
});
}
if (shift !== 0) {
const offset = Math.floor(shift / 100 * count);
return [...colors.slice(offset), ...colors.slice(0, offset)];
} else {
return colors;
}
}
export const ColorParams = {
type: PD.Select('generate', PD.arrayToOptions(['generate', 'uniform', 'custom'])),
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type === 'custom' }),
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'generate' }),
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }, { hideIf: p => p.type === 'custom' }),
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { hideIf: p => p.type === 'custom' }),
};
export type ColorProps = PD.Values<typeof ColorParams>
export const ColorValueParam = PD.Color(Color(0xFFFFFF));
export const RootParams = {
type: PD.Select('custom', PD.arrayToOptions(['group-generate', 'group-uniform', 'generate', 'uniform', 'custom'])),
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type !== 'uniform' }),
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'group-generate' }),
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }, { hideIf: p => p.type === 'custom' }),
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { hideIf: p => p.type === 'custom' }),
};
export const LightnessParams = {
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
};
export const DimLightness = 6;
export const OpacityParams = {
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
};
export const PatternParams = {
frequency: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
amplitude: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
};
export const LodParams = {
lodLevels: Spheres.Params.lodLevels,
cellSize: Spheres.Params.cellSize,
batchSize: Spheres.Params.batchSize,
approximate: Spheres.Params.approximate,
};
export const SimpleClipParams = {
type: PD.Select('none', PD.objectToOptions(Clip.Type, t => stringToWords(t))),
invert: PD.Boolean(false),
position: PD.Group({
x: PD.Numeric(0, { min: -100, max: 100, step: 1 }, { immediateUpdate: true }),
y: PD.Numeric(0, { min: -100, max: 100, step: 1 }, { immediateUpdate: true }),
z: PD.Numeric(0, { min: -100, max: 100, step: 1 }, { immediateUpdate: true }),
}, { hideIf: g => g.type === 'none', isExpanded: true }),
rotation: PD.Group({
axis: PD.Vec3(Vec3.create(1, 0, 0)),
angle: PD.Numeric(0, { min: -180, max: 180, step: 1 }, { immediateUpdate: true }),
}, { hideIf: g => g.type === 'none', isExpanded: true }),
scale: PD.Group({
x: PD.Numeric(100, { min: 0, max: 100, step: 1 }, { immediateUpdate: true }),
y: PD.Numeric(100, { min: 0, max: 100, step: 1 }, { immediateUpdate: true }),
z: PD.Numeric(100, { min: 0, max: 100, step: 1 }, { immediateUpdate: true }),
}, { hideIf: g => ['none', 'plane'].includes(g.type), isExpanded: true }),
};
export type SimpleClipParams = typeof SimpleClipParams
export type SimpleClipProps = PD.Values<SimpleClipParams>
export function getClipObjects(values: SimpleClipProps, boundingSphere: Sphere3D): Clip.Props['objects'] {
const { center, radius } = boundingSphere;
const position = Vec3.clone(center);
Vec3.add(position, position, Vec3.create(
radius * values.position.x / 100,
radius * values.position.y / 100,
radius * values.position.z / 100
));
const scale = Vec3.create(values.scale.x, values.scale.y, values.scale.z);
Vec3.scale(scale, scale, 2 * radius / 100);
return [{
type: values.type,
invert: values.invert,
position,
scale,
rotation: values.rotation
}];
}
export function createClipMapping(node: EntityNode) {
return ParamMapping({
params: SimpleClipParams,
target: (ctx: PluginContext) => {
return node.clipValue;
}
})({
values(props, ctx) {
if (!props || props.objects.length === 0) {
return {
type: 'none',
invert: false,
position: { x: 0, y: 0, z: 0 },
rotation: { axis: Vec3.create(1, 0, 0), angle: 0 },
scale: { x: 100, y: 100, z: 100 },
};
}
const { center, radius } = node.plugin.canvas3d!.boundingSphere;
const { invert, position, scale, rotation, type } = props.objects[0];
const p = Vec3.clone(position);
Vec3.sub(p, p, center);
Vec3.scale(p, p, 100 / radius);
Vec3.round(p, p);
const s = Vec3.clone(scale);
Vec3.scale(s, s, 100 / radius / 2);
Vec3.round(s, s);
return {
type,
invert,
position: { x: p[0], y: p[1], z: p[2] },
rotation,
scale: { x: s[0], y: s[1], z: s[2] },
};
},
update: (s, props) => {
if (!props) return;
const clipObjects = getClipObjects(s, node.plugin.canvas3d!.boundingSphere);
props.objects = clipObjects;
},
apply: async (props, ctx) => {
if (props) node.updateClip(props);
}
});
}
export const MesoscaleGroupParams = {
root: PD.Value<boolean>(false, { isHidden: true }),
index: PD.Value<number>(-1, { isHidden: true }),
tag: PD.Value<string>('', { isHidden: true }),
label: PD.Value<string>('', { isHidden: true }),
hidden: PD.Boolean(false),
color: PD.Group(RootParams),
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
lod: PD.Group(LodParams),
clip: PD.Group(SimpleClipParams),
};
export type MesoscaleGroupProps = PD.Values<typeof MesoscaleGroupParams>;
export class MesoscaleGroupObject extends PSO.Create({ name: 'Mesoscale Group', typeClass: 'Object' }) { }
export const MesoscaleGroup = PluginStateTransform.BuiltIn({
name: 'mesoscale-group',
display: { name: 'Mesoscale Group' },
from: [PSO.Root, MesoscaleGroupObject],
to: MesoscaleGroupObject,
params: MesoscaleGroupParams,
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Apply Mesoscale Group', async () => {
return new MesoscaleGroupObject({}, { label: params.label });
});
},
});
export function getMesoscaleGroupParams(graphicsMode: GraphicsMode): MesoscaleGroupProps {
const groupParams = PD.getDefaultValues(MesoscaleGroupParams);
if (graphicsMode === 'custom') return groupParams;
return {
...groupParams,
lod: {
...groupParams.lod,
...getGraphicsModeProps(graphicsMode),
}
};
}
//
export type LodLevels = typeof SpacefillRepresentationProvider.defaultValues['lodLevels']
export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): LodLevels {
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: 2000, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 2000, maxDistance: 8000, overlap: 0, stride: 10, scaleBias: 3 },
{ minDistance: 8000, maxDistance: 20000, overlap: 0, stride: 50, scaleBias: 2.5 },
{ minDistance: 20000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
];
default:
assertUnreachable(graphicsMode);
}
}
export type GraphicsMode = 'ultra' | 'quality' | 'balanced' | 'performance' | 'custom';
export function getGraphicsModeProps(graphicsMode: Exclude<GraphicsMode, 'custom'>) {
return {
lodLevels: getLodLevels(graphicsMode),
approximate: graphicsMode !== 'quality' && graphicsMode !== 'ultra',
alphaThickness: graphicsMode === 'performance' ? 15 : 12,
};
}
export function setGraphicsCanvas3DProps(ctx: PluginContext, graphics: GraphicsMode) {
const pixelScale = graphics === 'balanced' ? 0.75
: graphics === 'performance' ? 0.5 : 1;
ctx.canvas3dContext?.setProps({ pixelScale });
ctx.canvas3d?.setProps({
postprocessing: {
sharpening: pixelScale < 1 ? {
name: 'on',
params: { sharpness: 0.5, denoise: true }
} : { name: 'off', params: {} }
}
});
}
//
export const MesoscaleStateParams = {
filter: PD.Value<string>('', { isHidden: true }),
graphics: PD.Select('quality', PD.arrayToOptions(['ultra', 'quality', 'balanced', 'performance', 'custom'] as GraphicsMode[])),
description: PD.Value<string>('', { isHidden: true }),
link: PD.Value<string>('', { isHidden: true }),
};
export class MesoscaleStateObject extends PSO.Create<MesoscaleState>({ name: 'Mesoscale State', typeClass: 'Object' }) { }
const MesoscaleStateTransform = PluginStateTransform.BuiltIn({
name: 'mesoscale-state',
display: { name: 'Mesoscale State' },
from: PSO.Root,
to: MesoscaleStateObject,
params: MesoscaleStateParams,
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Apply Mesoscale State', async () => {
return new MesoscaleStateObject(params);
});
},
});
export { MesoscaleState };
type MesoscaleState = PD.Values<typeof MesoscaleStateParams>;
const MesoscaleState = {
async init(ctx: PluginContext) {
const cell = ctx.state.data.selectQ(q => q.ofType(MesoscaleStateObject))[0];
if (cell) throw new Error('MesoscaleState already initialized');
const customState = ctx.customState as MesoscaleExplorerState;
const state = await ctx.state.data.build().toRoot().apply(MesoscaleStateTransform, {
filter: '',
graphics: customState.graphicsMode,
}).commit();
customState.stateRef = state.ref;
},
get(ctx: PluginContext): MesoscaleState {
const ref = this.ref(ctx);
return ctx.state.data.tryGetCellData<MesoscaleStateObject>(ref);
},
async set(ctx: PluginContext, props: Partial<MesoscaleState>) {
const ref = this.ref(ctx);
await ctx.state.data.build().to(ref).update(MesoscaleStateTransform, old => Object.assign(old, props)).commit();
},
ref(ctx: PluginContext): string {
const ref = (ctx.customState as MesoscaleExplorerState).stateRef;
if (!ref) throw new Error('MesoscaleState not initialized');
return ref;
},
has(ctx: PluginContext): boolean {
const ref = (ctx.customState as MesoscaleExplorerState).stateRef || '';
return ctx.state.data.cells.has(ref) ? true : false;
},
};
//
export function getRoots(plugin: PluginContext): StateSelection.CellSeq<StateObjectCell<MesoscaleGroupObject>> {
const s = plugin.customState as MesoscaleExplorerState;
if (!s.stateCache.roots) {
s.stateCache.roots = plugin.state.data.select(StateSelection.Generators.rootsOfType(MesoscaleGroupObject));
}
return s.stateCache.roots;
}
export function getGroups(plugin: PluginContext, tag?: string): StateSelection.CellSeq<StateObjectCell<MesoscaleGroupObject>> {
const s = plugin.customState as MesoscaleExplorerState;
const k = `groups-${tag || ''}`;
if (!s.stateCache[k]) {
const selector = tag !== undefined
? StateSelection.Generators.ofTransformer(MesoscaleGroup).withTag(tag)
: StateSelection.Generators.ofTransformer(MesoscaleGroup);
s.stateCache[k] = plugin.state.data.select(selector);
}
return s.stateCache[k];
}
function _getAllGroups(plugin: PluginContext, tag: string | undefined, list: StateObjectCell[]) {
const groups = getGroups(plugin, tag);
list.push(...groups);
for (const g of groups) {
_getAllGroups(plugin, g.params?.values.tag, list);
}
return list;
}
export function getAllGroups(plugin: PluginContext, tag?: string) {
return _getAllGroups(plugin, tag, []);
}
export function getAllLeafGroups(plugin: PluginContext, tag: string) {
const allGroups = getAllGroups(plugin, tag);
allGroups.sort((a, b) => a.params?.values.index - b.params?.values.index);
return allGroups.filter(g => {
return getEntities(plugin, g.params?.values.tag).length > 0;
});
}
type EntityCells = StateSelection.CellSeq<StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D>>
export function getEntities(plugin: PluginContext, tag?: string): EntityCells {
const s = plugin.customState as MesoscaleExplorerState;
const k = `entities-${tag || ''}`;
if (!s.stateCache[k]) {
const structureSelector = tag !== undefined
? StateSelection.Generators.ofTransformer(StructureRepresentation3D).withTag(tag)
: StateSelection.Generators.ofTransformer(StructureRepresentation3D);
const shapeSelector = tag !== undefined
? StateSelection.Generators.ofTransformer(ShapeRepresentation3D).withTag(tag)
: StateSelection.Generators.ofTransformer(ShapeRepresentation3D);
s.stateCache[k] = [
...plugin.state.data.select(structureSelector).filter(c => c.obj!.data.sourceData.elementCount > 0),
...plugin.state.data.select(shapeSelector),
];
}
return s.stateCache[k];
}
function getFilterMatcher(filter: string) {
return filter.startsWith('"') && filter.endsWith('"')
? new RegExp(`^${escapeRegExp(filter.substring(1, filter.length - 1))}$`, 'g')
: new RegExp(escapeRegExp(filter), 'gi');
}
export function getFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
const matcher = getFilterMatcher(filter);
return getEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
}
function _getAllEntities(plugin: PluginContext, tag: string | undefined, list: EntityCells) {
list.push(...getEntities(plugin, tag));
for (const g of getGroups(plugin, tag)) {
_getAllEntities(plugin, g.params?.values.tag, list);
}
return list;
}
export function getAllEntities(plugin: PluginContext, tag?: string) {
return _getAllEntities(plugin, tag, []);
}
export function getAllFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
const matcher = getFilterMatcher(filter);
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
}
export function getEntityLabel(plugin: PluginContext, cell: StateObjectCell) {
return StateObjectRef.resolve(plugin.state.data, cell.transform.parent)?.obj?.label || 'Entity';
}
//
export async function updateColors(plugin: PluginContext, values: PD.Values, tag: string, filter: string) {
const update = plugin.state.data.build();
const { type, value, shift, lightness, alpha } = values;
if (type === 'group-generate' || type === 'group-uniform') {
const groups = getAllLeafGroups(plugin, tag);
const baseColors = getDistinctBaseColors(groups.length, shift);
for (let i = 0; i < groups.length; ++i) {
const g = groups[i];
const entities = getFilteredEntities(plugin, g.params?.values.tag, filter);
let groupColors: Color[] = [];
if (type === 'group-generate') {
const c = g.params?.values.color;
groupColors = getDistinctGroupColors(entities.length, baseColors[i], c.variability, c.shift);
}
for (let j = 0; j < entities.length; ++j) {
const c = type === 'group-generate' ? groupColors[j] : baseColors[i];
update.to(entities[j]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
} else if (old.coloring) {
old.coloring.params.color = c;
old.coloring.params.lightness = lightness;
old.alpha = alpha;
old.xrayShaded = alpha < 1 ? true : false;
}
});
}
update.to(g.transform.ref).update(old => {
old.color.type = type === 'group-generate' ? 'generate' : 'uniform';
old.color.value = baseColors[i];
old.color.lightness = lightness;
old.color.alpha = alpha;
});
}
} else if (type === 'generate' || type === 'uniform') {
const entities = getAllFilteredEntities(plugin, tag, filter);
let groupColors: Color[] = [];
if (type === 'generate') {
groupColors = getDistinctBaseColors(entities.length, shift);
}
for (let j = 0; j < entities.length; ++j) {
const c = type === 'generate' ? groupColors[j] : value;
update.to(entities[j]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
} else if (old.coloring) {
old.coloring.params.color = c;
old.coloring.params.lightness = lightness;
old.alpha = alpha;
old.xrayShaded = alpha < 1 ? true : false;
}
});
}
const others = getAllLeafGroups(plugin, tag);
for (const o of others) {
update.to(o).update(old => {
old.color.type = type === 'generate' ? 'custom' : 'uniform';
old.color.value = value;
old.color.lightness = lightness;
old.color.alpha = alpha;
});
}
}
await update.commit();
};
export function expandAllGroups(plugin: PluginContext) {
for (const g of getAllGroups(plugin)) {
if (g.state.isCollapsed) {
plugin.state.data.updateCellState(g.transform.ref, { isCollapsed: false });
}
}
};

View File

@@ -1,87 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { OrderedSet, SortedArray } from '../../../mol-data/int';
import { Box3D, GridLookup3D, PositionData, Sphere3D } from '../../../mol-math/geometry';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { ElementIndex, Unit } from '../../../mol-model/structure';
export function mergeUnits(units: readonly Unit[], id: number): Unit {
const u = units[0];
let start = -1 as ElementIndex, end = -1 as ElementIndex;
let elements = SortedArray.Empty as SortedArray<ElementIndex>;
for (let i = 0, il = units.length; i < il; ++i) {
const e = units[i].elements;
if (SortedArray.isRange(e)) {
if (end !== -1 && e[0] === end + 1) {
// extend range
end = e[e.length - 1];
} else {
if (end !== -1) {
// pending range
elements = SortedArray.union(elements, SortedArray.ofRange(start, end));
}
// new range
start = e[0];
end = e[e.length - 1];
}
} else {
if (end !== -1) {
// pending range
elements = SortedArray.union(elements, SortedArray.ofRange(start, end));
start = -1 as ElementIndex, end = -1 as ElementIndex;
}
elements = SortedArray.union(elements, e);
}
}
if (end !== -1) {
// pending range
elements = SortedArray.union(elements, SortedArray.ofRange(start, end));
}
return Unit.create(id, id, 0, u.traits | Unit.Trait.MultiChain, u.kind, u.model, u.conformation.operator, elements);
}
export function partitionUnits(units: readonly Unit[], cellSize: number) {
const unitCount = units.length;
const mergedUnits: Unit[] = [];
const box = Box3D.setEmpty(Box3D());
const x = new Float32Array(unitCount);
const y = new Float32Array(unitCount);
const z = new Float32Array(unitCount);
const indices = OrderedSet.ofBounds(0, unitCount);
for (let i = 0, il = unitCount; i < il; ++i) {
const v = units[i].boundary.sphere.center;
x[i] = v[0];
y[i] = v[1];
z[i] = v[2];
Box3D.add(box, v);
}
Box3D.expand(box, box, Vec3.create(1, 1, 1));
const positionData: PositionData = { x, y, z, indices };
const boundary = { box, sphere: Sphere3D.fromBox3D(Sphere3D(), box) };
const lookup = GridLookup3D(positionData, boundary, Vec3.create(cellSize, cellSize, cellSize));
const { array, offset, count } = lookup.buckets;
for (let i = 0, il = offset.length; i < il; ++i) {
const start = offset[i];
const size = count[i];
const cellUnits: Unit[] = [];
for (let j = start, jl = start + size; j < jl; ++j) {
cellUnits.push(units[array[j]]);
}
mergedUnits.push(mergeUnits(cellUnits, i));
}
return mergedUnits;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,101 +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* Mesoscale Explorer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
hr {
margin: 10px;
}
h1, h2, h3, h4, h5 {
margin-top: 5px;
margin-bottom: 3px;
}
button {
padding: 2px;
}
#app {
position: absolute;
left: 100px;
top: 100px;
width: 800px;
height: 600px;
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
function getParam(name, regex) {
var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
}
var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
if (debugMode) molstar.setDebugMode(debugMode);
var timingMode = getParam('timing-mode', '[^&]+').trim() === '1';
if (timingMode) molstar.setTimingMode(timingMode);
var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
var graphicsMode = getParam('graphics-mode', '[^&]+').trim().toLowerCase();
molstar.MesoscaleExplorer.create('app', {
layoutShowControls: !hideControls,
viewportShowExpand: false,
preferWebgl1: preferWebgl1,
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
graphicsMode: graphicsMode || 'quality',
}).then(me => {
var example = getParam('example', '[^&]+').trim();
if (example) {
me.loadExample(example);
return;
}
var url = getParam('url', '[^&]+').trim();
var type = getParam('type', '[^&]+').trim();
if (url && type) {
me.loadUrl(url, type);
return;
}
var pdb = getParam('pdb', '[^&]+').trim();
if (pdb) {
me.loadPdb(pdb);
return;
}
var pdbdev = getParam('pdbdev', '[^&]+').trim();
if (pdbdev) {
me.loadPdbDev(pdbdev);
return;
}
window.addEventListener('unload', () => {
// to aid GC
me.dispose();
});
});
</script>
<!-- __MOLSTAR_ANALYTICS__ -->
</body>
</html>

View File

@@ -1,10 +0,0 @@
/**
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import './favicon.ico';
import './index.html';
require('./style.scss');
export * from './app';

View File

@@ -1,33 +0,0 @@
$default-background: #2D3E50;
$font-color: #EDF1F2;
$hover-font-color: #3B9AD9;
$entity-current-font-color: #FFFFFF;
$msp-btn-remove-background: #BF3A31;
$msp-btn-remove-hover-font-color:#ffffff;
$msp-btn-commit-on-font-color: #ffffff;
$entity-badge-font-color: #ccd4e0;
// used in LOG
$log-message: #0CCA5D;
$log-info: #5E3673;
$log-warning: #FCC937;
$log-error: #FD354B;
$logo-background: rgba(0,0,0,0.75);
@function color-lower-contrast($color, $amount) {
@return darken($color, $amount);
}
@function color-increase-contrast($color, $amount) {
@return lighten($color, $amount);
}
@import 'mol-plugin-ui/skin/base/base';
a {
color: $font-color;
&:hover {
color: $hover-font-color;
}
}

View File

@@ -1,953 +0,0 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { Button, ControlGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
import { ArrowDropDownSvg, ArrowRightSvg, CloseSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, ContentCutSvg, BrushSvg, SearchSvg } from '../../../mol-plugin-ui/controls/icons';
import { PluginCommands } from '../../../mol-plugin/commands';
import { State, StateObjectCell, StateSelection, StateTransformer } from '../../../mol-state';
import { ParameterControls, ParameterMappingControl, ParamOnChange, SelectControl } from '../../../mol-plugin-ui/controls/parameters';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Clip } from '../../../mol-util/clip';
import { StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
import { Color } from '../../../mol-util/color';
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, 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 } from '../data/state';
import React from 'react';
import { MesoscaleExplorerState } from '../app';
import { StructureElement } from '../../../mol-model/structure/structure/element';
import { PluginStateObject as PSO } from '../../../mol-plugin-state/objects';
import { Structure } from '../../../mol-model/structure';
import { PluginContext } from '../../../mol-plugin/context';
import { Sphere3D } from '../../../mol-math/geometry';
function centerLoci(plugin: PluginContext, loci: Loci, durationMs = 250) {
const { canvas3d } = plugin;
if (!canvas3d) return;
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
const snapshot = canvas3d.camera.getCenter(sphere.center);
canvas3d.requestCameraReset({ durationMs, snapshot });
}
export class ModelInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
state = {
isDisabled: false,
};
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
});
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
if (!this.state.isDisabled && MesoscaleState.has(this.plugin) && MesoscaleState.ref(this.plugin) === e.ref) {
this.forceUpdate();
}
});
}
get info() {
if (!MesoscaleState.has(this.plugin)) return;
const state = MesoscaleState.get(this.plugin);
if (!state.description && !state.link) return;
return {
description: state.description,
link: state.link,
};
}
render() {
const info = this.info;
return info && <>
<div className='msp-help-text'>
<div>{info.description}</div>
<div><a href={info.link} target='_blank'>Source</a></div>
</div>
</>;
}
}
const SelectionStyleParam = PD.Select('color+outline', PD.objectToOptions({
'color+outline': 'Color & Outline',
'color': 'Color',
'outline': 'Outline'
} as const));
type SelectionStyle = typeof SelectionStyleParam['defaultValue']
export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
state = {
isDisabled: false,
};
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
});
this.subscribe(this.plugin.managers.structure.selection.events.changed, e => {
if (!this.state.isDisabled) {
this.forceUpdate();
}
});
}
get info() {
const info: { label: string, key: string }[] = [];
this.plugin.managers.structure.selection.entries.forEach((e, k) => {
if (StructureElement.Loci.is(e.selection) && !StructureElement.Loci.isEmpty(e.selection)) {
const cell = this.plugin.helpers.substructureParent.get(e.selection.structure);
info.push({
label: cell?.obj?.label || 'Unknown',
key: k,
});
}
});
return info;
}
find(label: string) {
MesoscaleState.set(this.plugin, { filter: `"${label}"` });
if (label) expandAllGroups(this.plugin);
};
remove(key: string) {
const e = this.plugin.managers.structure.selection.entries.get(key);
if (!e) return;
const loci = Structure.toStructureElementLoci(e.selection.structure);
this.plugin.managers.interactivity.lociSelects.deselect({ loci }, false);
}
center(key: string) {
const e = this.plugin.managers.structure.selection.entries.get(key);
if (!e) return;
const loci = Structure.toStructureElementLoci(e.selection.structure);
centerLoci(this.plugin, loci);
}
get selection() {
const info = this.info;
if (!info.length) return <>
<div className='msp-help-text'>
<div>Use <i>ctrl+left click</i> to select entities, either on the 3D canvas or in the tree below</div>
</div>
</>;
return <>
{info.map((entry, index) => {
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={this.state.isDisabled}
onClick={() => this.center(entry.key)}
>
<span title={entry.label}>{entry.label}</span>
</Button>;
const find = <IconButton svg={SearchSvg} toggleState={false} disabled={this.state.isDisabled} small onClick={() => this.find(entry.label)} />;
const remove = <IconButton svg={CloseSvg} toggleState={false} disabled={this.state.isDisabled} onClick={() => this.remove(entry.key)} />;
return <div key={index} className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${1 * 10 + 5}px` }}>
{label}
{find}
{remove}
</div>;
})}
</>;
}
get style() {
const p = this.plugin.canvas3d?.props;
if (!p) return;
if (p.renderer.dimStrength === 1 && p.marking.enabled) return 'color+outline';
if (p.renderer.dimStrength === 1) return 'color';
if (p.marking.enabled) return 'outline';
}
setStyle(value: SelectionStyle) {
if (value.includes('color') && value.includes('outline')) {
this.plugin.canvas3d?.setProps({
renderer: {
dimStrength: 1,
},
marking: {
enabled: true
}
});
} else if (value.includes('color')) {
this.plugin.canvas3d?.setProps({
renderer: {
dimStrength: 1,
},
marking: {
enabled: false
}
});
} else if (value.includes('outline')) {
this.plugin.canvas3d?.setProps({
renderer: {
dimStrength: 0,
selectStrength: 0.3,
},
marking: {
enabled: true
}
});
} else {
this.plugin.canvas3d?.setProps({
renderer: {
dimStrength: 0,
selectStrength: 0,
},
marking: {
enabled: false
}
});
}
this.forceUpdate();
}
renderStyle() {
const style = this.style || '';
return <div style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Style'} param={SelectionStyleParam} value={style} onChange={(e) => { this.setStyle(e.value); }} />
</div>;
}
render() {
return <>
{this.renderStyle()}
{this.selection}
</>;
}
}
export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean }> {
filterRef = React.createRef<HTMLInputElement>();
prevFilter = '';
filterFocus = false;
state = {
isDisabled: false,
};
componentDidMount() {
this.subscribe(this.plugin.state.events.object.created, e => {
this.forceUpdate();
});
this.subscribe(this.plugin.state.events.object.removed, e => {
this.forceUpdate();
});
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
});
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
if (!this.state.isDisabled && this.roots.some(r => e.cell === r) || (MesoscaleState.has(this.plugin) && MesoscaleState.ref(this.plugin) === e.ref)) {
this.forceUpdate();
}
});
}
componentDidUpdate(): void {
const filter = this.filter;
if (this.filterFocus) {
this.filterRef.current?.focus();
this.prevFilter = filter;
}
}
get roots() {
return getRoots(this.plugin);
}
setGroupBy = (value: number) => {
this.roots.forEach((c, i) => {
if (c.state.isHidden && value === i || !c.state.isHidden && value !== i) {
PluginCommands.State.ToggleVisibility(this.plugin, { state: c.parent!, ref: c.transform.ref });
}
});
};
get groupBy() {
const roots = this.roots;
for (let i = 0, il = roots.length; i < il; ++i) {
if (!roots[i].state.isHidden) return i;
}
return 0;
}
setFilter = (value: string) => {
this.filterFocus = true;
const filter = value.trim().replace(/\s+/gi, ' ');
MesoscaleState.set(this.plugin, { filter });
if (filter) expandAllGroups(this.plugin);
};
get filter() {
return MesoscaleState.has(this.plugin) ? MesoscaleState.get(this.plugin).filter : '';
}
setGraphics = (graphics: GraphicsMode) => {
MesoscaleState.set(this.plugin, { graphics });
(this.plugin.customState as MesoscaleExplorerState).graphicsMode = graphics;
if (graphics === 'custom') return;
const update = this.plugin.state.data.build();
const { lodLevels, approximate, alphaThickness } = getGraphicsModeProps(graphics);
for (const r of getAllEntities(this.plugin)) {
update.to(r).update(old => {
if (old.type) {
old.type.params.lodLevels = lodLevels;
old.type.params.approximate = approximate;
old.type.params.alphaThickness = alphaThickness;
}
});
}
for (const g of getAllGroups(this.plugin)) {
update.to(g).update(old => {
old.lod.lodLevels = lodLevels;
old.lod.approximate = approximate;
});
}
update.commit();
setGraphicsCanvas3DProps(this.plugin, graphics);
};
get graphics() {
const customState = this.plugin.customState as MesoscaleExplorerState;
return MesoscaleState.has(this.plugin) ? MesoscaleState.get(this.plugin).graphics : customState.graphicsMode;
}
renderGraphics() {
const graphics = this.graphics;
return <div style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Graphics'} param={MesoscaleStateParams.graphics} value={`${graphics}`} onChange={(e) => { this.setGraphics(e.value); }} />
</div>;
}
render() {
const roots = this.roots;
if (roots.length === 0 || !MesoscaleState.has(this.plugin)) {
return <>
{this.renderGraphics()}
</>;
}
const disabled = this.state.isDisabled;
const groupBy = this.groupBy;
const options: [string, string][] = [];
roots.forEach((c, i) => {
options.push([`${i}`, c.obj!.label]);
});
const groupParam = PD.Select(options[0][0], options);
const root = roots.length === 1 ? roots[0] : roots[groupBy];
const filter = this.filter;
return <>
{this.renderGraphics()}
<div className={`msp-flex-row msp-control-row`} style={{ margin: '5px', marginBottom: '10px' }}>
<input type='text' ref={this.filterRef}
value={filter}
placeholder='Search'
onChange={e => this.setFilter(e.target.value)}
disabled={disabled}
onBlur={() => this.filterFocus = false}
/>
<IconButton svg={CloseSvg} toggleState={false} disabled={disabled} onClick={() => this.setFilter('')} />
</div>
{options.length > 1 && <div style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Group By'} param={groupParam} value={`${groupBy}`} onChange={(e) => { this.setGroupBy(parseInt(e.value)); }} />
</div>}
<GroupNode filter={filter} cell={root} depth={0} />
</>;
}
}
class Node<P extends {}, S extends { isDisabled: boolean }> extends PluginUIComponent<P & { cell: StateObjectCell, depth: number }, S> {
is(e: State.ObjectEvent) {
return e.ref === this.ref && e.state === this.props.cell.parent;
}
get ref() {
return this.props.cell.transform.ref;
}
get cell() {
return this.props.cell;
}
get roots() {
return getRoots(this.plugin);
}
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
});
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
if (!this.state.isDisabled && this.is(e)) {
this.forceUpdate();
}
});
}
}
export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean, action?: 'color' | 'clip' | 'root', isDisabled: boolean }> {
state = {
isCollapsed: !!this.props.cell.state.isCollapsed,
action: undefined,
isDisabled: false,
};
toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
PluginCommands.State.ToggleExpanded(this.plugin, { state: this.cell.parent!, ref: this.ref });
};
toggleColor = (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
this.setState({ action: this.state.action === 'color' ? undefined : 'color' });
};
toggleClip = () => {
this.setState({ action: this.state.action === 'clip' ? undefined : 'clip' });
};
toggleRoot = () => {
this.setState({ action: this.state.action === 'root' ? undefined : 'root' });
};
highlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
for (const r of this.allFilteredEntities) {
const repr = r.obj?.data.repr;
if (repr) {
this.plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
}
};
clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
e.currentTarget.blur();
};
get groups() {
return getGroups(this.plugin, this.cell.params?.values.tag);
}
get allGroups() {
const allGroups = getAllGroups(this.plugin, this.cell.params?.values.tag);
allGroups.push(this.cell);
return allGroups;
}
get entities() {
return getEntities(this.plugin, this.cell.params?.values.tag);
}
get filteredEntities() {
return getFilteredEntities(this.plugin, this.cell.params?.values.tag, this.props.filter);
}
get allEntities() {
return getAllEntities(this.plugin, this.cell.params?.values.tag);
}
get allFilteredEntities() {
return getAllFilteredEntities(this.plugin, this.cell.params?.values.tag, this.props.filter);
}
toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
PluginCommands.State.ToggleVisibility(this.plugin, { state: this.cell.parent!, ref: this.ref });
const isHidden = this.cell.state.isHidden;
for (const r of this.allFilteredEntities) {
this.plugin.state.data.updateCellState(r.transform.ref, { isHidden });
}
this.plugin.build().to(this.ref).update(old => {
old.hidden = isHidden;
}).commit();
};
updateColor = (values: ColorProps) => {
const update = this.plugin.state.data.build();
const { value, type, lightness, alpha } = values;
const entities = this.filteredEntities;
let groupColors: Color[] = [];
if (type === 'generate') {
groupColors = getDistinctGroupColors(entities.length, value, values.variability, values.shift);
}
for (let i = 0; i < entities.length; ++i) {
const c = type === 'generate' ? groupColors[i] : value;
update.to(entities[i]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
} else {
old.coloring.params.color = c;
old.coloring.params.lightness = lightness;
old.alpha = alpha;
old.xrayShaded = alpha < 1 ? true : false;
}
});
}
update.to(this.ref).update(old => {
old.color = values;
});
for (const r of this.roots) {
update.to(r).update(old => {
old.color.type = 'custom';
});
}
update.commit();
};
updateRoot = async (values: PD.Values) => {
await updateColors(this.plugin, values, this.cell.params?.values.tag, this.props.filter);
const update = this.plugin.state.data.build();
for (const r of this.roots) {
if (r !== this.cell) {
update.to(r).update(old => {
old.color.type = 'custom';
});
const others = getAllLeafGroups(this.plugin, r.params?.values.tag);
for (const o of others) {
update.to(o).update(old => {
old.color.type = 'custom';
});
}
}
}
update.to(this.ref).update(old => {
old.color = values;
});
update.commit();
};
updateClip = (values: PD.Values) => {
const update = this.plugin.state.data.build();
const clipObjects = getClipObjects(values as SimpleClipProps, this.plugin.canvas3d!.boundingSphere);
for (const r of this.allFilteredEntities) {
update.to(r).update(old => {
if (old.type) {
old.type.params.clip.objects = clipObjects;
} else {
old.clip.objects = clipObjects;
}
});
}
for (const g of this.allGroups) {
update.to(g).update(old => {
old.clip = values;
});
}
update.commit();
};
updateLod = (values: PD.Values) => {
MesoscaleState.set(this.plugin, { graphics: 'custom' });
(this.plugin.customState as MesoscaleExplorerState).graphicsMode = 'custom';
const update = this.plugin.state.data.build();
for (const r of this.allFilteredEntities) {
update.to(r).update(old => {
if (old.type) {
old.type.params.lodLevels = values.lodLevels;
old.type.params.cellSize = values.cellSize;
old.type.params.batchSize = values.batchSize;
old.type.params.approximate = values.approximate;
}
});
}
for (const g of this.allGroups) {
update.to(g).update(old => {
old.lod = values;
});
}
update.commit();
};
update = (props: MesoscaleGroupProps) => {
this.plugin.state.data.build().to(this.ref).update(props);
};
renderColor() {
const color = this.cell.params?.values.color;
if (this.cell.params?.values.color.type === 'uniform') {
const style = {
backgroundColor: Color.toStyle(color.value),
minWidth: 32,
width: 32,
borderRight: `6px solid ${Color.toStyle(Color.lighten(color.value, color.lightness))}`
};
return <Button style={style} onClick={this.toggleColor} />;
} else if (this.cell.params?.values.color.type === 'generate') {
const style = {
minWidth: 32,
width: 32,
borderRight: `6px solid ${Color.toStyle(Color.lighten(color.value, color.lightness))}`
};
return <IconButton style={style} svg={BrushSvg} toggleState={false} small onClick={this.toggleColor} />;
} else {
return <IconButton svg={BrushSvg} toggleState={false} small onClick={this.toggleColor} />;
}
}
render() {
if (this.allFilteredEntities.length === 0) return;
const state = this.cell.state;
const disabled = false;
const groupLabel = this.cell.obj!.label;
const depth = this.props.depth;
const colorValue = this.cell.params?.values.color;
const rootValue = this.cell.params?.values.color;
const clipValue = this.cell.params?.values.clip;
const lodValue = this.cell.params?.values.lod;
const isRoot = this.cell.params?.values.root;
const groups = this.groups;
const entities = this.entities;
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={disabled}
onMouseEnter={this.highlight}
onMouseLeave={this.clearHighlight}
>
<span title={groupLabel}>{groupLabel}</span>
</Button>;
const expand = <IconButton svg={state.isCollapsed ? ArrowRightSvg : ArrowDropDownSvg} flex='20px' disabled={disabled} onClick={this.toggleExpanded} transparent className='msp-no-hover-outline' style={{ visibility: groups.length > 0 || entities.length > 0 ? 'visible' : 'hidden' }} />;
const color = (entities.length > 0 && !isRoot) && this.renderColor();
const root = (isRoot && this.allGroups.length > 1) && <IconButton svg={BrushSvg} toggleState={false} disabled={disabled} small onClick={this.toggleRoot} />;
const clip = <IconButton svg={ContentCutSvg} toggleState={false} disabled={disabled} small onClick={this.toggleClip} />;
const visibility = <IconButton svg={state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} disabled={disabled} small onClick={this.toggleVisible} />;
return <>
<div className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${depth * 10 + 5}px` }}>
{expand}
{label}
{root || color}
{clip}
{visibility}
</div>
{this.state.action === 'color' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
<ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor}
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<ParameterControls params={ColorParams} values={colorValue} onChangeValues={this.updateColor} />
</ControlGroup>
</div>}
{this.state.action === 'clip' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
<ControlGroup header='Clip' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleClip}
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} />
</ControlGroup>
</div>}
{this.state.action === 'root' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
<ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleRoot}
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<ParameterControls params={RootParams} values={rootValue} onChangeValues={this.updateRoot} />
</ControlGroup>
</div>}
{(!state.isCollapsed) && <>
{groups.map(c => {
return <GroupNode filter={this.props.filter} cell={c} depth={depth + 1} key={c.transform.ref} />;
})}
{this.filteredEntities.map(c => {
return <EntityNode cell={c} depth={depth + 1} key={c.transform.ref} />;
})}
</>}
</>;
}
}
export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled: boolean }> {
state = {
action: undefined,
isDisabled: false,
};
clipMapping = createClipMapping(this);
get groups() {
return this.plugin.state.data.select(StateSelection.Generators.ofTransformer(MesoscaleGroup)
.filter(c => !!this.cell.transform.tags?.includes(c.params?.values.tag)));
}
toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
e.currentTarget.blur();
};
toggleColor = (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (e?.ctrlKey) {
this.updateLightness({ lightness: this.lightnessValue?.lightness ? 0 : DimLightness });
e.preventDefault();
} else {
this.setState({ action: this.state.action === 'color' ? undefined : 'color' });
}
};
toggleClip = () => {
this.setState({ action: this.state.action === 'clip' ? undefined : 'clip' });
};
highlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const repr = this.cell?.obj?.data.repr;
if (repr) {
this.plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
e.currentTarget.blur();
};
clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
e.currentTarget.blur();
};
toggleSelect = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
const cell = this.cell as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
if (!(cell?.obj?.data.sourceData instanceof Structure)) return;
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
this.plugin.managers.interactivity.lociSelects.toggle({ loci }, false);
};
center = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
const cell = this.cell as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
if (!(cell?.obj?.data.sourceData instanceof Structure)) return;
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
centerLoci(this.plugin, loci);
};
handleClick = (e: React.MouseEvent<HTMLElement>) => {
if (e.ctrlKey) {
this.toggleSelect(e);
} else {
this.center(e);
}
};
get colorValue(): Color | undefined {
return this.cell.transform.params?.colorTheme?.params.value ?? this.cell.transform.params?.coloring?.params.color;
}
get lightnessValue(): { lightness: number } | undefined {
return {
lightness: this.cell.transform.params?.colorTheme?.params.lightness ?? this.cell.transform.params?.coloring?.params.lightness ?? 0
};
}
get opacityValue(): { alpha: number } | undefined {
return {
alpha: this.cell.transform.params?.type?.params.alpha ?? this.cell.transform.params?.alpha ?? 1
};
}
get clipValue(): Clip.Props | undefined {
return this.cell.transform.params.type?.params.clip ?? this.cell.transform.params.clip;
}
get lodValue(): PD.Values<typeof LodParams> | undefined {
const p = this.cell.transform.params?.type?.params;
if (!p) return;
return {
lodLevels: p.lodLevels,
cellSize: p.cellSize,
batchSize: p.batchSize,
approximate: p.approximate,
};
}
get patternValue(): { amplitude: number, frequency: number } | undefined {
const p = this.cell.transform.params;
if (p.type) return;
return {
amplitude: p.bumpAmplitude,
frequency: p.bumpFrequency * 10,
};
}
updateColor: ParamOnChange = ({ value }) => {
const update = this.plugin.state.data.build();
for (const g of this.groups) {
update.to(g.transform.ref).update(old => {
old.color.type = 'custom';
});
}
for (const r of this.roots) {
update.to(r).update(old => {
old.color.type = 'custom';
});
}
update.to(this.ref).update(old => {
if (old.colorTheme) {
old.colorTheme.params.value = value;
} else if (old.coloring) {
old.coloring.params.color = value;
}
});
update.commit();
};
updateLightness = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (old.colorTheme) {
old.colorTheme.params.lightness = values.lightness;
} else if (old.coloring) {
old.coloring.params.lightness = values.lightness;
}
}).commit();
};
updateOpacity = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (old.type) {
old.type.params.alpha = values.alpha;
old.type.params.xrayShaded = values.alpha < 1 ? 'inverted' : false;
} else {
old.alpha = values.alpha;
old.xrayShaded = values.alpha < 1 ? true : false;
}
}).commit();
};
updateClip = (props: Clip.Props) => {
const params = this.cell.transform.params;
const clip = params.type ? params.type.params.clip : params.clip;
if (!PD.areEqual(Clip.Params, clip, props)) {
this.plugin.build().to(this.ref).update(old => {
if (old.type) {
old.type.params.clip = props;
} else {
old.clip = props;
}
}).commit();
}
};
updateLod = (values: PD.Values) => {
const params = this.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
if (!params.type) return;
MesoscaleState.set(this.plugin, { graphics: 'custom' });
(this.plugin.customState as MesoscaleExplorerState).graphicsMode = 'custom';
if (!deepEqual(params.type.params.lodLevels, values.lodLevels) || params.type.params.cellSize !== values.cellSize || params.type.params.batchSize !== values.batchSize || params.type.params.approximate !== values.approximate) {
this.plugin.build().to(this.ref).update(old => {
old.type.params.lodLevels = values.lodLevels;
old.type.params.cellSize = values.cellSize;
old.type.params.batchSize = values.batchSize;
old.type.params.approximate = values.approximate;
}).commit();
}
};
updatePattern = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (!old.type) {
old.bumpAmplitude = values.amplitude;
old.bumpFrequency = values.frequency / 10;
}
}).commit();
};
render() {
const cellState = this.cell.state;
const disabled = this.cell.status !== 'error' && this.cell.status !== 'ok';
const depth = this.props.depth;
const colorValue = this.colorValue;
const lightnessValue = this.lightnessValue;
const opacityValue = this.opacityValue;
const lodValue = this.lodValue;
const patternValue = this.patternValue;
const l = getEntityLabel(this.plugin, this.cell);
const label = <Button className={`msp-btn-tree-label msp-type-class-${this.cell.obj!.type.typeClass}`} noOverflow disabled={disabled}
onClick={this.handleClick}
onMouseEnter={this.highlight}
onMouseLeave={this.clearHighlight}
>
<span title={l}>{l}</span>
</Button>;
const color = colorValue !== undefined && <Button style={{ backgroundColor: Color.toStyle(colorValue), minWidth: 32, width: 32, borderRight: `6px solid ${Color.toStyle(Color.lighten(colorValue, lightnessValue?.lightness || 0))}` }} onClick={this.toggleColor} />;
const clip = <IconButton svg={ContentCutSvg} toggleState={false} disabled={disabled} small onClick={this.toggleClip} />;
const visibility = <IconButton svg={cellState.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} disabled={disabled} small onClick={this.toggleVisible} />;
return <>
<div className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${depth * 10 + 5}px` }}>
{label}
{color}
{clip}
{visibility}
</div>
{this.state.action === 'color' && colorValue !== void 0 && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
<ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor}
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<CombinedColorControl param={ColorValueParam} value={colorValue ?? Color(0xFFFFFF)} onChange={this.updateColor} name='color' hideNameRow />
<ParameterControls params={LightnessParams} values={lightnessValue} onChangeValues={this.updateLightness} />
<ParameterControls params={OpacityParams} values={opacityValue} onChangeValues={this.updateOpacity} />
{patternValue && <ParameterControls params={PatternParams} values={patternValue} onChangeValues={this.updatePattern} />}
</ControlGroup>
</div>}
{this.state.action === 'clip' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
<ControlGroup header='Clip' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleClip}
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<ParameterMappingControl mapping={this.clipMapping} />
{lodValue && <ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />}
</ControlGroup>
</div>}
</>;
}
}

View File

@@ -1,98 +0,0 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Mp4EncoderUI } from '../../../extensions/mp4-export/ui';
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { SectionHeader } from '../../../mol-plugin-ui/controls/common';
import { MesoscaleExplorerState } from '../app';
import { MesoscaleState } from '../data/state';
import { EntityControls, ModelInfo, SelectionInfo } from './entities';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls } from './states';
const Spacer = () => <div style={{ height: '2em' }} />;
export class LeftPanel extends PluginUIComponent {
render() {
const customState = this.plugin.customState as MesoscaleExplorerState;
return <div className='msp-scrollable-container'>
<SectionHeader title='Database' />
<DatabaseControls />
<Spacer />
<SectionHeader title='Open' />
<LoaderControls />
<Spacer />
{customState.examples?.length && <>
<SectionHeader title='Example' />
<ExampleControls />
<Spacer />
</>}
<SectionHeader title='Session' />
<SessionControls />
<Spacer />
<SectionHeader title='Snapshots' />
<SnapshotControls />
<Spacer />
<Mp4EncoderUI />
</div>;
}
}
export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
state = {
isDisabled: false,
};
get hasModelInfo() {
return (
MesoscaleState.has(this.plugin) &&
!!(MesoscaleState.get(this.plugin).description ||
MesoscaleState.get(this.plugin).link)
);
}
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
});
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
if (!this.state.isDisabled && MesoscaleState.has(this.plugin) && MesoscaleState.ref(this.plugin) === e.ref) {
this.forceUpdate();
}
});
this.subscribe(this.plugin.managers.structure.selection.events.changed, e => {
if (!this.state.isDisabled) {
this.forceUpdate();
}
});
}
render() {
return <div className='msp-scrollable-container'>
{this.hasModelInfo && <>
<SectionHeader title='Model' />
<ModelInfo />
<Spacer />
</>}
<>
<SectionHeader title='Selection' />
<SelectionInfo />
<Spacer />
</>
<SectionHeader title='Entities' />
<EntityControls />
</div>;
}
}

View File

@@ -1,363 +0,0 @@
/**
* Copyright (c) 2022-2023 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 { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { Button, ExpandGroup } from '../../../mol-plugin-ui/controls/common';
import { GetAppSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
import { PluginCommands } from '../../../mol-plugin/commands';
import { PluginContext } from '../../../mol-plugin/context';
import { StateAction, StateObjectRef, StateTransform } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { Color } from '../../../mol-util/color/color';
import { getFileNameInfo } from '../../../mol-util/file-info';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { ExampleEntry, MesoscaleExplorerState } from '../app';
import { createCellpackHierarchy } from '../data/cellpack/preset';
import { createGenericHierarchy } from '../data/generic/preset';
import { createMmcifHierarchy } from '../data/mmcif/preset';
import { createPetworldHierarchy } from '../data/petworld/preset';
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps } from '../data/state';
function adjustPluginProps(ctx: PluginContext) {
ctx.managers.interactivity.setProps({ granularity: 'chain' });
ctx.canvas3d?.setProps({
multiSample: { mode: 'off' },
cameraClipping: { far: false, minNear: 50 },
sceneRadiusFactor: 2,
renderer: {
colorMarker: true,
highlightColor: Color(0xffffff),
highlightStrength: 0,
selectColor: Color(0xffffff),
selectStrength: 0,
dimColor: Color(0xffffff),
dimStrength: 1,
markerPriority: 2,
interiorColorFlag: false,
interiorDarkening: 0.15,
exposure: 1.1,
xrayEdgeFalloff: 3,
},
marking: {
enabled: true,
highlightEdgeColor: Color(0x999999),
selectEdgeColor: Color(0xffff00),
highlightEdgeStrength: 1,
selectEdgeStrength: 1,
ghostEdgeStrength: 1,
innerEdgeFactor: 2.5,
edgeScale: 2,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: {
name: 'on',
params: {
levels: [
{ radius: 2, bias: 1.0 },
{ radius: 5, bias: 1.0 },
{ radius: 8, bias: 1.0 },
{ radius: 11, bias: 1.0 },
],
nearThreshold: 10,
farThreshold: 1500,
}
},
radius: 5,
bias: 1,
blurKernelSize: 11,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {
name: 'on',
params: {
bias: 0.6,
maxDistance: 80,
steps: 3,
tolerance: 1.0,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.15,
color: Color(0x000000),
includeTransparent: false,
}
}
}
});
const { graphics } = MesoscaleState.get(ctx);
setGraphicsCanvas3DProps(ctx, graphics);
}
async function createHierarchy(ctx: PluginContext, ref: string) {
const parsed = await MmcifProvider.parse(ctx, ref);
const tr = StateObjectRef.resolveAndCheck(ctx.state.data, parsed.trajectory)?.obj?.data;
if (!tr) throw new Error('no trajectory');
if (!MmcifFormat.is(tr.representative.sourceData)) {
throw new Error('not mmcif');
}
const { frame, db } = tr.representative.sourceData.data;
let hasCellpackAssemblyMethodDetails = false;
const { method_details } = db.pdbx_struct_assembly;
for (let i = 0, il = method_details.rowCount; i < il; ++i) {
if (method_details.value(i).toUpperCase() === 'CELLPACK') {
hasCellpackAssemblyMethodDetails = true;
break;
}
}
if (frame.categories.pdbx_model) {
await createPetworldHierarchy(ctx, parsed.trajectory);
} else if (
frame.header.toUpperCase().includes('CELLPACK') ||
hasCellpackAssemblyMethodDetails
) {
await createCellpackHierarchy(ctx, parsed.trajectory);
} else {
await createMmcifHierarchy(ctx, parsed.trajectory);
}
}
async function reset(ctx: PluginContext) {
const customState = ctx.customState as MesoscaleExplorerState;
delete customState.stateRef;
customState.stateCache = {};
ctx.managers.asset.clear();
await PluginCommands.State.Snapshots.Clear(ctx);
await PluginCommands.State.RemoveObject(ctx, { state: ctx.state.data, ref: StateTransform.RootRef });
await MesoscaleState.init(ctx);
adjustPluginProps(ctx);
}
export async function loadExampleEntry(ctx: PluginContext, entry: ExampleEntry) {
const { url, type } = entry;
await loadUrl(ctx, url, type);
MesoscaleState.set(ctx, {
description: entry.description || entry.label,
link: entry.link,
});
}
export async function loadUrl(ctx: PluginContext, url: string, type: 'molx' | 'molj' | 'cif' | 'bcif') {
if (type === 'molx' || type === 'molj') {
await PluginCommands.State.Snapshots.OpenUrl(ctx, { url, type });
} else {
await reset(ctx);
const isBinary = type === 'bcif';
const data = await ctx.builders.data.download({ url, isBinary });
await createHierarchy(ctx, data.ref);
}
}
export async function loadPdb(ctx: PluginContext, id: string) {
await reset(ctx);
const url = `https://models.rcsb.org/${id.toUpperCase()}.bcif`;
const data = await ctx.builders.data.download({ url, isBinary: true });
await createHierarchy(ctx, data.ref);
}
export async function loadPdbDev(ctx: PluginContext, id: string) {
await reset(ctx);
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
const url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
const data = await ctx.builders.data.download({ url, isBinary: true });
await createHierarchy(ctx, data.ref);
}
//
export const LoadDatabase = StateAction.build({
display: { name: 'Database', description: 'Load from Database' },
params: (a, ctx: PluginContext) => {
return {
source: PD.Select('pdb', PD.objectToOptions({ pdb: 'PDB', pdbDev: 'PDB-Dev' })),
entry: PD.Text(''),
};
},
from: PluginStateObject.Root
})(({ params }, ctx: PluginContext) => Task.create('Loading from database...', async taskCtx => {
if (params.source === 'pdb') {
await loadPdb(ctx, params.entry);
} else if (params.source === 'pdbDev') {
await loadPdbDev(ctx, params.entry);
}
}));
export const LoadExample = StateAction.build({
display: { name: 'Load', description: 'Load an example' },
params: (a, ctx: PluginContext) => {
const entries = (ctx.customState as MesoscaleExplorerState).examples || [];
return {
entry: PD.Select(0, entries.map((s, i) => [i, s.label])),
};
},
from: PluginStateObject.Root
})(({ params }, ctx: PluginContext) => Task.create('Loading example...', async taskCtx => {
const entries = (ctx.customState as MesoscaleExplorerState).examples || [];
await loadExampleEntry(ctx, entries[params.entry]);
}));
export const LoadModel = StateAction.build({
display: { name: 'Load', description: 'Load a model' },
params: {
files: PD.FileList({ accept: '.cif,.bcif,.cif.gz,.bcif.gz,.zip', multiple: true, description: 'mmCIF or Cellpack- or Petworld-style cif file.', label: 'File(s)' }),
},
from: PluginStateObject.Root
})(({ params }, ctx: PluginContext) => Task.create('Loading model...', async taskCtx => {
if (params.files === null || params.files.length === 0) {
ctx.log.error('No file(s) selected');
return;
}
await reset(ctx);
const firstFile = params.files[0];
const firstInfo = getFileNameInfo(firstFile.file!.name);
if (firstInfo.name.endsWith('zip')) {
try {
await createGenericHierarchy(ctx, firstFile);
} catch (e) {
console.error(e);
ctx.log.error(`Error opening file '${firstFile.name}'`);
}
} else {
for (const file of params.files) {
try {
const info = getFileNameInfo(file.file!.name);
if (!['cif', 'bcif'].includes(info.ext)) continue;
const isBinary = ctx.dataFormats.binaryExtensions.has(info.ext);
const { data } = await ctx.builders.data.readFile({ file, isBinary });
await createHierarchy(ctx, data.ref);
} catch (e) {
console.error(e);
ctx.log.error(`Error opening file '${file.name}'`);
}
}
}
}));
//
export class DatabaseControls extends PluginUIComponent {
componentDidMount() {
}
render() {
return <div style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadDatabase} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
}
export class LoaderControls extends PluginUIComponent {
componentDidMount() {
}
render() {
return <div style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadModel} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
}
export class ExampleControls extends PluginUIComponent {
componentDidMount() {
}
render() {
return <div style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadExample} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
}
export async function openState(ctx: PluginContext, file: File) {
const customState = ctx.customState as MesoscaleExplorerState;
delete customState.stateRef;
customState.stateCache = {};
ctx.managers.asset.clear();
await PluginCommands.State.Snapshots.Clear(ctx);
await PluginCommands.State.Snapshots.OpenFile(ctx, { file });
const cell = ctx.state.data.selectQ(q => q.ofType(MesoscaleStateObject))[0];
if (!cell) throw new Error('Missing MesoscaleState');
customState.stateRef = cell.transform.ref;
customState.graphicsMode = cell.obj?.data.graphics || customState.graphicsMode;
}
export class SessionControls extends PluginUIComponent {
downloadToFileZip = () => {
PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'zip' });
};
open = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files[0]) {
this.plugin.log.error('No state file selected');
return;
}
openState(this.plugin, e.target.files[0]);
};
render() {
return <div style={{ margin: '5px' }}>
<div className='msp-flex-row'>
<Button icon={GetAppSvg} onClick={this.downloadToFileZip} title='Download the state.'>
Download
</Button>
<div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
<Icon svg={OpenInBrowserSvg} inline /> Open <input onChange={this.open} type='file' multiple={false} accept='.molx,.molj' />
</div>
</div>
</div>;
}
}
export class SnapshotControls extends PluginUIComponent<{}> {
render() {
return <div style={{ margin: '5px' }}>
<div style={{ marginBottom: '10px' }}>
<LocalStateSnapshotList />
</div>
<div style={{ marginBottom: '10px' }}>
<LocalStateSnapshots />
</div>
<div style={{ marginBottom: '10px' }}>
<ExpandGroup header='Snapshot Options' initiallyExpanded={false}>
<LocalStateSnapshotParams />
</ExpandGroup>
</div>
</div>;
}
}

View File

@@ -1,49 +1,39 @@
/**
* Copyright (c) 2018-2024 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 { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { Backgrounds } from '../../extensions/backgrounds';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { CellPack } from '../../extensions/cellpack';
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
import { GeometryExport } from '../../extensions/geo-export';
import { MAQualityAssessment, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior';
import { 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 { loadMVSX } from '../../extensions/mvs/components/formats';
import { loadMVS } from '../../extensions/mvs/load';
import { MVSData } from '../../extensions/mvs/mvs-data';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBValidationReport } from '../../extensions/rcsb';
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
import { ZenodoImport } from '../../extensions/zenodo';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { Volume } from '../../mol-model/volume';
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
import { BuiltInTopologyFormat } from '../../mol-plugin-state/formats/topology';
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
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/react18';
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 { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
@@ -51,25 +41,26 @@ import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { PluginSpec } from '../../mol-plugin/spec';
import { PluginState } from '../../mol-plugin/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 '../../mol-util/polyfill';
import { ObjectKeys } from '../../mol-util/type-helpers';
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
import { Backgrounds } from '../../extensions/backgrounds';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { consoleStats, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
const CustomFormats = [
['g3d', G3dProvider] as const
];
export const ExtensionMap = {
'volseg': PluginSpec.Behavior(Volseg),
const Extensions = {
'backgrounds': PluginSpec.Behavior(Backgrounds),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'cellpack': PluginSpec.Behavior(CellPack),
'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
'assembly-symmetry': PluginSpec.Behavior(AssemblySymmetry),
'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
'g3d': PluginSpec.Behavior(G3DFormat),
@@ -78,15 +69,11 @@ export const ExtensionMap = {
'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),
};
const DefaultViewerOptions = {
customFormats: CustomFormats as [string, DataFormatProvider][],
extensions: ObjectKeys(ExtensionMap),
disabledExtensions: [] as string[],
extensions: ObjectKeys(Extensions),
layoutIsExpanded: true,
layoutShowControls: true,
layoutShowRemoteState: true,
@@ -99,10 +86,11 @@ const DefaultViewerOptions = {
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
pixelScale: PluginConfig.General.PixelScale.defaultValue,
pickScale: PluginConfig.General.PickScale.defaultValue,
transparency: PluginConfig.General.Transparency.defaultValue,
pickPadding: PluginConfig.General.PickPadding.defaultValue,
enableWboit: PluginConfig.General.EnableWboit.defaultValue,
enableDpoit: PluginConfig.General.EnableDpoit.defaultValue,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -116,10 +104,6 @@ const DefaultViewerOptions = {
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
};
type ViewerOptions = typeof DefaultViewerOptions;
@@ -138,13 +122,11 @@ export class Viewer {
const o: ViewerOptions = { ...DefaultViewerOptions, ...definedOptions };
const defaultSpec = DefaultPluginUISpec();
const disabledExtension = new Set(o.disabledExtensions ?? []);
const spec: PluginUISpec = {
actions: defaultSpec.actions,
behaviors: [
...defaultSpec.behaviors,
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
...o.extensions.map(e => Extensions[e]),
],
animations: [...defaultSpec.animations || []],
customParamEditors: defaultSpec.customParamEditors,
@@ -176,10 +158,11 @@ export class Viewer {
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
[PluginConfig.General.PixelScale, o.pixelScale],
[PluginConfig.General.PickScale, o.pickScale],
[PluginConfig.General.Transparency, o.transparency],
[PluginConfig.General.PickPadding, o.pickPadding],
[PluginConfig.General.EnableWboit, o.enableWboit],
[PluginConfig.General.EnableDpoit, o.enableDpoit],
[PluginConfig.General.PreferWebGl1, o.preferWebgl1],
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
[PluginConfig.General.PowerPreference, o.powerPreference],
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
@@ -194,10 +177,6 @@ export class Viewer {
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
]
};
@@ -205,10 +184,7 @@ export class Viewer {
? document.getElementById(elementOrId)
: elementOrId;
if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
const plugin = await createPluginUI({
target: element,
spec,
render: renderReact18,
const plugin = await createPluginUI(element, spec, {
onBeforeUIRender: plugin => {
// the preset needs to be added before the UI renders otherwise
// "Download Structure" wont be able to pick it up
@@ -470,55 +446,9 @@ export class Viewer {
return { model, coords, preset };
}
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx') {
if (format === 'mvsj') {
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
const mvsData = MVSData.fromMVSJ(data);
await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: url });
} 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);
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl });
}));
} else {
throw new Error(`Unknown MolViewSpec format: ${format}`);
}
}
/** 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,'. */
async loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx') {
if (typeof data === 'string' && data.startsWith('base64')) {
data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array
}
if (format === 'mvsj') {
if (typeof data !== 'string') {
data = new TextDecoder().decode(data); // Decode Uint8Array to string using UTF8
}
const mvsData = MVSData.fromMVSJ(data);
await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: undefined });
} else if (format === 'mvsx') {
if (typeof data === 'string') {
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
}
await this.plugin.runTask(Task.create('Load MVSX file', async ctx => {
const parsed = await loadMVSX(this.plugin, ctx, data as Uint8Array);
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl });
}));
} else {
throw new Error(`Unknown MolViewSpec format: ${format}`);
}
}
handleResize() {
this.plugin.layout.events.updated.next(void 0);
}
dispose() {
this.plugin.dispose();
}
}
export interface LoadStructureOptions {
@@ -567,15 +497,8 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
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 },
};

View File

@@ -59,15 +59,12 @@
var pixelScale = getParam('pixel-scale', '[^&]+').trim();
var pickScale = getParam('pick-scale', '[^&]+').trim();
var pickPadding = getParam('pick-padding', '[^&]+').trim();
var transparency = getParam('transparency', '[^&]+').trim().toLowerCase();
var disableWboit = getParam('disable-wboit', '[^&]+').trim() === '1';
var enableDpoit = getParam('enable-dpoit', '[^&]+').trim() === '1';
var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
molstar.Viewer.create('app', {
disabledExtensions: [], // anything from Object.keys(molstar.ExtensionMap)
layoutShowControls: !hideControls,
viewportShowExpand: false,
collapseLeftPanel: collapseLeftPanel,
@@ -79,10 +76,10 @@
pixelScale: parseFloat(pixelScale) || 1,
pickScale: parseFloat(pickScale) || 0.25,
pickPadding: isNaN(parseFloat(pickPadding)) ? 1 : parseFloat(pickPadding),
transparency: transparency || undefined,
enableWboit: (disableWboit || enableDpoit) ? false : void 0, // use default value if disable-wboit is not set
enableDpoit: enableDpoit ? true : void 0,
preferWebgl1: preferWebgl1,
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
}).then(viewer => {
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
@@ -96,14 +93,6 @@
var structureUrlIsBinary = getParam('structure-url-is-binary', '[^&]+').trim() === '1';
if (structureUrl) viewer.loadStructureFromUrl(structureUrl, structureUrlFormat, structureUrlIsBinary);
var mvsUrl = getParam('mvs-url', '[^&]+').trim();
var mvsData = getParam('mvs-data', '[^&]+').trim();
var mvsFormat = getParam('mvs-format', '[^&]+').trim() || 'mvsj';
if (mvsUrl && mvsData) console.error('Cannot specify mvs-url and mvs-data URL parameters at the same time. Ignoring both.');
else if (mvsUrl) viewer.loadMvsFromUrl(mvsUrl, mvsFormat);
else if (mvsData) viewer.loadMvsData(mvsData, mvsFormat);
var pdb = getParam('pdb', '[^&]+').trim();
if (pdb) viewer.loadPdb(pdb);
@@ -118,11 +107,6 @@
var modelArchive = getParam('model-archive', '[^&]+').trim();
if (modelArchive) viewer.loadModelArchive(modelArchive);
window.addEventListener('unload', () => {
// to aid GC
viewer.dispose();
});
});
</script>
<!-- __MOLSTAR_ANALYTICS__ -->

View File

@@ -81,5 +81,5 @@ export const DefaultDataOptions: DataOptions = {
const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
const CCD_PATH = path.join(DATA_DIR, 'components.cif');
const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
const CCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/components.cif';
const PVCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';

View File

@@ -158,8 +158,8 @@ async function ensureDicAvailable(dicPath: string, dicUrl: string) {
const DIC_DIR = path.resolve(__dirname, '../../../../build/dics/');
const MMCIF_DIC_PATH = `${DIC_DIR}/mmcif_pdbx_v50.dic`;
const MMCIF_DIC_URL = 'http://mmcif.wwpdb.org/dictionaries/ascii/mmcif_pdbx_v50.dic';
const IHM_DIC_PATH = `${DIC_DIR}/mmcif_ihm_ext.dic`;
const IHM_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/IHMCIF/master/dist/mmcif_ihm_ext.dic';
const IHM_DIC_PATH = `${DIC_DIR}/ihm-extension.dic`;
const IHM_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/IHM-dictionary/master/ihm-extension.dic';
const MA_DIC_PATH = `${DIC_DIR}/ma-extension.dic`;
const MA_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist/mmcif_ma.dic';

View File

@@ -22,7 +22,6 @@ export function getFieldType(type: string, description: string, values?: string[
case 'uline':
case 'uchar3':
case 'uchar1':
case 'uchar5':
// only force lower-case for enums
return values && values.length ? EnumCol(values.map(x => x.toLowerCase()), 'lstr', description) : StrCol(description);
case 'aliasname':
@@ -62,7 +61,6 @@ export function getFieldType(type: string, description: string, values?: string[
case 'symop':
case 'exp_data_doi':
case 'asym_id':
case 'uniprot_ptm_id':
return StrCol(description);
case 'int':
case 'non_negative_int':
@@ -91,7 +89,6 @@ export function getFieldType(type: string, description: string, values?: string[
case 'Tag':
case 'Implied':
case 'Word':
case 'Uri':
return wrapContainer('str', ',', description, container);
case 'Real':
return wrapContainer('float', ',', description, container);
@@ -155,7 +152,7 @@ function getImportFrames(d: Data.CifFrame, imports: Imports) {
}
/** get field from given or linked category */
function getField(category: string, field: string, d: Data.CifFrame, imports: Imports, ctx: FrameData): Data.CifField | undefined {
function getField(category: string, field: string, d: Data.CifFrame, imports: Imports, ctx: FrameData): Data.CifField|undefined {
const { categories, links } = ctx;
const cat = d.categories[category];
if (cat) {

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env node
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*
* Command-line application for printing MolViewSpec tree schema
* Build: npm run build
* Run: node lib/commonjs/cli/mvs/mvs-print-schema
* node lib/commonjs/cli/mvs/mvs-print-schema --markdown
*/
import { ArgumentParser } from 'argparse';
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
import { MVSDefaults } from '../../extensions/mvs/tree/mvs/mvs-defaults';
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';
/** Command line argument values for `main` */
interface Args {
markdown: boolean,
}
/** Return parsed command line arguments for `main` */
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Command-line application for printing MolViewSpec tree schema.' });
parser.add_argument('-m', '--markdown', { action: 'store_true', help: 'Print the schema as markdown instead of plain text.' });
const args = parser.parse_args();
return { ...args };
}
/** Main workflow for printing MolViewSpec tree schema. */
function main(args: Args) {
if (args.markdown) {
console.log(treeSchemaToMarkdown(MVSTreeSchema, MVSDefaults));
} else {
console.log(treeSchemaToString(MVSTreeSchema, MVSDefaults));
}
}
main(parseArguments());

View File

@@ -1,158 +0,0 @@
#!/usr/bin/env node
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*
* Command-line application for rendering images from MolViewSpec files
* Build: npm install --no-save canvas gl jpeg-js pngjs // these packages are not listed in Mol* dependencies for performance reasons
* npm run build
* Run: node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
*/
import { ArgumentParser } from 'argparse';
import fs from 'fs';
import gl from 'gl';
import jpegjs from 'jpeg-js';
import path from 'path';
import pngjs from 'pngjs';
import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
import { setCanvasModule } from '../../mol-geo/geometry/text/font-atlas';
import { PluginContext } from '../../mol-plugin/context';
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
import { DefaultPluginSpec, PluginSpec } from '../../mol-plugin/spec';
import { ExternalModules, defaultCanvas3DParams } from '../../mol-plugin/util/headless-screenshot';
import { Task } from '../../mol-task';
import { setFSModule } from '../../mol-util/data-source';
import { onelinerJsonString } from '../../mol-util/json';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
// MolViewSpec must be imported after HeadlessPluginContext
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { loadMVSX } from '../../extensions/mvs/components/formats';
import { loadMVS } from '../../extensions/mvs/load';
import { MVSData } from '../../extensions/mvs/mvs-data';
setFSModule(fs);
setCanvasModule(require('canvas'));
const DEFAULT_SIZE = '800x800';
/** Command line argument values for `main` */
interface Args {
input: string[],
output: string[],
size: { width: number, height: number },
molj: boolean,
}
/** Return parsed command line arguments for `main` */
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Command-line application for rendering images from MolViewSpec files' });
parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj or .mvsx format. File format is inferred from the file extension.' });
parser.add_argument('-o', '--output', { required: true, nargs: '+', help: 'File path(s) for output files (one output path for each input file). Output format is inferred from the file extension (.png or .jpg)' });
parser.add_argument('-s', '--size', { help: `Output image resolution, {width}x{height}. Default: ${DEFAULT_SIZE}.`, default: DEFAULT_SIZE });
parser.add_argument('-m', '--molj', { action: 'store_true', help: `Save Mol* state (.molj) in addition to rendered images (use the same output file paths but with .molj extension)` });
const args = parser.parse_args();
try {
const parts = args.size.split('x');
if (parts.length !== 2) throw new Error('Must contain two x-separated parts');
args.size = { width: parseIntStrict(parts[0]), height: parseIntStrict(parts[1]) };
} catch {
parser.error(`argument: --size: invalid image size string: '${args.size}' (must be two x-separated integers (width and height), e.g. '400x300')`);
}
if (args.input.length !== args.output.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 rendering images from MolViewSpec files */
async function main(args: Args): Promise<void> {
const plugin = await createHeadlessPlugin(args);
for (let i = 0; i < args.input.length; i++) {
const input = args.input[i];
const output = args.output[i];
console.log(`Processing ${input} -> ${output}`);
let mvsData: MVSData;
let sourceUrl: string | undefined;
if (input.toLowerCase().endsWith('.mvsj')) {
const data = fs.readFileSync(input, { encoding: 'utf8' });
mvsData = MVSData.fromMVSJ(data);
sourceUrl = `file://${path.resolve(input)}`;
} else if (input.toLowerCase().endsWith('.mvsx')) {
const data = fs.readFileSync(input);
const mvsx = await plugin.runTask(Task.create('Load MVSX', async ctx => loadMVSX(plugin, ctx, data)));
mvsData = mvsx.mvsData;
sourceUrl = mvsx.sourceUrl;
} else {
throw new Error(`Input file name must end with .mvsj or .mvsx: ${input}`);
}
await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: true, sourceUrl: sourceUrl });
fs.mkdirSync(path.dirname(output), { recursive: true });
if (args.molj) {
await plugin.saveStateSnapshot(withExtension(output, '.molj'));
}
await plugin.saveImage(output);
checkState(plugin);
}
await plugin.clear();
plugin.dispose();
}
/** Return a new and initiatized HeadlessPlugin */
async function createHeadlessPlugin(args: Pick<Args, 'size'>): Promise<HeadlessPluginContext> {
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
const spec = DefaultPluginSpec();
spec.behaviors.push(PluginSpec.Behavior(MolViewSpec));
const headlessCanvasOptions = defaultCanvas3DParams();
const canvasOptions = {
...PD.getDefaultValues(Canvas3DParams),
cameraResetDurationMs: headlessCanvasOptions.cameraResetDurationMs,
postprocessing: headlessCanvasOptions.postprocessing,
};
const plugin = new HeadlessPluginContext(externalModules, spec, args.size, { canvas: canvasOptions });
try {
await plugin.init();
} catch (error) {
plugin.dispose();
throw error;
}
return plugin;
}
/** Parse integer, fail early. */
function parseIntStrict(str: string): number {
if (str === '') throw new Error('Is empty string');
const result = Number(str);
if (isNaN(result)) throw new Error('Is NaN');
if (Math.floor(result) !== result) throw new Error('Is not integer');
return result;
}
/** Replace the file extension in `filename` by `extension`. If `filename` has no extension, add it. */
function withExtension(filename: string, extension: string): string {
const oldExtension = path.extname(filename);
return filename.slice(0, -oldExtension.length) + extension;
}
/** Check Mol* state, print and throw error if any cell is not OK. */
function checkState(plugin: PluginContext): void {
const cells = Array.from(plugin.state.data.cells.values());
const badCell = cells.find(cell => cell.status !== 'ok');
if (badCell) {
console.error(`Building Mol* state failed`);
console.error(` Transformer: ${badCell.transform.transformer.id}`);
console.error(` Params: ${onelinerJsonString(badCell.transform.params)}`);
console.error(` Error: ${badCell.errorText}`);
console.error(``);
throw new Error(`Building Mol* state failed: ${badCell.errorText}`);
}
}
main(parseArguments());

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env node
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*
* Command-line application for validating MolViewSpec files
* Build: npm run build
* Run: node lib/commonjs/cli/mvs/mvs-validate examples/mvs/1cbs.mvsj
*/
import { ArgumentParser } from 'argparse';
import fs from 'fs';
import { setFSModule } from '../../mol-util/data-source';
import { MVSData } from '../../extensions/mvs/mvs-data';
setFSModule(fs);
/** Command line argument values for `main` */
interface Args {
input: string[],
no_extra: boolean,
}
/** Return parsed command line arguments for `main` */
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Command-line application for validating MolViewSpec files. Prints validation status (OK/FAILED) to stdout, detailed validation issues to stderr. Exits with a zero exit code if all input files are OK.' });
parser.add_argument('input', { nargs: '+', help: 'Input file(s) in .mvsj format' });
parser.add_argument('--no-extra', { action: 'store_true', help: 'Treat presence of extra node params as an issue.' });
const args = parser.parse_args();
return { ...args };
}
/** Main workflow for validating MolViewSpec files. Returns the number of failed input files. */
function main(args: Args): number {
let nFailed = 0;
for (const input of args.input) {
const data = fs.readFileSync(input, { encoding: 'utf8' });
const mvsData = MVSData.fromMVSJ(data);
const issues = MVSData.validationIssues(mvsData, { noExtra: args.no_extra });
const status = issues ? 'FAILED' : 'OK';
console.log(`${status.padEnd(6)} ${input}`);
if (issues) {
nFailed++;
for (const issue of issues) {
console.error(issue);
}
}
}
return nFailed;
}
const nFailed = main(parseArguments());
if (nFailed > 0) {
process.exitCode = 1;
}

View File

@@ -25,7 +25,7 @@ async function readFile(path: string) {
}
}
async function parseCif(data: string | Uint8Array) {
async function parseCif(data: string|Uint8Array) {
const comp = CIF.parse(data);
const parsed = await comp.run(p => console.log(Progress.format(p)), 250);
if (parsed.isError) throw parsed;

View File

@@ -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) })).run();
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, 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

@@ -11,8 +11,7 @@ import { SphericalBasisOrder } from '../../extensions/alpha-orbitals/spherical-f
import { BasisAndOrbitals, CreateOrbitalDensityVolume, CreateOrbitalRepresentation3D, CreateOrbitalVolume, StaticBasisAndOrbitals } from '../../extensions/alpha-orbitals/transforms';
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { createPluginUI } from '../../mol-plugin-ui';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { createPluginUI } from '../../mol-plugin-ui/react18';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginCommands } from '../../mol-plugin/commands';
@@ -26,7 +25,7 @@ import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
import { setDebugMode, setTimingMode, consoleStats } from '../../mol-util/debug';
import { setDebugMode, setTimingMode } from '../../mol-util/debug';
interface DemoInput {
moleculeSdf: string,
@@ -57,32 +56,28 @@ export class AlphaOrbitalsExample {
async init(target: string | HTMLElement) {
const defaultSpec = DefaultPluginUISpec();
this.plugin = await createPluginUI({
target: typeof target === 'string' ? document.getElementById(target)! : target,
render: renderReact18,
spec: {
...defaultSpec,
layout: {
initial: {
isExpanded: false,
showControls: false
},
this.plugin = await createPluginUI(typeof target === 'string' ? document.getElementById(target)! : target, {
...defaultSpec,
layout: {
initial: {
isExpanded: false,
showControls: false
},
components: {
controls: { left: 'none', right: 'none', top: 'none', bottom: 'none' },
},
canvas3d: {
camera: {
helper: { axes: { name: 'off', params: {} } }
}
},
config: [
[PluginConfig.Viewport.ShowExpand, false],
[PluginConfig.Viewport.ShowControls, false],
[PluginConfig.Viewport.ShowSelectionMode, false],
[PluginConfig.Viewport.ShowAnimation, false],
]
}
},
components: {
controls: { left: 'none', right: 'none', top: 'none', bottom: 'none' },
},
canvas3d: {
camera: {
helper: { axes: { name: 'off', params: {} } }
}
},
config: [
[PluginConfig.Viewport.ShowExpand, false],
[PluginConfig.Viewport.ShowControls, false],
[PluginConfig.Viewport.ShowSelectionMode, false],
[PluginConfig.Viewport.ShowAnimation, false],
]
});
this.plugin.managers.interactivity.setProps({ granularity: 'element' });
@@ -227,5 +222,4 @@ export class AlphaOrbitalsExample {
(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
(window as any).AlphaOrbitalsExample.setDebugMode = setDebugMode;
(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;
(window as any).AlphaOrbitalsExample.consoleStats = consoleStats;
(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;

View File

@@ -9,9 +9,8 @@ import { EmptyLoci } from '../../mol-model/loci';
import { StructureSelection } from '../../mol-model/structure';
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/model-index';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { createPluginUI } from '../../mol-plugin-ui';
import { createPluginUI } from '../../mol-plugin-ui/react18';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginCommands } from '../../mol-plugin/commands';
import { Script } from '../../mol-script/script';
@@ -30,20 +29,16 @@ class BasicWrapper {
plugin: PluginUIContext;
async init(target: string | HTMLElement) {
this.plugin = await createPluginUI({
target: typeof target === 'string' ? document.getElementById(target)! : target,
render: renderReact18,
spec: {
...DefaultPluginUISpec(),
layout: {
initial: {
isExpanded: false,
showControls: false
}
},
components: {
remoteState: 'none'
this.plugin = await createPluginUI(typeof target === 'string' ? document.getElementById(target)! : target, {
...DefaultPluginUISpec(),
layout: {
initial: {
isExpanded: false,
showControls: false
}
},
components: {
remoteState: 'none'
}
});
@@ -51,14 +46,6 @@ class BasicWrapper {
this.plugin.representation.structure.themes.colorThemeRegistry.add(CustomColorThemeProvider);
this.plugin.managers.lociLabels.addProvider(StripedResidues.labelProvider!);
this.plugin.customModelProperties.register(StripedResidues.propertyProvider, true);
this.plugin.managers.dragAndDrop.addHandler('custom-wrapper', (files) => {
if (files.some(f => f.name.toLowerCase().endsWith('.testext'))) {
console.log('.testext File dropped');
return true;
}
return false;
});
}
async load({ url, format = 'mmcif', isBinary = false, assemblyId = '' }: LoadParams) {

View File

@@ -1,94 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*
* Example command-line application generating images of PDB structures
* Build: npm install --no-save gl jpeg-js pngjs // these packages are not listed in dependencies for performance reasons
* npm run build
* Run: node lib/commonjs/examples/image-renderer 1cbs ../outputs_1cbs/
*/
import { ArgumentParser } from 'argparse';
import fs from 'fs';
import path from 'path';
import gl from 'gl';
import pngjs from 'pngjs';
import jpegjs from 'jpeg-js';
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
import { DefaultPluginSpec } from '../../mol-plugin/spec';
import { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
import { setFSModule } from '../../mol-util/data-source';
setFSModule(fs);
interface Args {
pdbId: string,
outDirectory: string
}
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Example command-line application generating images of PDB structures' });
parser.add_argument('pdbId', { help: 'PDB identifier' });
parser.add_argument('outDirectory', { help: 'Directory for outputs' });
const args = parser.parse_args();
return { ...args };
}
async function main() {
const args = parseArguments();
const url = `https://www.ebi.ac.uk/pdbe/entry-files/download/${args.pdbId}.bcif`;
console.log('PDB ID:', args.pdbId);
console.log('Source URL:', url);
console.log('Outputs:', args.outDirectory);
// Create a headless plugin
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec(), { width: 800, height: 800 });
await plugin.init();
// Download and visualize data in the plugin
const update = plugin.build();
const structure = update.toRoot()
.apply(Download, { url, isBinary: true })
.apply(ParseCif)
.apply(TrajectoryFromMmCif)
.apply(ModelFromTrajectory)
.apply(StructureFromModel);
const polymer = structure.apply(StructureComponent, { type: { name: 'static', params: 'polymer' } });
const ligand = structure.apply(StructureComponent, { type: { name: 'static', params: 'ligand' } });
polymer.apply(StructureRepresentation3D, {
type: { name: 'cartoon', params: { alpha: 1 } },
colorTheme: { name: 'sequence-id', params: {} },
});
ligand.apply(StructureRepresentation3D, {
type: { name: 'ball-and-stick', params: { sizeFactor: 1 } },
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
sizeTheme: { name: 'physical', params: {} },
});
await update.commit();
// Export images
fs.mkdirSync(args.outDirectory, { recursive: true });
await plugin.saveImage(path.join(args.outDirectory, 'basic.png'));
await plugin.saveImage(path.join(args.outDirectory, 'basic.jpg'));
await plugin.saveImage(path.join(args.outDirectory, 'large.png'), { width: 1600, height: 1200 });
await plugin.saveImage(path.join(args.outDirectory, 'large.jpg'), { width: 1600, height: 1200 });
await plugin.saveImage(path.join(args.outDirectory, 'stylized.png'), undefined, STYLIZED_POSTPROCESSING);
await plugin.saveImage(path.join(args.outDirectory, 'stylized.jpg'), undefined, STYLIZED_POSTPROCESSING);
await plugin.saveImage(path.join(args.outDirectory, 'stylized-compressed-jpg.jpg'), undefined, STYLIZED_POSTPROCESSING, undefined, 10);
// Export state loadable in Mol* Viewer
await plugin.saveStateSnapshot(path.join(args.outDirectory, 'molstar-state.molj'));
// Cleanup
await plugin.clear();
plugin.dispose();
}
main();

View File

@@ -1,14 +1,13 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Canvas3DProps } from '../../mol-canvas3d/canvas3d';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { createPluginUI } from '../../mol-plugin-ui';
import { createPluginUI } from '../../mol-plugin-ui/react18';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginCommands } from '../../mol-plugin/commands';
import { Asset } from '../../mol-util/assets';
@@ -25,31 +24,8 @@ const Canvas3DPresets = {
illustrative: {
canvas3d: <Preset>{
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
resolutionScale: 1,
color: Color(0x000000),
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.33,
color: Color(0x000000),
includeTransparent: true,
}
},
shadow: {
name: 'off',
params: {}
},
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000) } }
},
renderer: {
ambientIntensity: 1.0,
@@ -60,25 +36,8 @@ const Canvas3DPresets = {
occlusion: {
canvas3d: <Preset>{
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
blurKernelSize: 15,
resolutionScale: 1,
}
},
outline: {
name: 'off',
params: {}
},
shadow: {
name: 'off',
params: {}
},
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
outline: { name: 'off', params: {} }
},
renderer: {
ambientIntensity: 0.4,
@@ -91,8 +50,7 @@ const Canvas3DPresets = {
canvas3d: <Preset>{
postprocessing: {
occlusion: { name: 'off', params: {} },
outline: { name: 'off', params: {} },
shadow: { name: 'off', params: {} },
outline: { name: 'off', params: {} }
},
renderer: {
ambientIntensity: 0.4,
@@ -114,20 +72,16 @@ class LightingDemo {
private preset: Canvas3DPreset = 'illustrative';
async init(target: string | HTMLElement) {
this.plugin = await createPluginUI({
target: typeof target === 'string' ? document.getElementById(target)! : target,
render: renderReact18,
spec: {
...DefaultPluginUISpec(),
layout: {
initial: {
isExpanded: false,
showControls: false
},
this.plugin = await createPluginUI(typeof target === 'string' ? document.getElementById(target)! : target, {
...DefaultPluginUISpec(),
layout: {
initial: {
isExpanded: false,
showControls: false
},
components: {
controls: { left: 'none', right: 'none', top: 'none', bottom: 'none' }
}
},
components: {
controls: { left: 'none', right: 'none', top: 'none', bottom: 'none' }
}
});

View File

@@ -10,8 +10,7 @@ import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/mod
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
import { PluginStateObject, PluginStateObject as PSO } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { createPluginUI } from '../../mol-plugin-ui';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { createPluginUI } from '../../mol-plugin-ui/react18';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
import { CreateVolumeStreamingInfo, InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
@@ -47,23 +46,19 @@ class MolStarProteopediaWrapper {
async init(target: string | HTMLElement, options?: {
customColorList?: number[]
}) {
this.plugin = await createPluginUI({
target: typeof target === 'string' ? document.getElementById(target)! : target,
render: renderReact18,
spec: {
...DefaultPluginUISpec(),
animations: [
AnimateModelIndex
],
layout: {
initial: {
isExpanded: false,
showControls: false
}
},
components: {
remoteState: 'none'
this.plugin = await createPluginUI(typeof target === 'string' ? document.getElementById(target)! : target, {
...DefaultPluginUISpec(),
animations: [
AnimateModelIndex
],
layout: {
initial: {
isExpanded: false,
showControls: false
}
},
components: {
remoteState: 'none'
}
});

View File

@@ -50,7 +50,7 @@ const _testBasis: Basis = {
0.025886090588624934,
0.019164790004065606,
-0.013539970104105408
],
] as Vec3,
'shells': [
{
'angularMomentum': [0],
@@ -101,7 +101,7 @@ const _testBasis: Basis = {
0.5082729578468134,
1.6880351220025265,
0.4963443067810461
],
] as Vec3,
'shells': [
{
'angularMomentum': [0],
@@ -158,7 +158,7 @@ const _testBasis: Basis = {
1.1367367844436005,
-0.47018519422670163,
-1.356802622574504
],
] as Vec3,
'shells': [
{
'angularMomentum': [0],

View File

@@ -53,7 +53,7 @@ export async function sphericalCollocation(
L,
shell.coefficients[amIndex++],
shell.exponents,
atom.center as unknown as Vec3,
atom.center,
cutoffThreshold,
alpha
);

View File

@@ -22,7 +22,7 @@ export interface SphericalElectronShell {
export interface Basis {
atoms: {
// in Bohr units!
center: [number, number, number];
center: Vec3;
shells: SphericalElectronShell[];
}[];
}
@@ -78,7 +78,7 @@ export function initCubeGrid(params: CubeGridComputationParams): CubeGridInfo {
const count = geometry.length;
const box = Box3D.expand(
Box3D(),
Box3D.fromVec3Array(Box3D(), geometry as unknown as Vec3[]),
Box3D.fromVec3Array(Box3D(), geometry),
Vec3.create(expand, expand, expand)
);
const size = Box3D.size(Vec3(), box);

View File

@@ -1,7 +0,0 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
export { AssemblySymmetry, AssemblySymmetryConfig } from './behavior';

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2024 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>
*/
@@ -28,12 +28,6 @@ export const Backgrounds = PluginBehavior.create<{ }>({
ctor: class extends PluginBehavior.Handler<{ }> {
register(): void {
this.ctx.config.set(PluginConfig.Background.Styles, [
[{
variant: {
name: 'off',
params: {}
}
}, 'Off'],
[{
variant: {
name: 'radialGradient',
@@ -56,7 +50,6 @@ export const Backgrounds = PluginBehavior.create<{ }>({
lightness: 0,
saturation: 0,
opacity: 1,
blur: 0,
coverage: 'viewport',
}
}
@@ -80,7 +73,6 @@ export const Backgrounds = PluginBehavior.create<{ }>({
saturation: 0,
opacity: 1,
blur: 0.3,
rotation: { x: 0, y: 0, z: 0 },
}
}
}, 'Purple Nebula Skybox'],

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ThemeDataContext } from '../../../mol-theme/theme';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Color } from '../../../mol-util/color';
import { getPalette } from '../../../mol-util/color/palette';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
import { StructureElement, Bond, Model } from '../../../mol-model/structure';
import { Location } from '../../../mol-model/location';
import { CellPackInfoProvider } from '../property';
import { distinctColors } from '../../../mol-util/color/distinct';
import { Hcl } from '../../../mol-util/color/spaces/hcl';
const DefaultColor = Color(0xCCCCCC);
const Description = 'Gives every model in a CellPack packing a unique generated color similar to other models in the packing.';
export const CellPackGenerateColorThemeParams = {};
export type CellPackGenerateColorThemeParams = typeof CellPackGenerateColorThemeParams
export function getCellPackGenerateColorThemeParams(ctx: ThemeDataContext) {
return CellPackGenerateColorThemeParams; // TODO return copy
}
export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Values<CellPackGenerateColorThemeParams>): ColorTheme<CellPackGenerateColorThemeParams> {
let color: LocationColor;
let legend: ScaleLegend | TableLegend | undefined;
const info = ctx.structure && CellPackInfoProvider.get(ctx.structure).value;
if (ctx.structure && info) {
const colors = distinctColors(info.packingsCount);
const hcl = Hcl.fromColor(Hcl(), colors[info.packingIndex]);
const hue = [Math.max(0, hcl[0] - 35), Math.min(360, hcl[0] + 35)] as [number, number];
const { models } = ctx.structure.root;
let size = 0;
for (const m of models) size = Math.max(size, Model.TrajectoryInfo.get(m).size);
const palette = getPalette(size, { palette: {
name: 'generate',
params: {
hue, chroma: [30, 80], luminance: [15, 85],
clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
}
} }, { minLabel: 'Min', maxLabel: 'Max' });
legend = palette.legend;
const modelColor = new Map<number, Color>();
for (let i = 0, il = models.length; i < il; ++i) {
const idx = Model.TrajectoryInfo.get(models[i]).index;
modelColor.set(Model.TrajectoryInfo.get(models[i]).index, palette.color(idx));
}
color = (location: Location): Color => {
if (StructureElement.Location.is(location)) {
return modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!;
} else if (Bond.isLocation(location)) {
return modelColor.get(Model.TrajectoryInfo.get(location.aUnit.model).index)!;
}
return DefaultColor;
};
} else {
color = () => DefaultColor;
}
return {
factory: CellPackGenerateColorTheme,
granularity: 'instance',
color,
props,
description: Description,
legend
};
}
export const CellPackGenerateColorThemeProvider: ColorTheme.Provider<CellPackGenerateColorThemeParams, 'cellpack-generate'> = {
name: 'cellpack-generate',
label: 'CellPack Generate',
category: ColorTheme.Category.Chain,
factory: CellPackGenerateColorTheme,
getParams: getCellPackGenerateColorThemeParams,
defaultValues: PD.getDefaultValues(CellPackGenerateColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => {
return (
!!ctx.structure && ctx.structure.elementCount > 0 &&
!!CellPackInfoProvider.get(ctx.structure).value
);
}
};

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ThemeDataContext } from '../../../mol-theme/theme';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Color } from '../../../mol-util/color';
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
import { StructureElement, Model, Bond } from '../../../mol-model/structure';
import { Location } from '../../../mol-model/location';
import { CellPackInfoProvider } from '../property';
const DefaultColor = Color(0xCCCCCC);
const Description = 'Gives every model in a CellPack the color provied in the packing data.';
export const CellPackProvidedColorThemeParams = {};
export type CellPackProvidedColorThemeParams = typeof CellPackProvidedColorThemeParams
export function getCellPackProvidedColorThemeParams(ctx: ThemeDataContext) {
return CellPackProvidedColorThemeParams; // TODO return copy
}
export function CellPackProvidedColorTheme(ctx: ThemeDataContext, props: PD.Values<CellPackProvidedColorThemeParams>): ColorTheme<CellPackProvidedColorThemeParams> {
let color: LocationColor;
let legend: ScaleLegend | TableLegend | undefined;
const info = ctx.structure && CellPackInfoProvider.get(ctx.structure).value;
if (ctx.structure && info?.colors) {
const { models } = ctx.structure.root;
const modelColor = new Map<number, Color>();
for (let i = 0, il = models.length; i < il; ++i) {
const idx = Model.TrajectoryInfo.get(models[i]).index;
modelColor.set(Model.TrajectoryInfo.get(models[i]).index, info.colors[idx]);
}
color = (location: Location): Color => {
if (StructureElement.Location.is(location)) {
return modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!;
} else if (Bond.isLocation(location)) {
return modelColor.get(Model.TrajectoryInfo.get(location.aUnit.model).index)!;
}
return DefaultColor;
};
} else {
color = () => DefaultColor;
}
return {
factory: CellPackProvidedColorTheme,
granularity: 'instance',
color,
props,
description: Description,
legend
};
}
export const CellPackProvidedColorThemeProvider: ColorTheme.Provider<CellPackProvidedColorThemeParams, 'cellpack-provided'> = {
name: 'cellpack-provided',
label: 'CellPack Provided',
category: ColorTheme.Category.Chain,
factory: CellPackProvidedColorTheme,
getParams: getCellPackProvidedColorThemeParams,
defaultValues: PD.getDefaultValues(CellPackProvidedColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => {
return (
!!ctx.structure && ctx.structure.elementCount > 0 &&
Model.TrajectoryInfo.get(ctx.structure.models[0]).size > 1 &&
!!CellPackInfoProvider.get(ctx.structure).value?.colors
);
}
};

View File

@@ -0,0 +1,229 @@
/**
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3, Quat, Mat4 } from '../../mol-math/linear-algebra';
import { NumberArray } from '../../mol-util/type-helpers';
interface Frame {
t: Vec3,
r: Vec3,
s: Vec3,
}
const a0Tmp = Vec3();
const a1Tmp = Vec3();
const a2Tmp = Vec3();
const a3Tmp = Vec3();
function CubicInterpolate(out: Vec3, y0: Vec3, y1: Vec3, y2: Vec3, y3: Vec3, mu: number): Vec3 {
const mu2 = mu * mu;
Vec3.sub(a0Tmp, y3, y2);
Vec3.sub(a0Tmp, a0Tmp, y0);
Vec3.add(a0Tmp, a0Tmp, y1);
Vec3.sub(a1Tmp, y0, y1);
Vec3.sub(a1Tmp, a1Tmp, a0Tmp);
Vec3.sub(a2Tmp, y2, y0);
Vec3.copy(a3Tmp, y1);
out[0] = a0Tmp[0] * mu * mu2 + a1Tmp[0] * mu2 + a2Tmp[0] * mu + a3Tmp[0];
out[1] = a0Tmp[1] * mu * mu2 + a1Tmp[1] * mu2 + a2Tmp[1] * mu + a3Tmp[1];
out[2] = a0Tmp[2] * mu * mu2 + a1Tmp[2] * mu2 + a2Tmp[2] * mu + a3Tmp[2];
return out;
}
const cp0 = Vec3();
const cp1 = Vec3();
const cp2 = Vec3();
const cp3 = Vec3();
const currentPosition = Vec3();
function ResampleControlPoints(points: NumberArray, segmentLength: number) {
const nP = points.length / 3;
// insert a point at the end and at the begining
// controlPoints.Insert(0, controlPoints[0] + (controlPoints[0] - controlPoints[1]) / 2.0f);
// controlPoints.Add(controlPoints[nP - 1] + (controlPoints[nP - 1] - controlPoints[nP - 2]) / 2.0f);
const resampledControlPoints: Vec3[] = [];
// resampledControlPoints.Add(controlPoints[0]);
// resampledControlPoints.Add(controlPoints[1]);
let idx = 1;
// const currentPosition = Vec3.create(points[idx * 3], points[idx * 3 + 1], points[idx * 3 + 2])
Vec3.fromArray(currentPosition, points, idx * 3);
let lerpValue = 0.0;
// Normalize the distance between control points
while (true) {
if (idx + 2 >= nP) break;
Vec3.fromArray(cp0, points, (idx - 1) * 3);
Vec3.fromArray(cp1, points, idx * 3);
Vec3.fromArray(cp2, points, (idx + 1) * 3);
Vec3.fromArray(cp3, points, (idx + 2) * 3);
// const cp0 = Vec3.create(points[(idx-1)*3], points[(idx-1)*3+1], points[(idx-1)*3+2]) // controlPoints[currentPointId - 1];
// const cp1 = Vec3.create(points[idx*3], points[idx*3+1], points[idx*3+2]) // controlPoints[currentPointId];
// const cp2 = Vec3.create(points[(idx+1)*3], points[(idx+1)*3+1], points[(idx+1)*3+2]) // controlPoints[currentPointId + 1];
// const cp3 = Vec3.create(points[(idx+2)*3], points[(idx+2)*3+1], points[(idx+2)*3+2]); // controlPoints[currentPointId + 2];
let found = false;
for (; lerpValue <= 1; lerpValue += 0.01) {
// lerp?slerp
// let candidate:Vec3 = Vec3.lerp(Vec3.zero(), cp0, cp1, lerpValue);
// const candidate:Vec3 = Vec3.bezier(Vec3.zero(), cp0, cp1, cp2, cp3, lerpValue);
const candidate = CubicInterpolate(Vec3(), cp0, cp1, cp2, cp3, lerpValue);
const d = Vec3.distance(currentPosition, candidate);
if (d > segmentLength) {
resampledControlPoints.push(candidate);
Vec3.copy(currentPosition, candidate);
found = true;
break;
}
}
if (!found) {
lerpValue = 0;
idx += 1;
}
}
return resampledControlPoints;
}
const prevV = Vec3();
const tmpV1 = Vec3();
const tmpV2 = Vec3();
const tmpV3 = Vec3();
// easier to align to theses normals
function GetSmoothNormals(points: Vec3[]) {
const nP: number = points.length;
const smoothNormals: Vec3[] = [];
if (points.length < 3) {
for (let i = 0; i < points.length; ++i)
smoothNormals.push(Vec3.normalize(Vec3(), points[i]));
return smoothNormals;
}
let p0 = points[0];
let p1 = points[1];
let p2 = points[2];
const p21 = Vec3.sub(tmpV1, p2, p1);
const p01 = Vec3.sub(tmpV2, p0, p1);
const p0121 = Vec3.cross(tmpV3, p01, p21);
Vec3.normalize(prevV, p0121);
smoothNormals.push(Vec3.clone(prevV));
for (let i = 1; i < points.length - 1; ++i) {
p0 = points[i - 1];
p1 = points[i];
p2 = points[i + 1];
const t = Vec3.normalize(tmpV1, Vec3.sub(tmpV1, p2, p0));
const b = Vec3.normalize(tmpV2, Vec3.cross(tmpV2, t, prevV));
const n = Vec3.normalize(Vec3(), Vec3.cross(tmpV3, t, b));
Vec3.negate(n, n);
Vec3.copy(prevV, n);
smoothNormals.push(n);
}
const last = Vec3();
Vec3.normalize(last, Vec3.cross(last,
Vec3.sub(tmpV1, points[nP - 3], points[nP - 2]),
Vec3.sub(tmpV2, points[nP - 2], points[nP - 1]))
);
smoothNormals.push(last);
return smoothNormals;
}
const frameTmpV1 = Vec3();
const frameTmpV2 = Vec3();
const frameTmpV3 = Vec3();
function getFrame(reference: Vec3, tangent: Vec3) {
const t = Vec3.normalize(Vec3(), tangent);
// make reference vector orthogonal to tangent
const proj_r_to_t = Vec3.scale(
frameTmpV1, tangent, Vec3.dot(reference, tangent) / Vec3.dot(tangent, tangent)
);
const r = Vec3.normalize(Vec3(), Vec3.sub(frameTmpV2, reference, proj_r_to_t));
// make bitangent vector orthogonal to the others
const s = Vec3.normalize(Vec3(), Vec3.cross(frameTmpV3, t, r));
return { t, r, s };
}
const mfTmpV1 = Vec3();
const mfTmpV2 = Vec3();
const mfTmpV3 = Vec3();
const mfTmpV4 = Vec3();
const mfTmpV5 = Vec3();
const mfTmpV6 = Vec3();
const mfTmpV7 = Vec3();
const mfTmpV8 = Vec3();
const mfTmpV9 = Vec3();
// easier to align to theses normals
// https://github.com/bzamecnik/gpg/blob/master/rotation-minimizing-frame/rmf.py
function GetMiniFrame(points: Vec3[], normals: Vec3[]) {
const frames: Frame[] = [];
const t0 = Vec3.normalize(mfTmpV1, Vec3.sub(mfTmpV1, points[1], points[0]));
frames.push(getFrame(normals[0], t0));
for (let i = 0; i < points.length - 2; ++i) {
const t2 = Vec3.normalize(mfTmpV1, Vec3.sub(mfTmpV1, points[i + 2], points[i + 1]));
const v1 = Vec3.sub(mfTmpV2, points[i + 1], points[i]); // this is tangeant
const c1 = Vec3.dot(v1, v1);
// compute r_i^L = R_1 * r_i
const v1r = Vec3.scale(mfTmpV3, v1, (2.0 / c1) * Vec3.dot(v1, frames[i].r));
const ref_L_i = Vec3.sub(mfTmpV4, frames[i].r, v1r);
// compute t_i^L = R_1 * t_i
const v1t = Vec3.scale(mfTmpV5, v1, (2.0 / c1) * Vec3.dot(v1, frames[i].t));
const tan_L_i = Vec3.sub(mfTmpV6, frames[i].t, v1t);
// # compute reflection vector of R_2
const v2 = Vec3.sub(mfTmpV7, t2, tan_L_i);
const c2 = Vec3.dot(v2, v2);
// compute r_(i+1) = R_2 * r_i^L
const v2l = Vec3.scale(mfTmpV8, v1, (2.0 / c2) * Vec3.dot(v2, ref_L_i));
const ref_next = Vec3.sub(mfTmpV9, ref_L_i, v2l); // ref_L_i - (2 / c2) * v2.dot(ref_L_i) * v2
frames.push(getFrame(ref_next, t2)); // frames.append(Frame(ref_next, tangents[i+1]))
}
return frames;
}
const rpTmpVec1 = Vec3();
export function getMatFromResamplePoints(points: NumberArray, segmentLength: number, resample: boolean) {
let new_points: Vec3[] = [];
if (resample) new_points = ResampleControlPoints(points, segmentLength);
else {
for (let idx = 0; idx < points.length / 3; ++idx) {
new_points.push(Vec3.fromArray(Vec3.zero(), points, idx * 3));
}
}
const npoints = new_points.length;
const new_normal = GetSmoothNormals(new_points);
const frames = GetMiniFrame(new_points, new_normal);
const limit = npoints;
const transforms: Mat4[] = [];
const pti = Vec3.copy(rpTmpVec1, new_points[0]);
for (let i = 0; i < npoints - 2; ++i) {
const pti1: Vec3 = new_points[i + 1]; // Vec3.create(points[(i+1)*3],points[(i+1)*3+1],points[(i+1)*3+2]);
const d = Vec3.distance(pti, pti1);
if (d >= segmentLength) {
// use twist or random?
const quat = Quat.rotationTo(Quat.zero(), Vec3.create(0, 0, 1), frames[i].t); // Quat.rotationTo(Quat.zero(), Vec3.create(0,0,1),new_normal[i]);//Quat.rotationTo(Quat.zero(), Vec3.create(0,0,1),direction);new_normal
const rq = Quat.setAxisAngle(Quat.zero(), frames[i].t, Math.random() * 3.60); // Quat.setAxisAngle(Quat.zero(),direction, Math.random()*3.60 );//Quat.identity();//
const m = Mat4.fromQuat(Mat4.zero(), Quat.multiply(Quat.zero(), rq, quat)); // Mat4.fromQuat(Mat4.zero(),Quat.multiply(Quat.zero(),quat1,quat2));//Mat4.fromQuat(Mat4.zero(),quat);//Mat4.identity();//Mat4.fromQuat(Mat4.zero(),Quat.multiply(Quat.zero(),rq,quat));
// let pos:Vec3 = Vec3.add(Vec3.zero(),pti1,pti)
// pos = Vec3.scale(pos,pos,1.0/2.0);
// Vec3.makeRotation(Mat4.zero(),Vec3.create(0,0,1),frames[i].t);//
Mat4.setTranslation(m, pti1);
// let m2:Mat4 = GetTubePropertiesMatrix(pti,pti1);
// let q:Quat = Quat.rotationTo(Quat.zero(), Vec3.create(0,1,0),Vec3.create(0,0,1))
// m2=Mat4.mul(Mat4.identity(),Mat4.fromQuat(Mat4.zero(),q),m2);
transforms.push(m);
Vec3.copy(pti, pti1);
}
if (transforms.length >= limit) break;
}
return transforms;
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3, Quat } from '../../mol-math/linear-algebra';
export interface CellPack {
cell: Cell
packings: CellPacking[]
}
export interface CellPacking {
name: string,
location: 'surface' | 'interior' | 'cytoplasme'
ingredients: Packing['ingredients']
compartment?: CellCompartment
}
export interface CellCompartment {
filename?: string
geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None'
compartment_primitives?: CompartmentPrimitives
}
export interface Cell {
recipe: Recipe
options?: RecipeOptions
cytoplasme?: Packing
compartments?: { [key: string]: Compartment }
mapping_ids?: { [key: number]: [number, string] }
}
export interface RecipeOptions {
resultfile?: string
}
export interface Recipe {
setupfile: string
paths: [string, string][] // [name: string, path: string][]
version: string
name: string
}
export interface Compartment {
surface?: Packing
interior?: Packing
geom?: unknown
geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None'
mb?: CompartmentPrimitives
}
// Primitives discribing a compartment
export const enum CompartmentPrimitiveType {
MetaBall = 0,
Sphere = 1,
Cube = 2,
Cylinder = 3,
Cone = 4,
Plane = 5,
None = 6
}
export interface CompartmentPrimitives{
positions?: number[];
radii?: number[];
types?: CompartmentPrimitiveType[];
}
export interface Packing {
ingredients: { [key: string]: Ingredient }
}
export interface Positions {
coords?: Vec3[];
}
export interface Radii {
radii?: number[];
}
export interface Ingredient {
source: IngredientSource;
results: [Vec3, Quat][];
name: string;
/** Vec3[]];CoarseGraind Beads coordinates LOD */
positions?: [Positions];
/** number[]];CoarseGraind Beads radii LOD */
radii?: [Radii];
/** Number of `curveX` properties in the object where `X` is a 0-indexed number */
nbCurve?: number;
uLength?: number;
/** Curve properties are Vec3[] but that is not expressable in TypeScript */
[curveX: string]: unknown;
/** the orientation in the membrane */
principalAxis?: Vec3;
principalVector?: Vec3;
/** offset along membrane */
offset?: Vec3;
ingtype?: string;
color?: Vec3;
confidence?: number;
Type?: string;
}
export interface IngredientSource {
pdb: string;
bu?: string; /** biological unit e.g AU,BU1,etc.. */
selection?: string; /** NGL selection or :A or :B etc.. */
model?: string; /** model number e.g 0,1,2... */
transform: {
center: boolean;
translate?: Vec3;
};
biomt?: boolean;
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginBehavior } from '../../mol-plugin/behavior';
import { LoadCellPackModel } from './model';
import { CellPackGenerateColorThemeProvider } from './color/generate';
import { CellPackProvidedColorThemeProvider } from './color/provided';
export const CellPack = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
name: 'cellpack',
category: 'custom-props',
display: {
name: 'CellPack',
description: 'CellPack Model Loading and Viewing.'
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
register(): void {
this.ctx.state.data.actions.add(LoadCellPackModel);
this.ctx.representation.structure.themes.colorThemeRegistry.add(CellPackGenerateColorThemeProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.add(CellPackProvidedColorThemeProvider);
}
unregister() {
this.ctx.state.data.actions.remove(LoadCellPackModel);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(CellPackGenerateColorThemeProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(CellPackProvidedColorThemeProvider);
}
}
});

View File

@@ -0,0 +1,621 @@
/**
* Copyright (c) 2019-2022 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>
*/
import { StateAction, StateBuilder, StateTransformer, State } from '../../mol-state';
import { PluginContext } from '../../mol-plugin/context';
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Ingredient, CellPacking, CompartmentPrimitives } from './data';
import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
import { trajectoryFromPDB } from '../../mol-model-formats/structure/pdb';
import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
import { SymmetryOperator } from '../../mol-math/geometry';
import { Task, RuntimeContext } from '../../mol-task';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies, CreateCompartmentSphere } from './state';
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
import { getMatFromResamplePoints } from './curve';
import { compile } from '../../mol-script/runtime/query/compiler';
import { CellpackPackingPreset, CellpackMembranePreset } from './preset';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import { objectForEach } from '../../mol-util/object';
import { readFromFile } from '../../mol-util/data-source';
import { ColorNames } from '../../mol-util/color/names';
function getCellPackModelUrl(fileName: string, baseUrl: string) {
return `${baseUrl}/results/${fileName}`;
}
class TrajectoryCache {
private map = new Map<string, Trajectory>();
set(id: string, trajectory: Trajectory) { this.map.set(id, trajectory); }
get(id: string) { return this.map.get(id); }
}
async function getModel(plugin: PluginContext, id: string, ingredient: Ingredient,
baseUrl: string, trajCache: TrajectoryCache, location: string,
file?: Asset.File
) {
const assetManager = plugin.managers.asset;
const modelIndex = (ingredient.source.model) ? parseInt(ingredient.source.model) : 0;
let surface = (ingredient.ingtype) ? (ingredient.ingtype === 'transmembrane') : false;
if (location === 'surface') surface = true;
let trajectory = trajCache.get(id);
const assets: Asset.Wrapper[] = [];
if (!trajectory) {
if (file) {
if (file.name.endsWith('.cif')) {
const text = await plugin.runTask(assetManager.resolve(file, 'string'));
assets.push(text);
const cif = (await parseCif(plugin, text.data)).blocks[0];
trajectory = await plugin.runTask(trajectoryFromMmCIF(cif));
} else if (file.name.endsWith('.bcif')) {
const binary = await plugin.runTask(assetManager.resolve(file, 'binary'));
assets.push(binary);
const cif = (await parseCif(plugin, binary.data)).blocks[0];
trajectory = await plugin.runTask(trajectoryFromMmCIF(cif));
} else if (file.name.endsWith('.pdb')) {
const text = await plugin.runTask(assetManager.resolve(file, 'string'));
assets.push(text);
const pdb = await parsePDBfile(plugin, text.data, id);
trajectory = await plugin.runTask(trajectoryFromPDB(pdb));
} else {
throw new Error(`unsupported file type '${file.name}'`);
}
} else if (id.match(/^[1-9][a-zA-Z0-9]{3,3}$/i)) {
if (surface) {
try {
const data = await getFromOPM(plugin, id, assetManager);
assets.push(data.asset);
data.pdb.id! = id.toUpperCase();
trajectory = await plugin.runTask(trajectoryFromPDB(data.pdb));
} catch (e) {
// fallback to getFromPdb
// console.error(e);
const { mmcif, asset } = await getFromPdb(plugin, id, assetManager);
assets.push(asset);
trajectory = await plugin.runTask(trajectoryFromMmCIF(mmcif));
}
} else {
const { mmcif, asset } = await getFromPdb(plugin, id, assetManager);
assets.push(asset);
trajectory = await plugin.runTask(trajectoryFromMmCIF(mmcif));
}
} else {
const data = await getFromCellPackDB(plugin, id, baseUrl, assetManager);
assets.push(data.asset);
if ('pdb' in data) {
trajectory = await plugin.runTask(trajectoryFromPDB(data.pdb));
} else {
trajectory = await plugin.runTask(trajectoryFromMmCIF(data.mmcif));
}
}
trajCache.set(id, trajectory!);
}
const model = await plugin.resolveTask(trajectory?.getFrameAtIndex(modelIndex)!);
return { model, assets };
}
async function getStructure(plugin: PluginContext, model: Model, source: Ingredient, props: { assembly?: string } = {}) {
let structure = Structure.ofModel(model);
const { assembly } = props;
if (assembly) {
structure = await plugin.runTask(StructureSymmetry.buildAssembly(structure, assembly));
}
let query;
if (source.source.selection) {
const sel = source.source.selection;
// selection can have the model ID as well. remove it
const asymIds: string[] = sel.replace(/ /g, '').replace(/:/g, '').split('or').slice(1);
query = MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'chain-test': MS.core.set.has([MS.set(...asymIds), MS.ammp('auth_asym_id')])
})
]);
} else {
query = MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer'])
})
]);
}
const compiled = compile<StructureSelection>(query);
const result = compiled(new QueryContext(structure));
structure = StructureSelection.unionStructure(result);
// change here if possible the label ?
// structure.label = source.name;
return structure;
}
function getTransformLegacy(trans: Vec3, rot: Quat) {
const q: Quat = Quat.create(-rot[3], rot[0], rot[1], rot[2]);
const m: Mat4 = Mat4.fromQuat(Mat4(), q);
Mat4.transpose(m, m);
Mat4.scale(m, m, Vec3.create(-1.0, 1.0, -1.0));
Mat4.setTranslation(m, trans);
return m;
}
function getTransform(trans: Vec3, rot: Quat) {
const q: Quat = Quat.create(-rot[0], rot[1], rot[2], -rot[3]);
const m: Mat4 = Mat4.fromQuat(Mat4(), q);
const p: Vec3 = Vec3.create(-trans[0], trans[1], trans[2]);
Mat4.setTranslation(m, p);
return m;
}
function getResultTransforms(results: Ingredient['results'], legacy: boolean) {
if (legacy) return results.map((r: Ingredient['results'][0]) => getTransformLegacy(r[0], r[1]));
else return results.map((r: Ingredient['results'][0]) => getTransform(r[0], r[1]));
}
function getCurveTransforms(ingredient: Ingredient) {
const n = ingredient.nbCurve || 0;
const instances: Mat4[] = [];
let segmentLength = 3.4;
if (ingredient.uLength) {
segmentLength = ingredient.uLength;
} else if (ingredient.radii) {
segmentLength = ingredient.radii[0].radii
? ingredient.radii[0].radii[0] * 2.0
: 3.4;
}
let resampling: boolean = false;
for (let i = 0; i < n; ++i) {
const cname = `curve${i}`;
if (!(cname in ingredient)) {
console.warn(`Expected '${cname}' in ingredient`);
continue;
}
const _points = ingredient[cname] as Vec3[];
if (_points.length <= 2) {
// TODO handle curve with 2 or less points
continue;
}
// test for resampling
const distance: number = Vec3.distance(_points[0], _points[1]);
if (distance >= segmentLength + 2.0) {
// console.info(distance);
resampling = true;
}
const points = new Float32Array(_points.length * 3);
for (let i = 0, il = _points.length; i < il; ++i) Vec3.toArray(_points[i], points, i * 3);
const newInstances = getMatFromResamplePoints(points, segmentLength, resampling);
instances.push(...newInstances);
}
return instances;
}
function getAssembly(name: string, transforms: Mat4[], structure: Structure) {
const builder = Structure.Builder({ label: name });
const { units } = structure;
for (let i = 0, il = transforms.length; i < il; ++i) {
const id = `${i + 1}`;
const op = SymmetryOperator.create(id, transforms[i], { assembly: { id, operId: i, operList: [id] } });
for (const unit of units) {
builder.addWithOperator(unit, op);
}
}
return builder.getStructure();
}
async function getCurve(name: string, transforms: Mat4[], model: Model) {
const structure = Structure.ofModel(model);
const assembly = getAssembly(name, transforms, structure);
return assembly;
}
async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache, location: 'surface' | 'interior' | 'cytoplasme') {
const { name, source, results, nbCurve } = ingredient;
if (source.pdb === 'None') return;
const file = ingredientFiles[source.pdb];
if (!file) {
// TODO can these be added to the library?
if (name === 'HIV1_CAhex_0_1_0') return; // 1VU4CtoH_hex.pdb
if (name === 'HIV1_CAhexCyclophilA_0_1_0') return; // 1AK4fitTo1VU4hex.pdb
if (name === 'iLDL') return; // EMD-5239
if (name === 'peptides') return; // peptide.pdb
if (name === 'lypoglycane') return;
}
// model id in case structure is NMR
const { model, assets } = await getModel(plugin, source.pdb || name, ingredient, baseUrl, trajCache, location, file);
if (!model) return;
let structure: Structure;
if (nbCurve) {
structure = await getCurve(name, getCurveTransforms(ingredient), model);
} else {
if ((!results || results.length === 0)) return;
let bu: string|undefined = source.bu ? source.bu : undefined;
if (bu) {
if (bu === 'AU') {
bu = undefined;
} else {
bu = bu.slice(2);
}
}
structure = await getStructure(plugin, model, ingredient, { assembly: bu });
// transform with offset and pcp
let legacy: boolean = true;
const pcp = ingredient.principalVector ? ingredient.principalVector : ingredient.principalAxis;
if (pcp) {
legacy = false;
const structureMean = getStructureMean(structure);
Vec3.negate(structureMean, structureMean);
const m1: Mat4 = Mat4.identity();
Mat4.setTranslation(m1, structureMean);
structure = Structure.transform(structure, m1);
if (ingredient.offset) {
const o: Vec3 = Vec3.create(ingredient.offset[0], ingredient.offset[1], ingredient.offset[2]);
if (!Vec3.exactEquals(o, Vec3())) { // -1, 1, 4e-16 ??
if (location !== 'surface') {
Vec3.negate(o, o);
}
const m: Mat4 = Mat4.identity();
Mat4.setTranslation(m, o);
structure = Structure.transform(structure, m);
}
}
if (pcp) {
const p: Vec3 = Vec3.create(pcp[0], pcp[1], pcp[2]);
if (!Vec3.exactEquals(p, Vec3.unitZ)) {
const q: Quat = Quat.identity();
Quat.rotationTo(q, p, Vec3.unitZ);
const m: Mat4 = Mat4.fromQuat(Mat4(), q);
structure = Structure.transform(structure, m);
}
}
}
structure = getAssembly(name, getResultTransforms(results, legacy), structure);
}
return { structure, assets };
}
export function createStructureFromCellPack(plugin: PluginContext, packing: CellPacking, baseUrl: string, ingredientFiles: IngredientFiles) {
return Task.create('Create Packing Structure', async ctx => {
const { ingredients, location, name } = packing;
const assets: Asset.Wrapper[] = [];
const trajCache = new TrajectoryCache();
const structures: Structure[] = [];
const colors: Color[] = [];
for (const iName in ingredients) {
if (ctx.shouldUpdate) await ctx.update(iName);
const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache, location);
if (ingredientStructure) {
structures.push(ingredientStructure.structure);
assets.push(...ingredientStructure.assets);
const c = ingredients[iName].color;
if (c) {
colors.push(Color.fromNormalizedRgb(c[0], c[1], c[2]));
} else {
colors.push(Color.fromNormalizedRgb(1, 0, 0));
}
}
}
if (ctx.shouldUpdate) await ctx.update(`${name} - units`);
const units: Unit[] = [];
let offsetInvariantId = 0;
let offsetChainGroupId = 0;
for (const s of structures) {
if (ctx.shouldUpdate) await ctx.update(`${s.label}`);
let maxInvariantId = 0;
const maxChainGroupId = 0;
for (const u of s.units) {
const invariantId = u.invariantId + offsetInvariantId;
const chainGroupId = u.chainGroupId + offsetChainGroupId;
if (u.invariantId > maxInvariantId) maxInvariantId = u.invariantId;
units.push(Unit.create(units.length, invariantId, chainGroupId, u.traits, u.kind, u.model, u.conformation.operator, u.elements, u.props));
}
offsetInvariantId += maxInvariantId + 1;
offsetChainGroupId += maxChainGroupId + 1;
}
if (ctx.shouldUpdate) await ctx.update(`${name} - structure`);
const structure = Structure.create(units, { label: name + '.' + location });
for (let i = 0, il = structure.models.length; i < il; ++i) {
Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
}
return { structure, assets, colors: colors };
});
}
async function handleHivRna(plugin: PluginContext, packings: CellPacking[], baseUrl: string) {
for (let i = 0, il = packings.length; i < il; ++i) {
if (packings[i].name === 'HIV1_capsid_3j3q_PackInner_0_1_0' || packings[i].name === 'HIV_capsid') {
const url = Asset.getUrlAsset(plugin.managers.asset, `${baseUrl}/extras/rna_allpoints.json`);
const json = await plugin.runTask(plugin.managers.asset.resolve(url, 'json', false));
const points = json.data.points as number[];
const curve0: Vec3[] = [];
for (let j = 0, jl = points.length; j < jl; j += 3) {
curve0.push(Vec3.fromArray(Vec3(), points, j));
}
packings[i].ingredients['RNA'] = {
source: { pdb: 'RNA_U_Base.pdb', transform: { center: false } },
results: [],
name: 'RNA',
nbCurve: 1,
curve0
};
}
}
}
async function loadMembrane(plugin: PluginContext, name: string, state: State, params: LoadCellPackModelParams) {
let file: Asset.File | undefined = undefined;
if (params.ingredients !== null) {
const fileName = `${name}.bcif`;
for (const f of params.ingredients) {
if (fileName === f.name) {
file = f;
break;
}
}
if (!file) {
// check for cif directly
const cifileName = `${name}.cif`;
for (const f of params.ingredients) {
if (cifileName === f.name) {
file = f;
break;
}
}
}
}
let legacy_membrane: boolean = false; // temporary variable until all membrane are converted to the new correct cif format
let geometry_membrane: boolean = false; // membrane can be a mesh geometry
let b = state.build().toRoot();
if (file) {
if (file.name.endsWith('.cif')) {
b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: false, label: file.name }, { state: { isGhost: true } });
} else if (file.name.endsWith('.bcif')) {
b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: true, label: file.name }, { state: { isGhost: true } });
}
} else {
if (name.toLowerCase().endsWith('.bcif')) {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
} else if (name.toLowerCase().endsWith('.cif')) {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } });
} else if (name.toLowerCase().endsWith('.ply')) {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/geometries/${name}`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } });
geometry_membrane = true;
} else {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
legacy_membrane = true;
}
}
const props = {
type: {
name: 'assembly' as const,
params: { id: '1' }
}
};
if (legacy_membrane) {
// old membrane
const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
.apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
.commit({ revertOnError: true });
const membraneParams = {
ignoreLight: params.preset.adjustStyle,
representation: params.preset.representation,
};
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
} else if (geometry_membrane) {
await b.apply(StateTransforms.Data.ParsePly, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ShapeFromPly)
.apply(StateTransforms.Representation.ShapeRepresentation3D, { xrayShaded: true,
doubleSided: true, coloring: { name: 'uniform', params: { color: ColorNames.orange } } })
.commit({ revertOnError: true });
} else {
const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.StructureFromModel, props, { state: { isGhost: true } })
.commit({ revertOnError: true });
const membraneParams = {
ignoreLight: params.preset.adjustStyle,
representation: params.preset.representation,
};
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
}
}
async function handleMembraneSpheres(state: State, primitives: CompartmentPrimitives) {
const nSpheres = primitives.positions!.length / 3;
// console.log('ok mb ', nSpheres);
// TODO : take in account the type of the primitives.
for (let j = 0; j < nSpheres; j++) {
await state.build()
.toRoot()
.apply(CreateCompartmentSphere, {
center: Vec3.create(
primitives.positions![j * 3 + 0],
primitives.positions![j * 3 + 1],
primitives.positions![j * 3 + 2]
),
radius: primitives!.radii![j]
})
.commit();
}
}
async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
const ingredientFiles = params.ingredients || [];
let cellPackJson: StateBuilder.To<PSO.Format.Json, StateTransformer<PSO.Data.String, PSO.Format.Json>>;
let resultsFile: Asset.File | null = params.results;
if (params.source.name === 'id') {
const url = Asset.getUrlAsset(plugin.managers.asset, getCellPackModelUrl(params.source.params, params.baseUrl));
cellPackJson = state.build().toRoot()
.apply(StateTransforms.Data.Download, { url, isBinary: false, label: params.source.params }, { state: { isGhost: true } });
} else {
const file = params.source.params;
if (!file?.file) {
plugin.log.error('No file selected');
return;
}
let modelFile: Asset.File;
if (file.name.toLowerCase().endsWith('.zip')) {
const data = await readFromFile(file.file, 'zip').runInContext(runtime);
if (data['model.json']) {
modelFile = Asset.File(new File([data['model.json']], 'model.json'));
} else {
throw new Error('model.json missing from zip file');
}
if (data['results.bin']) {
resultsFile = Asset.File(new File([data['results.bin']], 'results.bin'));
}
objectForEach(data, (v, k) => {
if (k === 'model.json') return;
if (k === 'results.bin') return;
ingredientFiles.push(Asset.File(new File([v], k)));
});
} else {
modelFile = file;
}
cellPackJson = state.build().toRoot()
.apply(StateTransforms.Data.ReadFile, { file: modelFile, isBinary: false, label: modelFile.name }, { state: { isGhost: true } });
}
const cellPackBuilder = cellPackJson
.apply(StateTransforms.Data.ParseJson, undefined, { state: { isGhost: true } })
.apply(ParseCellPack, { resultsFile, baseUrl: params.baseUrl });
const cellPackObject = await state.updateTree(cellPackBuilder).runInContext(runtime);
const { packings } = cellPackObject.obj!.data;
await handleHivRna(plugin, packings, params.baseUrl);
for (let i = 0, il = packings.length; i < il; ++i) {
const p = { packing: i, baseUrl: params.baseUrl, ingredientFiles };
const packing = await state.build()
.to(cellPackBuilder.ref)
.apply(StructureFromCellpack, p)
.commit({ revertOnError: true });
const packingParams = {
traceOnly: params.preset.traceOnly,
ignoreLight: params.preset.adjustStyle,
representation: params.preset.representation,
};
await CellpackPackingPreset.apply(packing, packingParams, plugin);
if (packings[i].compartment) {
if (params.membrane === 'lipids') {
if (packings[i].compartment!.geom_type) {
if (packings[i].compartment!.geom_type === 'file') {
// TODO: load mesh files or vertex,faces data
await loadMembrane(plugin, packings[i].compartment!.filename!, state, params);
} else if (packings[i].compartment!.compartment_primitives) {
await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!);
}
} else {
// try loading membrane from repo as a bcif file or from the given list of files.
if (params.membrane === 'lipids') {
await loadMembrane(plugin, packings[i].name, state, params);
}
}
} else if (params.membrane === 'geometry') {
if (packings[i].compartment!.compartment_primitives) {
await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!);
} else if (packings[i].compartment!.geom_type === 'file') {
if (packings[i].compartment!.filename!.toLowerCase().endsWith('.ply')) {
await loadMembrane(plugin, packings[i].compartment!.filename!, state, params);
}
}
}
}
}
}
const LoadCellPackModelParams = {
source: PD.MappedStatic('id', {
'id': PD.Select('InfluenzaModel2.json', [
['blood_hiv_immature_inside.json', 'Blood HIV immature'],
['HIV_immature_model.json', 'HIV immature'],
['Blood_HIV.json', 'Blood HIV'],
['HIV-1_0.1.6-8_mixed_radii_pdb.json', 'HIV'],
['influenza_model1.json', 'Influenza envelope'],
['InfluenzaModel2.json', 'Influenza complete'],
['ExosomeModel.json', 'Exosome Model'],
['MycoplasmaGenitalium.json', 'Mycoplasma Genitalium curated model'],
] as const, { description: 'Download the model definition with `id` from the server at `baseUrl.`' }),
'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.', label: 'Recipe file' }),
}, { options: [['id', 'Id'], ['file', 'File']] }),
baseUrl: PD.Text(DefaultCellPackBaseUrl),
results: PD.File({ accept: '.bin', description: 'open results file in binary format from cellpackgpu for the specified recipe', label: 'Results file' }),
membrane: PD.Select('lipids', PD.arrayToOptions(['lipids', 'geometry', 'none'])),
ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredient files' }),
preset: PD.Group({
traceOnly: PD.Boolean(false),
adjustStyle: PD.Boolean(true),
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation'] as const))
}, { isExpanded: true })
};
type LoadCellPackModelParams = PD.Values<typeof LoadCellPackModelParams>
export const LoadCellPackModel = StateAction.build({
display: { name: 'Load CellPack', description: 'Open or download a model' },
params: LoadCellPackModelParams,
from: PSO.Root
})(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
if (params.preset.adjustStyle) {
ctx.managers.interactivity.setProps({ granularity: 'chain' });
ctx.managers.structure.component.setOptions({
... ctx.managers.structure.component.state.options,
visualQuality: 'custom',
ignoreLight: true,
showHydrogens: false,
});
ctx.canvas3d?.setProps({
multiSample: { mode: 'off' },
cameraClipping: { far: false },
renderer: { colorMarker: false },
marking: {
enabled: true,
ghostEdgeStrength: 1,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
radius: 8,
bias: 1,
blurKernelSize: 15,
resolutionScale: 1,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.33,
color: ColorNames.black,
}
}
}
});
}
await loadPackings(ctx, taskCtx, state, params);
}));

View File

@@ -0,0 +1,100 @@
/**
* Copyright (c) 2019-2022 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>
*/
import { StateObjectRef } from '../../mol-state';
import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../mol-plugin-state/builder/structure/representation-preset';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ColorNames } from '../../mol-util/color/names';
import { CellPackGenerateColorThemeProvider } from './color/generate';
export const CellpackPackingPresetParams = {
traceOnly: PD.Boolean(true),
ignoreLight: PD.Boolean(false),
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'] as const)),
};
export type CellpackPackingPresetParams = PD.ValuesFor<typeof CellpackPackingPresetParams>
export const CellpackPackingPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-cellpack-packing',
display: { name: 'CellPack Packing' },
params: () => CellpackPackingPresetParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
if (!structureCell) return {};
const reprProps = {
ignoreHydrogens: true,
traceOnly: params.traceOnly,
instanceGranularity: true,
ignoreLight: params.ignoreLight,
};
const components = {
polymer: await presetStaticComponent(plugin, structureCell, 'polymer')
};
if (params.representation === 'gaussian-surface') {
Object.assign(reprProps, {
quality: 'custom', resolution: 10, radiusOffset: 2, doubleSided: false
});
} else if (params.representation === 'spacefill') {
Object.assign(reprProps, { sizeFactor: params.traceOnly ? 2 : 1 });
}
// default is generated
const color = CellPackGenerateColorThemeProvider.name;
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
const representations = {
polymer: builder.buildRepresentation<any>(update, components.polymer, { type: params.representation, typeParams: { ...typeParams, ...reprProps }, color }, { tag: 'polymer' })
};
await update.commit({ revertOnError: true });
return { components, representations };
}
});
//
export const CellpackMembranePresetParams = {
ignoreLight: PD.Boolean(false),
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'] as const)),
};
export type CellpackMembranePresetParams = PD.ValuesFor<typeof CellpackMembranePresetParams>
export const CellpackMembranePreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-cellpack-membrane',
display: { name: 'CellPack Membrane' },
params: () => CellpackMembranePresetParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
if (!structureCell) return {};
const reprProps = {
ignoreHydrogens: true,
instanceGranularity: true,
ignoreLight: params.ignoreLight,
};
const components = {
membrane: await presetStaticComponent(plugin, structureCell, 'all', { label: 'Membrane' })
};
if (params.representation === 'gaussian-surface') {
Object.assign(reprProps, {
quality: 'custom', resolution: 10, radiusOffset: 2, doubleSided: false
});
}
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
const representations = {
membrane: builder.buildRepresentation(update, components.membrane, { type: params.representation, typeParams: { ...typeParams, ...reprProps }, color: 'uniform', colorParams: { value: ColorNames.lightgrey } }, { tag: 'all' })
};
await update.commit({ revertOnError: true });
return { components, representations };
}
});

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { CustomStructureProperty } from '../../mol-model-props/common/custom-structure-property';
import { Structure } from '../../mol-model/structure';
import { CustomProperty } from '../../mol-model-props/common/custom-property';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Color } from '../../mol-util/color';
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
export type CellPackInfoValue = {
packingsCount: number
packingIndex: number
colors?: Color[]
}
const CellPackInfoParams = {
info: PD.Value<CellPackInfoValue>({ packingsCount: 1, packingIndex: 0, colors: undefined }, { isHidden: true })
};
type CellPackInfoParams = PD.Values<typeof CellPackInfoParams>
export const CellPackInfoProvider: CustomStructureProperty.Provider<typeof CellPackInfoParams, CellPackInfoValue> = CustomStructureProperty.createProvider({
label: 'CellPack Info',
descriptor: CustomPropertyDescriptor({ name: 'cellpack-info' }),
type: 'root',
defaultParams: CellPackInfoParams,
getParams: (data: Structure) => CellPackInfoParams,
isApplicable: (data: Structure) => true,
obtain: async (ctx: CustomProperty.Context, data: Structure, props: CellPackInfoParams) => {
return {
value: { ...CellPackInfoParams.info.defaultValue, ...props.info }
};
}
});

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2019-2021 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>
*/
import { ShapeRepresentation } from '../../mol-repr/shape/representation';
import { Shape } from '../../mol-model/shape';
import { ColorNames } from '../../mol-util/color/names';
import { RuntimeContext } from '../../mol-task';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
// import { Polyhedron, DefaultPolyhedronProps } from '../../mol-geo/primitive/polyhedron';
// import { Icosahedron } from '../../mol-geo/primitive/icosahedron';
import { Sphere } from '../../mol-geo/primitive/sphere';
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { RepresentationParamsGetter, Representation, RepresentationContext } from '../../mol-repr/representation';
interface MembraneSphereData {
radius: number
center: Vec3
}
const MembraneSphereParams = {
...Mesh.Params,
cellColor: PD.Color(ColorNames.orange),
cellScale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
radius: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
center: PD.Vec3(Vec3.create(0, 0, 0)),
quality: { ...Mesh.Params.quality, isEssential: false },
};
type MeshParams = typeof MembraneSphereParams
const MembraneSphereVisuals = {
'mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MeshParams>) => ShapeRepresentation(getMBShape, Mesh.Utils),
};
export const MBParams = {
...MembraneSphereParams
};
export type MBParams = typeof MBParams
export type UnitcellProps = PD.Values<MBParams>
function getMBMesh(data: MembraneSphereData, props: UnitcellProps, mesh?: Mesh) {
const state = MeshBuilder.createState(256, 128, mesh);
const radius = props.radius;
const asphere = Sphere(3);
const trans: Mat4 = Mat4.identity();
Mat4.fromScaling(trans, Vec3.create(radius, radius, radius));
state.currentGroup = 1;
MeshBuilder.addPrimitive(state, trans, asphere);
const m = MeshBuilder.getMesh(state);
return m;
}
function getMBShape(ctx: RuntimeContext, data: MembraneSphereData, props: UnitcellProps, shape?: Shape<Mesh>) {
const geo = getMBMesh(data, props, shape && shape.geometry);
const label = 'mb';
return Shape.create(label, data, geo, () => props.cellColor, () => 1, () => label);
}
export type MBRepresentation = Representation<MembraneSphereData, MBParams>
export function MBRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MBParams>): MBRepresentation {
return Representation.createMulti('MB', ctx, getParams, Representation.StateBuilder, MembraneSphereVisuals as unknown as Representation.Def<MembraneSphereData, MBParams>);
}

View File

@@ -0,0 +1,326 @@
/**
* Copyright (c) 2019-2021 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>
*/
import { PluginStateObject as PSO, PluginStateTransform } from '../../mol-plugin-state/objects';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Task } from '../../mol-task';
import { CellPack as _CellPack, Cell, CellPacking } from './data';
import { createStructureFromCellPack } from './model';
import { IngredientFiles } from './util';
import { Asset } from '../../mol-util/assets';
import { PluginContext } from '../../mol-plugin/context';
import { CellPackInfoProvider } from './property';
import { Structure, StructureSymmetry, Unit, Model } from '../../mol-model/structure';
import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
import { Vec3, Quat } from '../../mol-math/linear-algebra';
import { StateTransformer } from '../../mol-state';
import { MBRepresentation, MBParams } from './representation';
import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
import { getFloatValue } from './util';
export const DefaultCellPackBaseUrl = 'https://raw.githubusercontent.com/mesoscope/cellPACK_data/master/cellPACK_database_1.1.0';
export class CellPack extends PSO.Create<_CellPack>({ name: 'CellPack', typeClass: 'Object' }) { }
export { ParseCellPack };
type ParseCellPack = typeof ParseCellPack
const ParseCellPack = PluginStateTransform.BuiltIn({
name: 'parse-cellpack',
display: { name: 'Parse CellPack', description: 'Parse CellPack from JSON data' },
from: PSO.Format.Json,
to: CellPack,
params: a => {
return {
resultsFile: PD.File({ accept: '.bin' }),
baseUrl: PD.Text(DefaultCellPackBaseUrl)
};
}
})({
apply({ a, params, cache }, plugin: PluginContext) {
return Task.create('Parse CellPack', async ctx => {
const cell = a.data as Cell;
let counter_id = 0;
let fiber_counter_id = 0;
let comp_counter = 0;
const packings: CellPacking[] = [];
const { compartments, cytoplasme } = cell;
if (!cell.mapping_ids) cell.mapping_ids = {};
if (cytoplasme) {
packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients });
for (const iName in cytoplasme.ingredients) {
if (cytoplasme.ingredients[iName].ingtype === 'fiber') {
cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
if (!cytoplasme.ingredients[iName].nbCurve) cytoplasme.ingredients[iName].nbCurve = 0;
fiber_counter_id++;
} else {
cell.mapping_ids[counter_id] = [comp_counter, iName];
if (!cytoplasme.ingredients[iName].results) { cytoplasme.ingredients[iName].results = []; }
counter_id++;
}
}
comp_counter++;
}
if (compartments) {
for (const name in compartments) {
const { surface, interior } = compartments[name];
let filename = '';
if (compartments[name].geom_type === 'file') {
filename = (compartments[name].geom) ? compartments[name].geom as string : '';
}
const compartment = { filename: filename, geom_type: compartments[name].geom_type, compartment_primitives: compartments[name].mb };
if (surface) {
packings.push({ name, location: 'surface', ingredients: surface.ingredients, compartment: compartment });
for (const iName in surface.ingredients) {
if (surface.ingredients[iName].ingtype === 'fiber') {
cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
if (!surface.ingredients[iName].nbCurve) surface.ingredients[iName].nbCurve = 0;
fiber_counter_id++;
} else {
cell.mapping_ids[counter_id] = [comp_counter, iName];
if (!surface.ingredients[iName].results) { surface.ingredients[iName].results = []; }
counter_id++;
}
}
comp_counter++;
}
if (interior) {
if (!surface) packings.push({ name, location: 'interior', ingredients: interior.ingredients, compartment: compartment });
else packings.push({ name, location: 'interior', ingredients: interior.ingredients });
for (const iName in interior.ingredients) {
if (interior.ingredients[iName].ingtype === 'fiber') {
cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
if (!interior.ingredients[iName].nbCurve) interior.ingredients[iName].nbCurve = 0;
fiber_counter_id++;
} else {
cell.mapping_ids[counter_id] = [comp_counter, iName];
if (!interior.ingredients[iName].results) { interior.ingredients[iName].results = []; }
counter_id++;
}
}
comp_counter++;
}
}
}
const { options } = cell;
let resultsAsset: Asset.Wrapper<'binary'> | undefined;
if (params.resultsFile) {
resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(params.resultsFile, 'binary', true));
} else if (options?.resultfile) {
const url = `${params.baseUrl}/results/${options.resultfile}`;
resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(Asset.getUrlAsset(plugin.managers.asset, url), 'binary', true));
}
if (resultsAsset) {
(cache as any).asset = resultsAsset;
const results = resultsAsset.data;
// flip the byte order if needed
const buffer = IsNativeEndianLittle ? results.buffer : flipByteOrder(results, 4);
const numbers = new DataView(buffer);
const ninst = getFloatValue(numbers, 0);
const npoints = getFloatValue(numbers, 4);
const ncurve = getFloatValue(numbers, 8);
let offset = 12;
if (ninst !== 0) {
const pos = new Float32Array(buffer, offset, ninst * 4);
offset += ninst * 4 * 4;
const quat = new Float32Array(buffer, offset, ninst * 4);
offset += ninst * 4 * 4;
for (let i = 0; i < ninst; i++) {
const x: number = pos[i * 4 + 0];
const y: number = pos[i * 4 + 1];
const z: number = pos[i * 4 + 2];
const ingr_id = pos[i * 4 + 3] as number;
const pid = cell.mapping_ids![ingr_id];
if (!packings[pid[0]].ingredients[pid[1]].results) {
packings[pid[0]].ingredients[pid[1]].results = [];
}
packings[pid[0]].ingredients[pid[1]].results.push([Vec3.create(x, y, z),
Quat.create(quat[i * 4 + 0], quat[i * 4 + 1], quat[i * 4 + 2], quat[i * 4 + 3])]);
}
}
if (npoints !== 0) {
const ctr_pos = new Float32Array(buffer, offset, npoints * 4);
offset += npoints * 4 * 4;
offset += npoints * 4 * 4;
const ctr_info = new Float32Array(buffer, offset, npoints * 4);
offset += npoints * 4 * 4;
const curve_ids = new Float32Array(buffer, offset, ncurve * 4);
offset += ncurve * 4 * 4;
let counter = 0;
let ctr_points: Vec3[] = [];
let prev_ctype = 0;
let prev_cid = 0;
for (let i = 0; i < npoints; i++) {
const x: number = -ctr_pos[i * 4 + 0];
const y: number = ctr_pos[i * 4 + 1];
const z: number = ctr_pos[i * 4 + 2];
const cid: number = ctr_info[i * 4 + 0]; // curve id
const ctype: number = curve_ids[cid * 4 + 0]; // curve type
// cid 148 165 -1 0
// console.log("cid ",cid,ctype,prev_cid,prev_ctype);//165,148
if (prev_ctype !== ctype) {
const pid = cell.mapping_ids![-prev_ctype - 1];
const cname = `curve${counter}`;
packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1;
packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
ctr_points = [];
counter = 0;
} else if (prev_cid !== cid) {
ctr_points = [];
const pid = cell.mapping_ids![-prev_ctype - 1];
const cname = `curve${counter}`;
packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
counter += 1;
}
ctr_points.push(Vec3.create(x, y, z));
prev_ctype = ctype;
prev_cid = cid;
}
// do the last one
const pid = cell.mapping_ids![-prev_ctype - 1];
const cname = `curve${counter}`;
packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1;
packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
}
}
return new CellPack({ cell, packings });
});
},
dispose({ cache }) {
((cache as any)?.asset as Asset.Wrapper | undefined)?.dispose();
},
});
export { StructureFromCellpack };
type StructureFromCellpack = typeof ParseCellPack
const StructureFromCellpack = PluginStateTransform.BuiltIn({
name: 'structure-from-cellpack',
display: { name: 'Structure from CellPack', description: 'Create Structure from CellPack Packing' },
from: CellPack,
to: PSO.Molecule.Structure,
params: a => {
const options = a ? a.data.packings.map((d, i) => [i, d.name] as const) : [];
return {
packing: PD.Select(0, options),
baseUrl: PD.Text(DefaultCellPackBaseUrl),
ingredientFiles: PD.FileList({ accept: '.cif,.bcif,.pdb' })
};
}
})({
apply({ a, params, cache }, plugin: PluginContext) {
return Task.create('Structure from CellPack', async ctx => {
const packing = a.data.packings[params.packing];
const ingredientFiles: IngredientFiles = {};
if (params.ingredientFiles !== null) {
for (const file of params.ingredientFiles) {
ingredientFiles[file.name] = file;
}
}
const { structure, assets, colors } = await createStructureFromCellPack(plugin, packing, params.baseUrl, ingredientFiles).runInContext(ctx);
await CellPackInfoProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, structure, {
info: { packingsCount: a.data.packings.length, packingIndex: params.packing, colors }
});
(cache as any).assets = assets;
return new PSO.Molecule.Structure(structure, { label: packing.name + '.' + packing.location });
});
},
dispose({ b, cache }) {
const assets = (cache as any).assets as Asset.Wrapper[];
if (assets) {
for (const a of assets) a.dispose();
}
if (b) {
b.data.customPropertyDescriptors.dispose();
for (const m of b.data.models) {
m.customProperties.dispose();
}
}
}
});
export { StructureFromAssemblies };
type StructureFromAssemblies = typeof StructureFromAssemblies
const StructureFromAssemblies = PluginStateTransform.BuiltIn({
name: 'Structure from all assemblies',
display: { name: 'Structure from all assemblies' },
from: PSO.Molecule.Model,
to: PSO.Molecule.Structure,
params: {
}
})({
canAutoUpdate({ newParams }) {
return true;
},
apply({ a, params }) {
return Task.create('Build Structure', async ctx => {
// TODO: optimze
// TODO: think of ways how to fast-track changes to this for animations
const model = a.data;
const initial_structure = Structure.ofModel(model);
const structures: Structure[] = [];
let structure: Structure = initial_structure;
// the list of asambly *?
const symmetry = ModelSymmetry.Provider.get(model);
if (symmetry && symmetry.assemblies.length !== 0) {
for (const a of symmetry.assemblies) {
const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
structures.push(s);
}
const builder = Structure.Builder({ label: 'Membrane' });
let offsetInvariantId = 0;
for (const s of structures) {
let maxInvariantId = 0;
for (const u of s.units) {
const invariantId = u.invariantId + offsetInvariantId;
if (u.invariantId > maxInvariantId) maxInvariantId = u.invariantId;
builder.addUnit(u.kind, u.model, u.conformation.operator, u.elements, Unit.Trait.None, invariantId);
}
offsetInvariantId += maxInvariantId + 1;
}
structure = builder.getStructure();
for (let i = 0, il = structure.models.length; i < il; ++i) {
Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
}
}
return new PSO.Molecule.Structure(structure, { label: a.label, description: `${a.description}` });
});
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});
const CreateTransformer = StateTransformer.builderFactory('cellPACK');
export const CreateCompartmentSphere = CreateTransformer({
name: 'create-compartment-sphere',
display: 'CompartmentSphere',
from: PSO.Root, // or whatever data source
to: PSO.Shape.Representation3D,
params: {
center: PD.Vec3(Vec3()),
radius: PD.Numeric(1),
label: PD.Text(`Compartment Sphere`)
}
})({
canAutoUpdate({ oldParams, newParams }) {
return true;
},
apply({ a, params }, plugin: PluginContext) {
return Task.create('Compartment Sphere', async ctx => {
const data = params;
const repr = MBRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => (MBParams));
await repr.createOrUpdate({ ...params, quality: 'custom', xrayShaded: true, doubleSided: true }, data).runInContext(ctx);
return new PSO.Shape.Representation3D({ repr, sourceData: a }, { label: data.label });
});
}
});

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) 2019-2021 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>
*/
import { CIF } from '../../mol-io/reader/cif';
import { parsePDB } from '../../mol-io/reader/pdb/parser';
import { AssetManager, Asset } from '../../mol-util/assets';
import { Structure } from '../../mol-model/structure';
import { Vec3 } from '../../mol-math/linear-algebra';
import { PluginContext } from '../../mol-plugin/context';
export async function parseCif(plugin: PluginContext, data: string | Uint8Array) {
const comp = CIF.parse(data);
const parsed = await plugin.runTask(comp);
if (parsed.isError) throw parsed;
return parsed.result;
}
export async function parsePDBfile(plugin: PluginContext, data: string, id: string) {
const comp = parsePDB(data, id);
const parsed = await plugin.runTask(comp);
if (parsed.isError) throw parsed;
return parsed.result;
}
async function downloadCif(plugin: PluginContext, url: string, isBinary: boolean, assetManager: AssetManager) {
const type = isBinary ? 'binary' : 'string';
const asset = await plugin.runTask(assetManager.resolve(Asset.getUrlAsset(assetManager, url), type));
return { cif: await parseCif(plugin, asset.data), asset };
}
async function downloadPDB(plugin: PluginContext, url: string, id: string, assetManager: AssetManager) {
const asset = await assetManager.resolve(Asset.getUrlAsset(assetManager, url), 'string').run();
return { pdb: await parsePDBfile(plugin, asset.data, id), asset };
}
export async function getFromPdb(plugin: PluginContext, pdbId: string, assetManager: AssetManager) {
const { cif, asset } = await downloadCif(plugin, `https://models.rcsb.org/${pdbId}.bcif`, true, assetManager);
return { mmcif: cif.blocks[0], asset };
}
export async function getFromOPM(plugin: PluginContext, pdbId: string, assetManager: AssetManager) {
const asset = await plugin.runTask(assetManager.resolve(Asset.getUrlAsset(assetManager, `https://opm-assets.storage.googleapis.com/pdb/${pdbId.toLowerCase()}.pdb`), 'string'));
return { pdb: await parsePDBfile(plugin, asset.data, pdbId), asset };
}
export async function getFromCellPackDB(plugin: PluginContext, id: string, baseUrl: string, assetManager: AssetManager) {
if (id.toLowerCase().endsWith('.cif') || id.toLowerCase().endsWith('.bcif')) {
const isBinary = id.toLowerCase().endsWith('.bcif');
const { cif, asset } = await downloadCif(plugin, `${baseUrl}/other/${id}`, isBinary, assetManager);
return { mmcif: cif.blocks[0], asset };
} else {
const name = id.endsWith('.pdb') ? id.substring(0, id.length - 4) : id;
return await downloadPDB(plugin, `${baseUrl}/other/${name}.pdb`, name, assetManager);
}
}
export type IngredientFiles = { [name: string]: Asset.File }
export function getStructureMean(structure: Structure) {
let xSum = 0, ySum = 0, zSum = 0;
for (let i = 0, il = structure.units.length; i < il; ++i) {
const unit = structure.units[i];
const { elements } = unit;
const { x, y, z } = unit.conformation;
for (let j = 0, jl = elements.length; j < jl; ++j) {
const eI = elements[j];
xSum += x(eI);
ySum += y(eI);
zSum += z(eI);
}
}
const { elementCount } = structure;
return Vec3.create(xSum / elementCount, ySum / elementCount, zSum / elementCount);
}
export function getFloatValue(value: DataView, offset: number) {
// if the last byte is a negative value (MSB is 1), the final
// float should be too
const negative = value.getInt8(offset + 2) >>> 31;
// this is how the bytes are arranged in the byte array/DataView
// buffer
const [b0, b1, b2, exponent] = [
// get first three bytes as unsigned since we only care
// about the last 8 bits of 32-bit js number returned by
// getUint8().
// Should be the same as: getInt8(offset) & -1 >>> 24
value.getUint8(offset),
value.getUint8(offset + 1),
value.getUint8(offset + 2),
// get the last byte, which is the exponent, as a signed int
// since it's already correct
value.getInt8(offset + 3)
];
let mantissa = b0 | (b1 << 8) | (b2 << 16);
if (negative) {
// need to set the most significant 8 bits to 1's since a js
// number is 32 bits but our mantissa is only 24.
mantissa |= 255 << 24;
}
return mantissa * Math.pow(10, exponent);
}

View File

@@ -1,59 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ConfalPyramidsPreset } from './confal-pyramids/behavior';
import { ConfalPyramidsColorThemeProvider } from './confal-pyramids/color';
import { ConfalPyramidsProvider } from './confal-pyramids/property';
import { ConfalPyramidsRepresentationProvider } from './confal-pyramids/representation';
import { NtCTubePreset } from './ntc-tube/behavior';
import { NtCTubeColorThemeProvider } from './ntc-tube/color';
import { NtCTubeProvider } from './ntc-tube/property';
import { NtCTubeRepresentationProvider } from './ntc-tube/representation';
export const DnatcoNtCs = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
name: 'dnatco-ntcs',
category: 'custom-props',
display: {
name: 'DNATCO NtC Annotations',
description: 'DNATCO NtC Annotations',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
register(): void {
this.ctx.customModelProperties.register(ConfalPyramidsProvider, this.params.autoAttach);
this.ctx.customModelProperties.register(NtCTubeProvider, this.params.autoAttach);
this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.add(NtCTubeColorThemeProvider);
this.ctx.representation.structure.registry.add(NtCTubeRepresentationProvider);
this.ctx.builders.structure.representation.registerPreset(ConfalPyramidsPreset);
this.ctx.builders.structure.representation.registerPreset(NtCTubePreset);
}
unregister() {
this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
this.ctx.customModelProperties.unregister(NtCTubeProvider.descriptor.name);
this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.remove(NtCTubeRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(NtCTubeColorThemeProvider);
this.ctx.builders.structure.representation.unregisterPreset(ConfalPyramidsPreset);
this.ctx.builders.structure.representation.unregisterPreset(NtCTubePreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
showToolTip: PD.Boolean(true)
})
});

View File

@@ -1,219 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { Color, ColorMap } from '../../mol-util/color';
export const DefaultNtCClassColors = {
A: 0xFFC1C1,
B: 0xC8CFFF,
BII: 0x0059DA,
miB: 0x3BE8FB,
Z: 0x01F60E,
IC: 0xFA5CFB,
OPN: 0xE90000,
SYN: 0xFFFF01,
N: 0xF2F2F2,
};
export const ErrorColor = Color(0xFFA10A);
export const NtCColors = ColorMap({
NANT_Upr: DefaultNtCClassColors.N,
NANT_Lwr: DefaultNtCClassColors.N,
AA00_Upr: DefaultNtCClassColors.A,
AA00_Lwr: DefaultNtCClassColors.A,
AA02_Upr: DefaultNtCClassColors.A,
AA02_Lwr: DefaultNtCClassColors.A,
AA03_Upr: DefaultNtCClassColors.A,
AA03_Lwr: DefaultNtCClassColors.A,
AA04_Upr: DefaultNtCClassColors.A,
AA04_Lwr: DefaultNtCClassColors.A,
AA08_Upr: DefaultNtCClassColors.A,
AA08_Lwr: DefaultNtCClassColors.A,
AA09_Upr: DefaultNtCClassColors.A,
AA09_Lwr: DefaultNtCClassColors.A,
AA01_Upr: DefaultNtCClassColors.A,
AA01_Lwr: DefaultNtCClassColors.A,
AA05_Upr: DefaultNtCClassColors.A,
AA05_Lwr: DefaultNtCClassColors.A,
AA06_Upr: DefaultNtCClassColors.A,
AA06_Lwr: DefaultNtCClassColors.A,
AA10_Upr: DefaultNtCClassColors.A,
AA10_Lwr: DefaultNtCClassColors.A,
AA11_Upr: DefaultNtCClassColors.A,
AA11_Lwr: DefaultNtCClassColors.A,
AA07_Upr: DefaultNtCClassColors.A,
AA07_Lwr: DefaultNtCClassColors.A,
AA12_Upr: DefaultNtCClassColors.A,
AA12_Lwr: DefaultNtCClassColors.A,
AA13_Upr: DefaultNtCClassColors.A,
AA13_Lwr: DefaultNtCClassColors.A,
AB01_Upr: DefaultNtCClassColors.A,
AB01_Lwr: DefaultNtCClassColors.B,
AB02_Upr: DefaultNtCClassColors.A,
AB02_Lwr: DefaultNtCClassColors.B,
AB03_Upr: DefaultNtCClassColors.A,
AB03_Lwr: DefaultNtCClassColors.B,
AB04_Upr: DefaultNtCClassColors.A,
AB04_Lwr: DefaultNtCClassColors.B,
AB05_Upr: DefaultNtCClassColors.A,
AB05_Lwr: DefaultNtCClassColors.B,
BA01_Upr: DefaultNtCClassColors.B,
BA01_Lwr: DefaultNtCClassColors.A,
BA05_Upr: DefaultNtCClassColors.B,
BA05_Lwr: DefaultNtCClassColors.A,
BA09_Upr: DefaultNtCClassColors.B,
BA09_Lwr: DefaultNtCClassColors.A,
BA08_Upr: DefaultNtCClassColors.BII,
BA08_Lwr: DefaultNtCClassColors.A,
BA10_Upr: DefaultNtCClassColors.B,
BA10_Lwr: DefaultNtCClassColors.A,
BA13_Upr: DefaultNtCClassColors.BII,
BA13_Lwr: DefaultNtCClassColors.A,
BA16_Upr: DefaultNtCClassColors.BII,
BA16_Lwr: DefaultNtCClassColors.A,
BA17_Upr: DefaultNtCClassColors.BII,
BA17_Lwr: DefaultNtCClassColors.A,
BB00_Upr: DefaultNtCClassColors.B,
BB00_Lwr: DefaultNtCClassColors.B,
BB01_Upr: DefaultNtCClassColors.B,
BB01_Lwr: DefaultNtCClassColors.B,
BB17_Upr: DefaultNtCClassColors.B,
BB17_Lwr: DefaultNtCClassColors.B,
BB02_Upr: DefaultNtCClassColors.B,
BB02_Lwr: DefaultNtCClassColors.B,
BB03_Upr: DefaultNtCClassColors.B,
BB03_Lwr: DefaultNtCClassColors.B,
BB11_Upr: DefaultNtCClassColors.B,
BB11_Lwr: DefaultNtCClassColors.B,
BB16_Upr: DefaultNtCClassColors.B,
BB16_Lwr: DefaultNtCClassColors.B,
BB04_Upr: DefaultNtCClassColors.B,
BB04_Lwr: DefaultNtCClassColors.BII,
BB05_Upr: DefaultNtCClassColors.B,
BB05_Lwr: DefaultNtCClassColors.BII,
BB07_Upr: DefaultNtCClassColors.BII,
BB07_Lwr: DefaultNtCClassColors.BII,
BB08_Upr: DefaultNtCClassColors.BII,
BB08_Lwr: DefaultNtCClassColors.BII,
BB10_Upr: DefaultNtCClassColors.miB,
BB10_Lwr: DefaultNtCClassColors.miB,
BB12_Upr: DefaultNtCClassColors.miB,
BB12_Lwr: DefaultNtCClassColors.miB,
BB13_Upr: DefaultNtCClassColors.miB,
BB13_Lwr: DefaultNtCClassColors.miB,
BB14_Upr: DefaultNtCClassColors.miB,
BB14_Lwr: DefaultNtCClassColors.miB,
BB15_Upr: DefaultNtCClassColors.miB,
BB15_Lwr: DefaultNtCClassColors.miB,
BB20_Upr: DefaultNtCClassColors.miB,
BB20_Lwr: DefaultNtCClassColors.miB,
IC01_Upr: DefaultNtCClassColors.IC,
IC01_Lwr: DefaultNtCClassColors.IC,
IC02_Upr: DefaultNtCClassColors.IC,
IC02_Lwr: DefaultNtCClassColors.IC,
IC03_Upr: DefaultNtCClassColors.IC,
IC03_Lwr: DefaultNtCClassColors.IC,
IC04_Upr: DefaultNtCClassColors.IC,
IC04_Lwr: DefaultNtCClassColors.IC,
IC05_Upr: DefaultNtCClassColors.IC,
IC05_Lwr: DefaultNtCClassColors.IC,
IC06_Upr: DefaultNtCClassColors.IC,
IC06_Lwr: DefaultNtCClassColors.IC,
IC07_Upr: DefaultNtCClassColors.IC,
IC07_Lwr: DefaultNtCClassColors.IC,
OP01_Upr: DefaultNtCClassColors.OPN,
OP01_Lwr: DefaultNtCClassColors.OPN,
OP02_Upr: DefaultNtCClassColors.OPN,
OP02_Lwr: DefaultNtCClassColors.OPN,
OP03_Upr: DefaultNtCClassColors.OPN,
OP03_Lwr: DefaultNtCClassColors.OPN,
OP04_Upr: DefaultNtCClassColors.OPN,
OP04_Lwr: DefaultNtCClassColors.OPN,
OP05_Upr: DefaultNtCClassColors.OPN,
OP05_Lwr: DefaultNtCClassColors.OPN,
OP06_Upr: DefaultNtCClassColors.OPN,
OP06_Lwr: DefaultNtCClassColors.OPN,
OP07_Upr: DefaultNtCClassColors.OPN,
OP07_Lwr: DefaultNtCClassColors.OPN,
OP08_Upr: DefaultNtCClassColors.OPN,
OP08_Lwr: DefaultNtCClassColors.OPN,
OP09_Upr: DefaultNtCClassColors.OPN,
OP09_Lwr: DefaultNtCClassColors.OPN,
OP10_Upr: DefaultNtCClassColors.OPN,
OP10_Lwr: DefaultNtCClassColors.OPN,
OP11_Upr: DefaultNtCClassColors.OPN,
OP11_Lwr: DefaultNtCClassColors.OPN,
OP12_Upr: DefaultNtCClassColors.OPN,
OP12_Lwr: DefaultNtCClassColors.OPN,
OP13_Upr: DefaultNtCClassColors.OPN,
OP13_Lwr: DefaultNtCClassColors.OPN,
OP14_Upr: DefaultNtCClassColors.OPN,
OP14_Lwr: DefaultNtCClassColors.OPN,
OP15_Upr: DefaultNtCClassColors.OPN,
OP15_Lwr: DefaultNtCClassColors.OPN,
OP16_Upr: DefaultNtCClassColors.OPN,
OP16_Lwr: DefaultNtCClassColors.OPN,
OP17_Upr: DefaultNtCClassColors.OPN,
OP17_Lwr: DefaultNtCClassColors.OPN,
OP18_Upr: DefaultNtCClassColors.OPN,
OP18_Lwr: DefaultNtCClassColors.OPN,
OP19_Upr: DefaultNtCClassColors.OPN,
OP19_Lwr: DefaultNtCClassColors.OPN,
OP20_Upr: DefaultNtCClassColors.OPN,
OP20_Lwr: DefaultNtCClassColors.OPN,
OP21_Upr: DefaultNtCClassColors.OPN,
OP21_Lwr: DefaultNtCClassColors.OPN,
OP22_Upr: DefaultNtCClassColors.OPN,
OP22_Lwr: DefaultNtCClassColors.OPN,
OP23_Upr: DefaultNtCClassColors.OPN,
OP23_Lwr: DefaultNtCClassColors.OPN,
OP24_Upr: DefaultNtCClassColors.OPN,
OP24_Lwr: DefaultNtCClassColors.OPN,
OP25_Upr: DefaultNtCClassColors.OPN,
OP25_Lwr: DefaultNtCClassColors.OPN,
OP26_Upr: DefaultNtCClassColors.OPN,
OP26_Lwr: DefaultNtCClassColors.OPN,
OP27_Upr: DefaultNtCClassColors.OPN,
OP27_Lwr: DefaultNtCClassColors.OPN,
OP28_Upr: DefaultNtCClassColors.OPN,
OP28_Lwr: DefaultNtCClassColors.OPN,
OP29_Upr: DefaultNtCClassColors.OPN,
OP29_Lwr: DefaultNtCClassColors.OPN,
OP30_Upr: DefaultNtCClassColors.OPN,
OP30_Lwr: DefaultNtCClassColors.OPN,
OP31_Upr: DefaultNtCClassColors.OPN,
OP31_Lwr: DefaultNtCClassColors.OPN,
OPS1_Upr: DefaultNtCClassColors.OPN,
OPS1_Lwr: DefaultNtCClassColors.OPN,
OP1S_Upr: DefaultNtCClassColors.OPN,
OP1S_Lwr: DefaultNtCClassColors.SYN,
AAS1_Upr: DefaultNtCClassColors.SYN,
AAS1_Lwr: DefaultNtCClassColors.A,
AB1S_Upr: DefaultNtCClassColors.A,
AB1S_Lwr: DefaultNtCClassColors.SYN,
AB2S_Upr: DefaultNtCClassColors.A,
AB2S_Lwr: DefaultNtCClassColors.SYN,
BB1S_Upr: DefaultNtCClassColors.B,
BB1S_Lwr: DefaultNtCClassColors.SYN,
BB2S_Upr: DefaultNtCClassColors.B,
BB2S_Lwr: DefaultNtCClassColors.SYN,
BBS1_Upr: DefaultNtCClassColors.SYN,
BBS1_Lwr: DefaultNtCClassColors.B,
ZZ01_Upr: DefaultNtCClassColors.Z,
ZZ01_Lwr: DefaultNtCClassColors.Z,
ZZ02_Upr: DefaultNtCClassColors.Z,
ZZ02_Lwr: DefaultNtCClassColors.Z,
ZZ1S_Upr: DefaultNtCClassColors.Z,
ZZ1S_Lwr: DefaultNtCClassColors.SYN,
ZZ2S_Upr: DefaultNtCClassColors.Z,
ZZ2S_Lwr: DefaultNtCClassColors.SYN,
ZZS1_Upr: DefaultNtCClassColors.SYN,
ZZS1_Lwr: DefaultNtCClassColors.Z,
ZZS2_Upr: DefaultNtCClassColors.SYN,
ZZS2_Lwr: DefaultNtCClassColors.Z,
});

View File

@@ -6,22 +6,23 @@
*/
import { ConfalPyramidsColorThemeProvider } from './color';
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsRepresentationProvider } from './representation';
import { Dnatco } from '../property';
import { DnatcoTypes } from '../types';
import { ConfalPyramidsTypes } from './types';
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-confal-pyramids',
display: {
name: 'Confal Pyramids', group: 'Annotation',
description: 'Schematic depiction of conformer class and confal value.',
},
isApplicable(a) {
return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
return a.data.models.length >= 1 && a.data.models.some(m => ConfalPyramids.isApplicable(m));
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
@@ -47,7 +48,50 @@ export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
}
});
export function confalPyramidLabel(step: DnatcoTypes.Step) {
export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
name: 'dnatco-confal-pyramids-prop',
category: 'custom-props',
display: {
name: 'Confal Pyramids',
description: 'Schematic depiction of conformer class and confal value.',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
private provider = ConfalPyramidsProvider;
register(): void {
this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
this.ctx.builders.structure.representation.registerPreset(DnatcoConfalPyramidsPreset);
}
update(p: { autoAttach: boolean, showToolTip: boolean }) {
const updated = this.params.autoAttach !== p.autoAttach;
this.params.autoAttach = p.autoAttach;
this.params.showToolTip = p.showToolTip;
this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
return updated;
}
unregister() {
this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
this.ctx.builders.structure.representation.unregisterPreset(DnatcoConfalPyramidsPreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
showToolTip: PD.Boolean(true)
})
});
export function confalPyramidLabel(halfPyramid: ConfalPyramidsTypes.HalfPyramid) {
const { step } = halfPyramid;
return `
<b>${step.auth_asym_id_1}</b> |
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}

View File

@@ -5,10 +5,8 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ErrorColor, NtCColors } from '../color';
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsTypes as CPT } from './types';
import { Dnatco } from '../property';
import { Location } from '../../../mol-model/location';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorTheme } from '../../../mol-theme/color';
@@ -21,7 +19,215 @@ import { ObjectKeys } from '../../../mol-util/type-helpers';
const Description = 'Assigns colors to confal pyramids';
const PyramidsColors = ColorMap({ ...NtCColors });
const DefaultClassColors = {
A: 0xFFC1C1,
B: 0xC8CFFF,
BII: 0x0059DA,
miB: 0x3BE8FB,
Z: 0x01F60E,
IC: 0xFA5CFB,
OPN: 0xE90000,
SYN: 0xFFFF01,
N: 0xF2F2F2,
};
const ErrorColor = Color(0xFFA10A);
const PyramidsColors = ColorMap({
NANT_Upr: DefaultClassColors.N,
NANT_Lwr: DefaultClassColors.N,
AA00_Upr: DefaultClassColors.A,
AA00_Lwr: DefaultClassColors.A,
AA02_Upr: DefaultClassColors.A,
AA02_Lwr: DefaultClassColors.A,
AA03_Upr: DefaultClassColors.A,
AA03_Lwr: DefaultClassColors.A,
AA04_Upr: DefaultClassColors.A,
AA04_Lwr: DefaultClassColors.A,
AA08_Upr: DefaultClassColors.A,
AA08_Lwr: DefaultClassColors.A,
AA09_Upr: DefaultClassColors.A,
AA09_Lwr: DefaultClassColors.A,
AA01_Upr: DefaultClassColors.A,
AA01_Lwr: DefaultClassColors.A,
AA05_Upr: DefaultClassColors.A,
AA05_Lwr: DefaultClassColors.A,
AA06_Upr: DefaultClassColors.A,
AA06_Lwr: DefaultClassColors.A,
AA10_Upr: DefaultClassColors.A,
AA10_Lwr: DefaultClassColors.A,
AA11_Upr: DefaultClassColors.A,
AA11_Lwr: DefaultClassColors.A,
AA07_Upr: DefaultClassColors.A,
AA07_Lwr: DefaultClassColors.A,
AA12_Upr: DefaultClassColors.A,
AA12_Lwr: DefaultClassColors.A,
AA13_Upr: DefaultClassColors.A,
AA13_Lwr: DefaultClassColors.A,
AB01_Upr: DefaultClassColors.A,
AB01_Lwr: DefaultClassColors.B,
AB02_Upr: DefaultClassColors.A,
AB02_Lwr: DefaultClassColors.B,
AB03_Upr: DefaultClassColors.A,
AB03_Lwr: DefaultClassColors.B,
AB04_Upr: DefaultClassColors.A,
AB04_Lwr: DefaultClassColors.B,
AB05_Upr: DefaultClassColors.A,
AB05_Lwr: DefaultClassColors.B,
BA01_Upr: DefaultClassColors.B,
BA01_Lwr: DefaultClassColors.A,
BA05_Upr: DefaultClassColors.B,
BA05_Lwr: DefaultClassColors.A,
BA09_Upr: DefaultClassColors.B,
BA09_Lwr: DefaultClassColors.A,
BA08_Upr: DefaultClassColors.BII,
BA08_Lwr: DefaultClassColors.A,
BA10_Upr: DefaultClassColors.B,
BA10_Lwr: DefaultClassColors.A,
BA13_Upr: DefaultClassColors.BII,
BA13_Lwr: DefaultClassColors.A,
BA16_Upr: DefaultClassColors.BII,
BA16_Lwr: DefaultClassColors.A,
BA17_Upr: DefaultClassColors.BII,
BA17_Lwr: DefaultClassColors.A,
BB00_Upr: DefaultClassColors.B,
BB00_Lwr: DefaultClassColors.B,
BB01_Upr: DefaultClassColors.B,
BB01_Lwr: DefaultClassColors.B,
BB17_Upr: DefaultClassColors.B,
BB17_Lwr: DefaultClassColors.B,
BB02_Upr: DefaultClassColors.B,
BB02_Lwr: DefaultClassColors.B,
BB03_Upr: DefaultClassColors.B,
BB03_Lwr: DefaultClassColors.B,
BB11_Upr: DefaultClassColors.B,
BB11_Lwr: DefaultClassColors.B,
BB16_Upr: DefaultClassColors.B,
BB16_Lwr: DefaultClassColors.B,
BB04_Upr: DefaultClassColors.B,
BB04_Lwr: DefaultClassColors.BII,
BB05_Upr: DefaultClassColors.B,
BB05_Lwr: DefaultClassColors.BII,
BB07_Upr: DefaultClassColors.BII,
BB07_Lwr: DefaultClassColors.BII,
BB08_Upr: DefaultClassColors.BII,
BB08_Lwr: DefaultClassColors.BII,
BB10_Upr: DefaultClassColors.miB,
BB10_Lwr: DefaultClassColors.miB,
BB12_Upr: DefaultClassColors.miB,
BB12_Lwr: DefaultClassColors.miB,
BB13_Upr: DefaultClassColors.miB,
BB13_Lwr: DefaultClassColors.miB,
BB14_Upr: DefaultClassColors.miB,
BB14_Lwr: DefaultClassColors.miB,
BB15_Upr: DefaultClassColors.miB,
BB15_Lwr: DefaultClassColors.miB,
BB20_Upr: DefaultClassColors.miB,
BB20_Lwr: DefaultClassColors.miB,
IC01_Upr: DefaultClassColors.IC,
IC01_Lwr: DefaultClassColors.IC,
IC02_Upr: DefaultClassColors.IC,
IC02_Lwr: DefaultClassColors.IC,
IC03_Upr: DefaultClassColors.IC,
IC03_Lwr: DefaultClassColors.IC,
IC04_Upr: DefaultClassColors.IC,
IC04_Lwr: DefaultClassColors.IC,
IC05_Upr: DefaultClassColors.IC,
IC05_Lwr: DefaultClassColors.IC,
IC06_Upr: DefaultClassColors.IC,
IC06_Lwr: DefaultClassColors.IC,
IC07_Upr: DefaultClassColors.IC,
IC07_Lwr: DefaultClassColors.IC,
OP01_Upr: DefaultClassColors.OPN,
OP01_Lwr: DefaultClassColors.OPN,
OP02_Upr: DefaultClassColors.OPN,
OP02_Lwr: DefaultClassColors.OPN,
OP03_Upr: DefaultClassColors.OPN,
OP03_Lwr: DefaultClassColors.OPN,
OP04_Upr: DefaultClassColors.OPN,
OP04_Lwr: DefaultClassColors.OPN,
OP05_Upr: DefaultClassColors.OPN,
OP05_Lwr: DefaultClassColors.OPN,
OP06_Upr: DefaultClassColors.OPN,
OP06_Lwr: DefaultClassColors.OPN,
OP07_Upr: DefaultClassColors.OPN,
OP07_Lwr: DefaultClassColors.OPN,
OP08_Upr: DefaultClassColors.OPN,
OP08_Lwr: DefaultClassColors.OPN,
OP09_Upr: DefaultClassColors.OPN,
OP09_Lwr: DefaultClassColors.OPN,
OP10_Upr: DefaultClassColors.OPN,
OP10_Lwr: DefaultClassColors.OPN,
OP11_Upr: DefaultClassColors.OPN,
OP11_Lwr: DefaultClassColors.OPN,
OP12_Upr: DefaultClassColors.OPN,
OP12_Lwr: DefaultClassColors.OPN,
OP13_Upr: DefaultClassColors.OPN,
OP13_Lwr: DefaultClassColors.OPN,
OP14_Upr: DefaultClassColors.OPN,
OP14_Lwr: DefaultClassColors.OPN,
OP15_Upr: DefaultClassColors.OPN,
OP15_Lwr: DefaultClassColors.OPN,
OP16_Upr: DefaultClassColors.OPN,
OP16_Lwr: DefaultClassColors.OPN,
OP17_Upr: DefaultClassColors.OPN,
OP17_Lwr: DefaultClassColors.OPN,
OP18_Upr: DefaultClassColors.OPN,
OP18_Lwr: DefaultClassColors.OPN,
OP19_Upr: DefaultClassColors.OPN,
OP19_Lwr: DefaultClassColors.OPN,
OP20_Upr: DefaultClassColors.OPN,
OP20_Lwr: DefaultClassColors.OPN,
OP21_Upr: DefaultClassColors.OPN,
OP21_Lwr: DefaultClassColors.OPN,
OP22_Upr: DefaultClassColors.OPN,
OP22_Lwr: DefaultClassColors.OPN,
OP23_Upr: DefaultClassColors.OPN,
OP23_Lwr: DefaultClassColors.OPN,
OP24_Upr: DefaultClassColors.OPN,
OP24_Lwr: DefaultClassColors.OPN,
OP25_Upr: DefaultClassColors.OPN,
OP25_Lwr: DefaultClassColors.OPN,
OP26_Upr: DefaultClassColors.OPN,
OP26_Lwr: DefaultClassColors.OPN,
OP27_Upr: DefaultClassColors.OPN,
OP27_Lwr: DefaultClassColors.OPN,
OP28_Upr: DefaultClassColors.OPN,
OP28_Lwr: DefaultClassColors.OPN,
OP29_Upr: DefaultClassColors.OPN,
OP29_Lwr: DefaultClassColors.OPN,
OP30_Upr: DefaultClassColors.OPN,
OP30_Lwr: DefaultClassColors.OPN,
OP31_Upr: DefaultClassColors.OPN,
OP31_Lwr: DefaultClassColors.OPN,
OPS1_Upr: DefaultClassColors.OPN,
OPS1_Lwr: DefaultClassColors.OPN,
OP1S_Upr: DefaultClassColors.OPN,
OP1S_Lwr: DefaultClassColors.SYN,
AAS1_Upr: DefaultClassColors.SYN,
AAS1_Lwr: DefaultClassColors.A,
AB1S_Upr: DefaultClassColors.A,
AB1S_Lwr: DefaultClassColors.SYN,
AB2S_Upr: DefaultClassColors.A,
AB2S_Lwr: DefaultClassColors.SYN,
BB1S_Upr: DefaultClassColors.B,
BB1S_Lwr: DefaultClassColors.SYN,
BB2S_Upr: DefaultClassColors.B,
BB2S_Lwr: DefaultClassColors.SYN,
BBS1_Upr: DefaultClassColors.SYN,
BBS1_Lwr: DefaultClassColors.B,
ZZ01_Upr: DefaultClassColors.Z,
ZZ01_Lwr: DefaultClassColors.Z,
ZZ02_Upr: DefaultClassColors.Z,
ZZ02_Lwr: DefaultClassColors.Z,
ZZ1S_Upr: DefaultClassColors.Z,
ZZ1S_Lwr: DefaultClassColors.SYN,
ZZ2S_Upr: DefaultClassColors.Z,
ZZ2S_Lwr: DefaultClassColors.SYN,
ZZS1_Upr: DefaultClassColors.SYN,
ZZS1_Lwr: DefaultClassColors.Z,
ZZS2_Upr: DefaultClassColors.SYN,
ZZS2_Lwr: DefaultClassColors.Z,
});
type PyramidsColors = typeof PyramidsColors;
export const ConfalPyramidsColorThemeParams = {
@@ -66,7 +272,7 @@ export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramid
factory: ConfalPyramidsColorTheme,
getParams: getConfalPyramidsColorThemeParams,
defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && ConfalPyramidsProvider.ref(data.structure.models[0], false)

View File

@@ -5,18 +5,90 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { Dnatco, DnatcoParams, DnatcoSteps } from '../property';
import { ConfalPyramidsTypes as CPT } from './types';
import { Column, Table } from '../../../mol-data/db';
import { toTable } from '../../../mol-io/reader/cif/schema';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Model } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
export const ConfalPyramidsParams = { ...DnatcoParams };
export type ConfalPyramids = PropertyWrapper<CPT.Steps | undefined>;
export namespace ConfalPyramids {
export const Schema = {
ndb_struct_ntc_step: {
id: Column.Schema.int,
name: Column.Schema.str,
PDB_model_number: Column.Schema.int,
label_entity_id_1: Column.Schema.int,
label_asym_id_1: Column.Schema.str,
label_seq_id_1: Column.Schema.int,
label_comp_id_1: Column.Schema.str,
label_alt_id_1: Column.Schema.str,
label_entity_id_2: Column.Schema.int,
label_asym_id_2: Column.Schema.str,
label_seq_id_2: Column.Schema.int,
label_comp_id_2: Column.Schema.str,
label_alt_id_2: Column.Schema.str,
auth_asym_id_1: Column.Schema.str,
auth_seq_id_1: Column.Schema.int,
auth_asym_id_2: Column.Schema.str,
auth_seq_id_2: Column.Schema.int,
PDB_ins_code_1: Column.Schema.str,
PDB_ins_code_2: Column.Schema.str,
},
ndb_struct_ntc_step_summary: {
step_id: Column.Schema.int,
assigned_CANA: Column.Schema.str,
assigned_NtC: Column.Schema.str,
confal_score: Column.Schema.int,
euclidean_distance_NtC_ideal: Column.Schema.float,
cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
closest_CANA: Column.Schema.str,
closest_NtC: Column.Schema.str,
closest_step_golden: Column.Schema.str
}
};
export type Schema = typeof Schema;
export async function fromCif(ctx: CustomProperty.Context, model: Model, props: ConfalPyramidsProps): Promise<CustomProperty.Data<ConfalPyramids>> {
const info = PropertyWrapper.createInfo();
const data = getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const fromCif = createPyramidsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: fromCif } };
}
function getCifData(model: Model) {
if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
if (!hasNdbStructNtcCategories(model)) return undefined;
return {
steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
};
}
function hasNdbStructNtcCategories(model: Model): boolean {
if (!MmcifFormat.is(model.sourceData)) return false;
const names = (model.sourceData).data.frame.categoryNames;
return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
}
export function isApplicable(model?: Model): boolean {
return !!model && hasNdbStructNtcCategories(model);
}
}
export const ConfalPyramidsParams = {};
export type ConfalPyramidsParams = typeof ConfalPyramidsParams;
export type ConfalPyramidsProps = PD.Values<ConfalPyramidsParams>;
export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, DnatcoSteps> = CustomModelProperty.createProvider({
export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, ConfalPyramids> = CustomModelProperty.createProvider({
label: 'Confal Pyramids',
descriptor: CustomPropertyDescriptor({
name: 'confal_pyramids',
@@ -24,9 +96,94 @@ export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramids
type: 'static',
defaultParams: ConfalPyramidsParams,
getParams: (data: Model) => ConfalPyramidsParams,
isApplicable: (data: Model) => Dnatco.isApplicable(data),
isApplicable: (data: Model) => ConfalPyramids.isApplicable(data),
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ConfalPyramidsProps>) => {
const p = { ...PD.getDefaultValues(ConfalPyramidsParams), ...props };
return Dnatco.fromCif(ctx, data, p);
return ConfalPyramids.fromCif(ctx, data, p);
}
});
type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
function createPyramidsFromCif(
model: Model,
cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
stepsSummary: StepsSummaryTable
): CPT.Steps {
const steps = new Array<CPT.Step>();
const mapping = new Array<CPT.MappedChains>();
const {
id, PDB_model_number, name,
auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
_rowCount
} = cifSteps;
if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
for (let i = 0; i < _rowCount; i++) {
const {
NtC,
confal_score,
rmsd
} = getSummaryData(id.value(i), i, stepsSummary);
const modelNum = PDB_model_number.value(i);
const chainId = auth_asym_id_1.value(i);
const seqId = auth_seq_id_1.value(i);
const modelIdx = modelNum - 1;
if (mapping.length <= modelIdx || !mapping[modelIdx])
mapping[modelIdx] = new Map<string, CPT.MappedResidues>();
const step = {
PDB_model_number: modelNum,
name: name.value(i),
auth_asym_id_1: chainId,
auth_seq_id_1: seqId,
label_comp_id_1: label_comp_id_1.value(i),
label_alt_id_1: label_alt_id_1.value(i),
PDB_ins_code_1: PDB_ins_code_1.value(i),
auth_asym_id_2: auth_asym_id_2.value(i),
auth_seq_id_2: auth_seq_id_2.value(i),
label_comp_id_2: label_comp_id_2.value(i),
label_alt_id_2: label_alt_id_2.value(i),
PDB_ins_code_2: PDB_ins_code_2.value(i),
confal_score,
NtC,
rmsd,
};
steps.push(step);
const mappedChains = mapping[modelIdx];
const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
const stepsForResidue = residuesOnChain.get(seqId) ?? [];
stepsForResidue.push(steps.length - 1);
residuesOnChain.set(seqId, stepsForResidue);
mappedChains.set(chainId, residuesOnChain);
mapping[modelIdx] = mappedChains;
}
return { steps, mapping };
}
function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
const {
step_id,
confal_score,
assigned_NtC,
cartesian_rmsd_closest_NtC_representative,
} = stepsSummary;
// Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
for (let j = i; j < stepsSummary._rowCount; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
// Safety net for cases where the previous assumption is not met
for (let j = 0; j < i; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
throw new Error('Inconsistent mmCIF data');
}

View File

@@ -5,10 +5,9 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsIterator } from './util';
import { ConfalPyramidsTypes as CPT } from './types';
import { Dnatco } from '../property';
import { Interval } from '../../../mol-data/int';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
@@ -88,8 +87,6 @@ function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Str
const it = new ConfalPyramidsIterator(structure, unit);
while (it.hasNext) {
const allPoints = it.move();
if (!allPoints)
continue;
for (const points of allPoints) {
const { O3, P, OP1, OP2, O5, confalScore } = points;
@@ -153,7 +150,9 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro
if (halfPyramidsCount <= groupId) return EmptyLoci;
const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
return CPT.Loci(data.steps, [idx]);
const step = data.steps[idx];
return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]);
}
function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
@@ -198,7 +197,7 @@ export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvi
defaultValues: PD.getDefaultValues(ConfalPyramidsParams),
defaultColorTheme: { name: 'confal-pyramids' },
defaultSizeTheme: { name: 'uniform' },
isApplicable: (structure: Structure) => structure.models.some(m => Dnatco.isApplicable(m)),
isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true),
detach: (data) => ConfalPyramidsProvider.ref(data.model, false),

View File

@@ -5,29 +5,61 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from '../types';
import { DataLocation } from '../../../mol-model/location';
import { DataLoci } from '../../../mol-model/loci';
import { confalPyramidLabel } from './behavior';
export namespace ConfalPyramidsTypes {
export interface Location extends DataLocation<DnatcoTypes.HalfStep, {}> {}
export const DataTag = 'dnatco-confal-half-pyramid';
export function Location(step: DnatcoTypes.Step, isLower: boolean) {
return DataLocation(DnatcoTypes.DataTag, { step, isLower }, {});
export type Step = {
PDB_model_number: number,
name: string,
auth_asym_id_1: string,
auth_seq_id_1: number,
label_comp_id_1: string,
label_alt_id_1: string,
PDB_ins_code_1: string,
auth_asym_id_2: string,
auth_seq_id_2: number,
label_comp_id_2: string,
label_alt_id_2: string,
PDB_ins_code_2: string,
confal_score: number,
NtC: string,
rmsd: number,
}
export type MappedChains = Map<string, MappedResidues>;
export type MappedResidues = Map<number, number[]>;
export interface Steps {
steps: Array<Step>,
mapping: MappedChains[],
}
export interface HalfPyramid {
step: Step,
isLower: boolean,
}
export interface Location extends DataLocation<HalfPyramid, {}> {}
export function Location(step: Step, isLower: boolean) {
return DataLocation(DataTag, { step, isLower }, {});
}
export function isLocation(x: any): x is Location {
return !!x && x.kind === 'data-location' && x.tag === DnatcoTypes.DataTag;
return !!x && x.kind === 'data-location' && x.tag === DataTag;
}
export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
export interface Loci extends DataLoci<HalfPyramid, {}> {}
export function Loci(data: DnatcoTypes.Step[], elements: ReadonlyArray<number>): Loci {
return DataLoci(DnatcoTypes.DataTag, data, elements, undefined, () => elements[0] !== undefined ? confalPyramidLabel(data[elements[0]]) : '');
export function Loci(data: HalfPyramid, elements: ReadonlyArray<{}>): Loci {
return DataLoci(DataTag, data, elements, undefined, () => confalPyramidLabel(data));
}
export function isLoci(x: any): x is Loci {
return !!x && x.kind === 'data-loci' && x.tag === DnatcoTypes.DataTag;
return !!x && x.kind === 'data-loci' && x.tag === DataTag;
}
}

View File

@@ -1,15 +1,16 @@
/**
* Copyright (c) 2018-2023 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 Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramidsProvider } from './property';
import { DnatcoTypes } from '../types';
import { DnatcoUtil } from '../util';
import { ConfalPyramidsTypes as CPT } from './types';
import { Segmentation } from '../../../mol-data/int';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
type Residue = Segmentation.Segment<ResidueIndex>;
export type Pyramid = {
O3: ElementIndex,
@@ -21,17 +22,31 @@ export type Pyramid = {
stepIdx: number,
};
function getPyramid(
loc: StructureElement.Location,
one: DnatcoUtil.Residue, two: DnatcoUtil.Residue,
altIdOne: string, altIdTwo: string,
insCodeOne: string, insCodeTwo: string,
confalScore: number, stepIdx: number): Pyramid {
const O3 = DnatcoUtil.getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne, insCodeOne);
const P = DnatcoUtil.getAtomIndex(loc, two, ['P'], altIdTwo, insCodeTwo);
const OP1 = DnatcoUtil.getAtomIndex(loc, two, ['OP1'], altIdTwo, insCodeTwo);
const OP2 = DnatcoUtil.getAtomIndex(loc, two, ['OP2'], altIdTwo, insCodeTwo);
const O5 = DnatcoUtil.getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo, insCodeTwo);
const EmptyStepIndices = new Array<number>();
function copyResidue(r?: Residue) {
return r ? { index: r.index, start: r.start, end: r.end } : void 0;
}
function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex {
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = loc.unit.elements[eI];
const elName = StructureProperties.atom.label_atom_id(loc);
const elAltId = StructureProperties.atom.label_alt_id(loc);
if (names.includes(elName) && (elAltId === altId || elAltId.length === 0))
return loc.element;
}
return -1 as ElementIndex;
}
function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid {
const O3 = getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne);
const P = getAtomIndex(loc, two, ['P'], altIdTwo);
const OP1 = getAtomIndex(loc, two, ['OP1'], altIdTwo);
const OP2 = getAtomIndex(loc, two, ['OP2'], altIdTwo);
const O5 = getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo);
return { O3, P, OP1, OP2, O5, confalScore, stepIdx };
}
@@ -39,29 +54,39 @@ function getPyramid(
export class ConfalPyramidsIterator {
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
private residueOne?: DnatcoUtil.Residue;
private residueTwo: DnatcoUtil.Residue;
private data?: DnatcoTypes.Steps;
private residueOne?: Residue;
private residueTwo: Residue;
private data?: CPT.Steps;
private loc: StructureElement.Location;
private moveStep() {
this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
private getStepIndices(r: Residue) {
this.loc.element = this.loc.unit.elements[r.start];
// Check for discontinuity
if (this.residueTwo.index !== (this.residueOne!.index + 1))
return void 0;
const modelIdx = StructureProperties.unit.model_num(this.loc) - 1;
const chainId = StructureProperties.chain.auth_asym_id(this.loc);
const seqId = StructureProperties.residue.auth_seq_id(this.loc);
const chains = this.data!.mapping[modelIdx];
if (!chains) return EmptyStepIndices;
const residues = chains.get(chainId);
if (!residues) return EmptyStepIndices;
return residues.get(seqId) ?? EmptyStepIndices;
}
private moveStep() {
this.residueOne = copyResidue(this.residueTwo);
this.residueTwo = copyResidue(this.residueIt.move())!;
return this.toPyramids(this.residueOne!, this.residueTwo);
}
private toPyramids(one: DnatcoUtil.Residue, two: DnatcoUtil.Residue) {
const indices = DnatcoUtil.getStepIndices(this.data!, this.loc, one);
private toPyramids(one: Residue, two: Residue) {
const indices = this.getStepIndices(one);
const points = [];
for (const idx of indices) {
const step = this.data!.steps[idx];
points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.PDB_ins_code_1, step.PDB_ins_code_2, step.confal_score, idx));
points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx));
}
return points;
@@ -96,8 +121,6 @@ export class ConfalPyramidsIterator {
return this.moveStep();
} else {
this.residueIt.setSegment(this.chainIt.move());
if (this.residueIt.hasNext)
this.residueTwo = this.residueIt.move();
return this.moveStep();
}
}

View File

@@ -1,8 +1,8 @@
/**
* Copyright (c) 2018-2022 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 Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
export { DnatcoNtCs } from './behavior';
export { DnatcoConfalPyramids } from './confal-pyramids/behavior';

View File

@@ -1,57 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeColorThemeProvider } from './color';
import { NtCTubeProvider } from './property';
import { NtCTubeRepresentationProvider } from './representation';
import { DnatcoTypes } from '../types';
import { Dnatco } from '../property';
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
export const NtCTubePreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-ntc-tube',
display: {
name: 'NtC Tube', group: 'Annotation',
description: 'NtC Tube',
},
isApplicable(a) {
return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const model = structureCell?.obj?.data.model;
if (!structureCell || !model) return {};
await plugin.runTask(Task.create('NtC tube', async runtime => {
await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
}));
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
const tube = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'NtC Tube' });
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
let tubeRepr;
if (representations)
tubeRepr = builder.buildRepresentation(update, tube, { type: NtCTubeRepresentationProvider, typeParams, color: NtCTubeColorThemeProvider }, { tag: 'ntc-tube' });
await update.commit({ revertOnError: true });
return { components: { ...components, tube }, representations: { ...representations, tubeRepr } };
}
});
export function NtCTubeSegmentLabel(step: DnatcoTypes.Step) {
return `
<b>${step.auth_asym_id_1}</b> |
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
<i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
`;
}

View File

@@ -1,99 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ErrorColor, NtCColors } from '../color';
import { NtCTubeProvider } from './property';
import { NtCTubeTypes as NTT } from './types';
import { Dnatco } from '../property';
import { Location } from '../../../mol-model/location';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorTheme } from '../../../mol-theme/color';
import { ThemeDataContext } from '../../../mol-theme/theme';
import { Color, ColorMap } from '../../../mol-util/color';
import { getColorMapParams } from '../../../mol-util/color/params';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TableLegend } from '../../../mol-util/legend';
import { ObjectKeys } from '../../../mol-util/type-helpers';
const Description = 'Assigns colors to NtC Tube segments';
const NtCTubeColors = ColorMap({
...NtCColors,
residueMarker: Color(0x222222),
stepBoundaryMarker: Color(0x656565),
});
type NtCTubeColors = typeof NtCTubeColors;
export const NtCTubeColorThemeParams = {
colors: PD.MappedStatic('default', {
'default': PD.EmptyGroup(),
'custom': PD.Group(getColorMapParams(NtCTubeColors)),
'uniform': PD.Color(Color(0xEEEEEE)),
}),
markResidueBoundaries: PD.Boolean(true),
markSegmentBoundaries: PD.Boolean(true),
};
export type NtCTubeColorThemeParams = typeof NtCTubeColorThemeParams;
export function getNtCTubeColorThemeParams(ctx: ThemeDataContext) {
return PD.clone(NtCTubeColorThemeParams);
}
export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTubeColorThemeParams>): ColorTheme<NtCTubeColorThemeParams> {
const colorMap = props.colors.name === 'default'
? NtCTubeColors
: props.colors.name === 'custom'
? props.colors.params
: ColorMap({
...Object.fromEntries(ObjectKeys(NtCTubeColors).map(item => [item, props.colors.params])),
residueMarker: NtCTubeColors.residueMarker,
stepBoundaryMarker: NtCTubeColors.stepBoundaryMarker
}) as NtCTubeColors;
function color(location: Location, isSecondary: boolean): Color {
if (NTT.isLocation(location)) {
const { data } = location;
const { step, kind } = data;
let key;
if (kind === 'upper')
key = step.NtC + '_Upr' as keyof NtCTubeColors;
else if (kind === 'lower')
key = step.NtC + '_Lwr' as keyof NtCTubeColors;
else if (kind === 'residue-boundary')
key = (!props.markResidueBoundaries ? step.NtC + '_Lwr' : 'residueMarker') as keyof NtCTubeColors;
else /* segment-boundary */
key = (!props.markSegmentBoundaries ? step.NtC + '_Lwr' : 'stepBoundaryMarker') as keyof NtCTubeColors;
return colorMap[key] ?? ErrorColor;
}
return ErrorColor;
}
return {
factory: NtCTubeColorTheme,
granularity: 'group',
color,
props,
description: Description,
legend: TableLegend(ObjectKeys(colorMap).map(k => [k.replace('_', ' '), colorMap[k]] as [string, Color]).concat([['Error', ErrorColor]])),
};
}
export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = {
name: 'ntc-tube',
label: 'NtC Tube',
category: ColorTheme.Category.Residue,
factory: NtCTubeColorTheme,
getParams: getNtCTubeColorThemeParams,
defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? NtCTubeProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && NtCTubeProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -1,44 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeTypes as NTT } from './types';
import { Dnatco, DnatcoParams } from '../property';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Model } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export const NtCTubeParams = { ...DnatcoParams };
export type NtCTubeParams = typeof NtCTubeParams;
export type NtCTubeProps = PD.Values<NtCTubeParams>;
export type NtCTubeData = PropertyWrapper<NTT.Data | undefined>;
async function fromCif(ctx: CustomProperty.Context, model: Model, props: NtCTubeProps): Promise<CustomProperty.Data<NtCTubeData>> {
const info = PropertyWrapper.createInfo();
const data = Dnatco.getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const steps = Dnatco.getStepsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: { data: steps } } };
}
export const NtCTubeProvider: CustomModelProperty.Provider<NtCTubeParams, NtCTubeData> = CustomModelProperty.createProvider({
label: 'NtC Tube',
descriptor: CustomPropertyDescriptor({
name: 'ntc-tube',
}),
type: 'static',
defaultParams: NtCTubeParams,
getParams: (data: Model) => NtCTubeParams,
isApplicable: (data: Model) => Dnatco.isApplicable(data),
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<NtCTubeProps>) => {
const p = { ...PD.getDefaultValues(NtCTubeParams), ...props };
return fromCif(ctx, data, p);
}
});

View File

@@ -1,454 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeProvider } from './property';
import { NtCTubeSegmentsIterator } from './util';
import { NtCTubeTypes as NTT } from './types';
import { Dnatco } from '../property';
import { DnatcoTypes } from '../types';
import { DnatcoUtil } from '../util';
import { Interval } from '../../../mol-data/int';
import { BaseGeometry, VisualQuality } from '../../../mol-geo/geometry/base';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { addFixedCountDashedCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
import { PickingId } from '../../../mol-geo/geometry/picking';
import { CylinderProps } from '../../../mol-geo/primitive/cylinder';
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { smoothstep } from '../../../mol-math/interpolate';
import { NullLocation } from '../../../mol-model/location';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
import { structureUnion } from '../../../mol-model/structure/query/utils/structure-set';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
import { createCurveSegmentState, CurveSegmentState } from '../../../mol-repr/structure/visual/util/polymer';
import { getStructureQuality, VisualUpdateState } from '../../../mol-repr/util';
import { VisualContext } from '../../../mol-repr/visual';
import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
const v3add = Vec3.add;
const v3copy = Vec3.copy;
const v3cross = Vec3.cross;
const v3fromArray = Vec3.fromArray;
const v3matchDirection = Vec3.matchDirection;
const v3normalize = Vec3.normalize;
const v3orthogonalize = Vec3.orthogonalize;
const v3scale = Vec3.scale;
const v3slerp = Vec3.slerp;
const v3spline = Vec3.spline;
const v3sub = Vec3.sub;
const v3toArray = Vec3.toArray;
const NtCTubeMeshParams = {
...UnitsMeshParams,
linearSegments: PD.Numeric(4, { min: 2, max: 8, step: 1 }, BaseGeometry.CustomQualityParamInfo),
radialSegments: PD.Numeric(22, { min: 4, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
residueMarkerWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
segmentBoundaryWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
};
type NtCTubeMeshParams = typeof NtCTubeMeshParams;
type QualityOptions = Exclude<VisualQuality, 'auto' | 'custom'>;
const LinearSegmentCount: Record<QualityOptions, number> = {
highest: 6,
higher: 6,
high: 4,
medium: 4,
low: 3,
lower: 3,
lowest: 2,
};
const RadialSegmentCount: Record<QualityOptions, number> = {
highest: 32,
higher: 26,
high: 22,
medium: 18,
low: 14,
lower: 10,
lowest: 6,
};
const _curvePoint = Vec3();
const _tanA = Vec3();
const _tanB = Vec3();
const _firstTangentVec = Vec3();
const _lastTangentVec = Vec3();
const _firstNormalVec = Vec3();
const _lastNormalVec = Vec3();
const _tmpNormal = Vec3();
const _tangentVec = Vec3();
const _normalVec = Vec3();
const _binormalVec = Vec3();
const _prevNormal = Vec3();
const _nextNormal = Vec3();
function interpolatePointsAndTangents(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, tRange: number[]) {
const { curvePoints, tangentVectors, linearSegments } = state;
const tension = 0.5;
const r = tRange[1] - tRange[0];
for (let j = 0; j <= linearSegments; ++j) {
const t = j * r / linearSegments + tRange[0];
v3spline(_curvePoint, p0, p1, p2, p3, t, tension);
v3spline(_tanA, p0, p1, p2, p3, t - 0.01, tension);
v3spline(_tanB, p0, p1, p2, p3, t + 0.01, tension);
v3toArray(_curvePoint, curvePoints, j * 3);
v3normalize(_tangentVec, v3sub(_tangentVec, _tanA, _tanB));
v3toArray(_tangentVec, tangentVectors, j * 3);
}
}
function interpolateNormals(state: CurveSegmentState, firstDirection: Vec3, lastDirection: Vec3) {
const { curvePoints, tangentVectors, normalVectors, binormalVectors } = state;
const n = curvePoints.length / 3;
v3fromArray(_firstTangentVec, tangentVectors, 0);
v3fromArray(_lastTangentVec, tangentVectors, (n - 1) * 3);
v3orthogonalize(_firstNormalVec, _firstTangentVec, firstDirection);
v3orthogonalize(_lastNormalVec, _lastTangentVec, lastDirection);
v3matchDirection(_lastNormalVec, _lastNormalVec, _firstNormalVec);
v3copy(_prevNormal, _firstNormalVec);
const n1 = n - 1;
for (let i = 0; i < n; ++i) {
const j = smoothstep(0, n1, i) * n1;
const t = i === 0 ? 0 : 1 / (n - j);
v3fromArray(_tangentVec, tangentVectors, i * 3);
v3orthogonalize(_normalVec, _tangentVec, v3slerp(_tmpNormal, _prevNormal, _lastNormalVec, t));
v3toArray(_normalVec, normalVectors, i * 3);
v3copy(_prevNormal, _normalVec);
v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
v3toArray(_binormalVec, binormalVectors, i * 3);
}
for (let i = 1; i < n1; ++i) {
v3fromArray(_prevNormal, normalVectors, (i - 1) * 3);
v3fromArray(_normalVec, normalVectors, i * 3);
v3fromArray(_nextNormal, normalVectors, (i + 1) * 3);
v3scale(_normalVec, v3add(_normalVec, _prevNormal, v3add(_normalVec, _nextNormal, _normalVec)), 1 / 3);
v3toArray(_normalVec, normalVectors, i * 3);
v3fromArray(_tangentVec, tangentVectors, i * 3);
v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
v3toArray(_binormalVec, binormalVectors, i * 3);
}
}
function interpolate(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, firstDir: Vec3, lastDir: Vec3, tRange = [0, 1]) {
interpolatePointsAndTangents(state, p0, p1, p2, p3, tRange);
interpolateNormals(state, firstDir, lastDir);
}
function createNtCTubeSegmentsIterator(structureGroup: StructureGroup): LocationIterator {
const { structure, group } = structureGroup;
const instanceCount = group.units.length;
const data = NtCTubeProvider.get(structure.model)?.value?.data;
if (!data) return LocationIterator(0, 1, 1, () => NullLocation);
const numBlocks = data.data.steps.length * 4;
const getLocation = (groupId: number, instanceId: number) => {
if (groupId > numBlocks) return NullLocation;
const stepIdx = Math.floor(groupId / 4);
const step = data.data.steps[stepIdx];
const r = groupId % 4;
const kind =
r === 0 ? 'upper' :
r === 1 ? 'lower' :
r === 2 ? 'residue-boundary' : 'segment-boundary';
return NTT.Location({ step, kind });
};
return LocationIterator(totalMeshGroupsCount(data.data.steps) + 1, instanceCount, 1, getLocation);
}
function segmentCount(structure: Structure, props: PD.Values<NtCTubeMeshParams>): { linear: number, radial: number } {
const quality = props.quality;
if (quality === 'custom')
return { linear: props.linearSegments, radial: props.radialSegments };
else if (quality === 'auto') {
const autoQuality = getStructureQuality(structure) as QualityOptions;
return { linear: LinearSegmentCount[autoQuality], radial: RadialSegmentCount[autoQuality] };
} else
return { linear: LinearSegmentCount[quality], radial: RadialSegmentCount[quality] };
}
function stepBoundingSphere(step: DnatcoTypes.Step, struLoci: StructureElement.Loci): Sphere3D | undefined {
const one = DnatcoUtil.residueToLoci(step.auth_asym_id_1, step.auth_seq_id_1, step.label_alt_id_1, step.PDB_ins_code_1, struLoci, 'auth');
const two = DnatcoUtil.residueToLoci(step.auth_asym_id_2, step.auth_seq_id_2, step.label_alt_id_2, step.PDB_ins_code_2, struLoci, 'auth');
if (StructureElement.Loci.is(one) && StructureElement.Loci.is(two)) {
const union = structureUnion(struLoci.structure, [StructureElement.Loci.toStructure(one), StructureElement.Loci.toStructure(two)]);
return union.boundary.sphere;
}
return void 0;
}
function totalMeshGroupsCount(steps: DnatcoTypes.Step[]) {
// Each segment has two blocks, Residue Boundary marker and a Segment Boundary marker
return steps.length * 4 - 1; // Subtract one because the last Segment Boundary marker is not drawn
}
function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<NtCTubeMeshParams>, mesh?: Mesh) {
if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
const prop = NtCTubeProvider.get(structure.model).value;
if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
const { data } = prop.data;
if (data.steps.length === 0) return Mesh.createEmpty(mesh);
const MarkerLinearSegmentCount = 2;
const segCount = segmentCount(structure, props);
const vertexCount = Math.floor((segCount.linear * 4 * data.steps.length / structure.model.atomicHierarchy.chains._rowCount) * segCount.radial);
const chunkSize = Math.floor(vertexCount / 3);
const diameter = 1.0 * theme.size.props.value;
const mb = MeshBuilder.createState(vertexCount, chunkSize, mesh);
const state = createCurveSegmentState(segCount.linear);
const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state;
for (let idx = 0; idx <= segCount.linear; idx++) {
widthValues[idx] = diameter;
heightValues[idx] = diameter;
}
const [normals, binormals] = [binormalVectors, normalVectors]; // Needed so that the tube is not drawn from inside out
const markerState = createCurveSegmentState(MarkerLinearSegmentCount);
const { curvePoints: mCurvePoints, normalVectors: mNormalVectors, binormalVectors: mBinormalVectors, widthValues: mWidthValues, heightValues: mHeightValues } = markerState;
for (let idx = 0; idx <= MarkerLinearSegmentCount; idx++) {
mWidthValues[idx] = diameter;
mHeightValues[idx] = diameter;
}
const [mNormals, mBinormals] = [mBinormalVectors, mNormalVectors];
const firstDir = Vec3();
const lastDir = Vec3();
const markerDir = Vec3();
const residueMarkerWidth = props.residueMarkerWidth / 2;
const it = new NtCTubeSegmentsIterator(structure, unit);
while (it.hasNext) {
const segment = it.move();
if (!segment)
continue;
const { p_1, p0, p1, p2, p3, p4, pP } = segment;
const FirstBlockId = segment.stepIdx * 4;
const SecondBlockId = FirstBlockId + 1;
const ResidueMarkerId = FirstBlockId + 2;
const SegmentBoundaryMarkerId = FirstBlockId + 3;
const { rmShift, rmPos } = calcResidueMarkerShift(p2, p3, pP);
if (segment.firstInChain) {
v3normalize(firstDir, v3sub(firstDir, p2, p1));
v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
} else {
v3copy(firstDir, lastDir);
v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
}
// C5' -> O3' block
interpolate(state, p0, p1, p2, p3, firstDir, lastDir);
mb.currentGroup = FirstBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, segment.firstInChain || segment.followsGap, false, 'rounded');
// O3' -> C5' block
v3copy(firstDir, lastDir);
v3normalize(markerDir, v3sub(markerDir, p3, rmPos));
v3normalize(lastDir, v3sub(lastDir, p4, p3));
// From O3' to the residue marker
interpolate(state, p1, p2, p3, p4, firstDir, markerDir, [0, rmShift - residueMarkerWidth]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
// Residue marker
interpolate(markerState, p1, p2, p3, p4, markerDir, markerDir, [rmShift - residueMarkerWidth, rmShift + residueMarkerWidth]);
mb.currentGroup = ResidueMarkerId;
addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
if (segment.capEnd) {
// From the residue marker to C5' of the end
interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, true, 'rounded');
} else {
// From the residue marker to C5' of the step boundary marker
interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1 - props.segmentBoundaryWidth]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
// Step boundary marker
interpolate(markerState, p1, p2, p3, p4, lastDir, lastDir, [1 - props.segmentBoundaryWidth, 1]);
mb.currentGroup = SegmentBoundaryMarkerId;
addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
}
if (segment.followsGap) {
const cylinderProps: CylinderProps = {
radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial,
};
mb.currentGroup = FirstBlockId;
addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, false, cylinderProps);
}
}
const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1.05);
const m = MeshBuilder.getMesh(mb);
m.setBoundingSphere(boundingSphere);
return m;
}
const _rmvCO = Vec3();
const _rmvPO = Vec3();
const _rmPos = Vec3();
const _HalfPi = Math.PI / 2;
function calcResidueMarkerShift(pO: Vec3, pC: Vec3, pP: Vec3): { rmShift: number, rmPos: Vec3 } {
v3sub(_rmvCO, pC, pO);
v3sub(_rmvPO, pP, pO);
// Project position of P atom on the O3' -> C5' vector
const beta = Vec3.angle(_rmvPO, _rmvCO);
const alpha = _HalfPi - Math.abs(beta);
const lengthMO = Math.cos(alpha) * Vec3.magnitude(_rmvPO);
const shift = lengthMO / Vec3.magnitude(_rmvCO);
v3scale(_rmvCO, _rmvCO, shift);
v3add(_rmPos, _rmvCO, pO);
return { rmShift: shift, rmPos: _rmPos };
}
function getNtCTubeSegmentLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
const { groupId, objectId, instanceId } = pickingId;
if (objectId !== id) return EmptyLoci;
const { structure } = structureGroup;
const unit = structureGroup.group.units[instanceId];
if (!Unit.isAtomic(unit)) return EmptyLoci;
const data = NtCTubeProvider.get(structure.model)?.value?.data ?? undefined;
if (!data) return EmptyLoci;
const MeshGroupsCount = totalMeshGroupsCount(data.data.steps);
if (groupId > MeshGroupsCount) return EmptyLoci;
const stepIdx = Math.floor(groupId / 4);
const bs = stepBoundingSphere(data.data.steps[stepIdx], Structure.toStructureElementLoci(structure));
/*
* NOTE 1) Each step is drawn with 4 mesh groups. We need to divide/multiply by 4 to convert between steps and mesh groups.
* NOTE 2) Molstar will create a mesh only for the asymmetric unit. When the entire biological assembly
* is displayed, Molstar just copies and transforms the mesh. This means that even though the mesh
* might be displayed multiple times, groupIds of the individual blocks in the mesh will be the same.
* If there are multiple copies of a mesh, Molstar needs to be able to tell which block belongs to which copy of the mesh.
* To do that, Molstar adds an offset to groupIds of the copied meshes. Offset is calculated as follows:
*
* offset = NumberOfBlocks * UnitIndex
*
* "NumberOfBlocks" is the number of valid Location objects got from LocationIterator *or* the greatest groupId set by
* the mesh generator - whichever is smaller.
*
* UnitIndex is the index of the Unit the mesh belongs to, starting from 0. (See "unitMap" in the Structure object).
* We can also get this index from the value "instanceId" of the "pickingId" object.
*
* If this offset is not applied, picking a piece of one of the copied meshes would actually pick that piece in the original mesh.
* This is particularly apparent with highlighting - hovering over items in a copied mesh incorrectly highlights those items in the source mesh.
*
* Molstar can take advantage of the fact that ElementLoci has a reference to the Unit object attached to it. Since we cannot attach ElementLoci
* to a step, we need to calculate the offseted groupId here and pass it as part of the DataLoci.
*/
const offsetGroupId = stepIdx * 4 + (MeshGroupsCount + 1) * instanceId;
return NTT.Loci(data.data.steps, [stepIdx], [offsetGroupId], bs);
}
function eachNtCTubeSegment(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
if (NTT.isLoci(loci)) {
const offsetGroupId = loci.elements[0];
return apply(Interval.ofBounds(offsetGroupId, offsetGroupId + 4));
}
return false;
}
function NtCTubeVisual(materialId: number): UnitsVisual<NtCTubeMeshParams> {
return UnitsMeshVisual<NtCTubeMeshParams>({
defaultProps: PD.getDefaultValues(NtCTubeMeshParams),
createGeometry: createNtCTubeMesh,
createLocationIterator: createNtCTubeSegmentsIterator,
getLoci: getNtCTubeSegmentLoci,
eachLocation: eachNtCTubeSegment,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NtCTubeMeshParams>, currentProps: PD.Values<NtCTubeMeshParams>) => {
state.createGeometry = (
newProps.quality !== currentProps.quality ||
newProps.residueMarkerWidth !== currentProps.residueMarkerWidth ||
newProps.segmentBoundaryWidth !== currentProps.segmentBoundaryWidth ||
newProps.doubleSided !== currentProps.doubleSided ||
newProps.alpha !== currentProps.alpha ||
newProps.linearSegments !== currentProps.linearSegments ||
newProps.radialSegments !== currentProps.radialSegments
);
}
}, materialId);
}
const NtCTubeVisuals = {
'ntc-tube-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeMeshParams>) => UnitsRepresentation('NtC Tube Mesh', ctx, getParams, NtCTubeVisual),
};
export const NtCTubeParams = {
...NtCTubeMeshParams
};
export type NtCTubeParams = typeof NtCTubeParams;
export function getNtCTubeParams(ctx: ThemeRegistryContext, structure: Structure) {
return PD.clone(NtCTubeParams);
}
export type NtCTubeRepresentation = StructureRepresentation<NtCTubeParams>;
export function NtCTubeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeParams>): NtCTubeRepresentation {
return Representation.createMulti('NtC Tube', ctx, getParams, StructureRepresentationStateBuilder, NtCTubeVisuals as unknown as Representation.Def<Structure, NtCTubeParams>);
}
export const NtCTubeRepresentationProvider = StructureRepresentationProvider({
name: 'ntc-tube',
label: 'NtC Tube',
description: 'Displays schematic representation of NtC conformers',
factory: NtCTubeRepresentation,
getParams: getNtCTubeParams,
defaultValues: PD.getDefaultValues(NtCTubeParams),
defaultColorTheme: { name: 'ntc-tube' },
defaultSizeTheme: { name: 'uniform', props: { value: 2.0 } },
isApplicable: (structure: Structure) => structure.models.every(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: async (ctx: CustomProperty.Context, structure: Structure) => structure.models.forEach(m => NtCTubeProvider.attach(ctx, m, void 0, true)),
detach: (data) => data.models.forEach(m => NtCTubeProvider.ref(m, false)),
},
});

View File

@@ -1,51 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeSegmentLabel } from './behavior';
import { DnatcoTypes } from '../types';
import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
import { DataLocation } from '../../../mol-model/location';
import { DataLoci } from '../../../mol-model/loci';
export namespace NtCTubeTypes {
const DataTag = 'dnatco-tube-segment-data';
const DummyTag = 'dnatco-tube-dummy';
export type Data = {
data: DnatcoTypes.Steps,
}
export type TubeBlock = {
step: DnatcoTypes.Step,
kind: 'upper' | 'lower' | 'residue-boundary' | 'segment-boundary';
}
export interface Location extends DataLocation<TubeBlock> {}
export function Location(payload: TubeBlock) {
return DataLocation(DataTag, payload, {});
}
export function isLocation(x: any): x is Location {
return !!x && x.kind === 'data-location' && x.tag === DataTag;
}
export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
export interface DummyLoci extends DataLoci<{}, number> {}
export function Loci(data: DnatcoTypes.Step[], stepIndices: number[], elements: number[], boundingSphere?: Sphere3D): Loci {
return DataLoci(DataTag, data, elements, boundingSphere ? () => boundingSphere : undefined, () => stepIndices[0] !== undefined ? NtCTubeSegmentLabel(data[stepIndices[0]]) : '');
}
export function DummyLoci(): DummyLoci {
return DataLoci(DummyTag, {}, [], undefined, () => '');
}
export function isLoci(x: any): x is Loci {
return !!x && x.kind === 'data-loci' && x.tag === DataTag;
}
}

View File

@@ -1,214 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeTypes as NTT } from './types';
import { NtCTubeProvider } from './property';
import { DnatcoUtil } from '../util';
import { Segmentation, SortedArray } from '../../../mol-data/int';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
function getAtomPosition(vec: Vec3, loc: StructureElement.Location, residue: DnatcoUtil.Residue, names: string[], altId: string, insCode: string) {
const eI = DnatcoUtil.getAtomIndex(loc, residue, names, altId, insCode);
if (eI !== -1) {
loc.unit.conformation.invariantPosition(eI, vec);
return true;
}
return false; // Atom not found
}
const p_1 = Vec3();
const p0 = Vec3();
const p1 = Vec3();
const p2 = Vec3();
const p3 = Vec3();
const p4 = Vec3();
const pP = Vec3();
const C5PrimeNames = ['C5\'', 'C5*'];
const O3PrimeNames = ['O3\'', 'O3*'];
const O5PrimeNames = ['O5\'', 'O5*'];
const PNames = ['P'];
function getPoints(
loc: StructureElement.Location,
r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue,
altId0: string, altId1: string, altId2: string,
insCode0: string, insCode1: string, insCode2: string,
) {
if (r0) {
if (!getAtomPosition(p_1, loc, r0, C5PrimeNames, altId0, insCode0))
return void 0;
if (!getAtomPosition(p0, loc, r0, O3PrimeNames, altId0, insCode0))
return void 0;
} else {
if (!getAtomPosition(p0, loc, r1, O5PrimeNames, altId1, insCode1))
return void 0;
}
if (!getAtomPosition(p1, loc, r1, C5PrimeNames, altId1, insCode1))
return void 0;
if (!getAtomPosition(p2, loc, r1, O3PrimeNames, altId1, insCode1))
return void 0;
if (!getAtomPosition(p3, loc, r2, C5PrimeNames, altId2, insCode2))
return void 0;
if (!getAtomPosition(p4, loc, r2, O3PrimeNames, altId2, insCode2))
return void 0;
if (!getAtomPosition(pP, loc, r2, PNames, altId2, insCode2))
return void 0;
return { p_1, p0, p1, p2, p3, p4, pP };
}
function hasGapElements(r: DnatcoUtil.Residue, unit: Unit) {
for (let xI = r.start; xI < r.end; xI++) {
const eI = unit.elements[xI];
if (SortedArray.has(unit.gapElements, eI)) {
return true;
}
}
return false;
}
export type NtCTubeSegment = {
p_1: Vec3,
p0: Vec3,
p1: Vec3,
p2: Vec3,
p3: Vec3,
p4: Vec3,
pP: Vec3,
stepIdx: number,
followsGap: boolean,
firstInChain: boolean,
capEnd: boolean,
}
export class NtCTubeSegmentsIterator {
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
/* Second residue of the previous step, may be undefined
* if we are at the beginning of a chain or right after a discontinuity */
private residuePrev?: DnatcoUtil.Residue;
/* First residue of the current step */
private residueOne?: DnatcoUtil.Residue;
/* Second residue of the current step */
private residueTwo: DnatcoUtil.Residue;
/* First residue of the next step, may be undefined
* if we are at the end of a chain.
* Undefined value indicates that the iterator has reached the end.*/
private residueNext?: DnatcoUtil.Residue;
private data?: NTT.Data;
private altIdOne = '';
private insCodeOne = '';
private loc: StructureElement.Location;
private moveStep() {
if (!this.residueNext)
return void 0;
/* Assume discontinuity of the ResidueIndex of the residue that would become residue one (= first residue of the corresponding step)
* does not equal to ResidueIndex of what would be residue two (= second residue of the corresponding step). */
if (this.residueTwo.index + 1 === this.residueNext.index) {
this.residuePrev = DnatcoUtil.copyResidue(this.residueOne);
this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
this.residueTwo = DnatcoUtil.copyResidue(this.residueNext)!;
this.residueNext = this.residueIt.hasNext ? DnatcoUtil.copyResidue(this.residueIt.move())! : void 0;
} else {
if (!this.residueIt.hasNext) {
this.residueNext = void 0;
return void 0;
}
// There is discontinuity, act as if we were at the beginning of a chain
this.residuePrev = void 0;
this.residueOne = DnatcoUtil.copyResidue(this.residueNext);
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
this.residueNext = this.residueIt.hasNext ? DnatcoUtil.copyResidue(this.residueIt.move())! : void 0;
}
return this.toSegment(this.residuePrev, this.residueOne!, this.residueTwo, this.residueNext);
}
private prime() {
if (this.residueIt.hasNext)
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
if (this.residueIt.hasNext)
this.residueNext = this.residueIt.move();
}
private toSegment(r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue, r3: DnatcoUtil.Residue | undefined): NtCTubeSegment | undefined {
const indices = DnatcoUtil.getStepIndices(this.data!.data, this.loc, r1);
if (indices.length === 0)
return void 0;
const stepIdx = indices[0];
const step = this.data!.data.steps[stepIdx];
const altIdPrev = this.altIdOne;
const insCodePrev = this.insCodeOne;
this.altIdOne = step.label_alt_id_1;
this.insCodeOne = step.PDB_ins_code_1;
const altIdTwo = step.label_alt_id_2;
const insCodeTwo = step.PDB_ins_code_2;
const followsGap = !!r0 && hasGapElements(r0, this.loc.unit) && hasGapElements(r1, this.loc.unit);
const precedesDiscontinuity = r3 ? r3.index !== r2.index + 1 : false;
const points = getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo);
if (!points)
return void 0;
return {
...points,
stepIdx,
followsGap,
firstInChain: !r0,
capEnd: !this.residueNext || precedesDiscontinuity || hasGapElements(r2, this.loc.unit),
};
}
constructor(structure: Structure, unit: Unit.Atomic) {
this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
const prop = NtCTubeProvider.get(unit.model).value;
this.data = prop?.data;
if (this.chainIt.hasNext) {
this.residueIt.setSegment(this.chainIt.move());
this.prime();
}
this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
}
get hasNext() {
if (!this.data)
return false;
return !!this.residueNext
? true
: this.chainIt.hasNext;
}
move() {
if (!!this.residueNext) {
return this.moveStep();
} else {
this.residuePrev = void 0; // Assume discontinuity when we switch chains
this.residueNext = void 0;
this.residueIt.setSegment(this.chainIt.move());
this.prime();
return this.moveStep();
}
}
}

View File

@@ -1,172 +0,0 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from './types';
import { Column, Table } from '../../mol-data/db';
import { toTable } from '../../mol-io/reader/cif/schema';
import { Model } from '../../mol-model/structure';
import { CustomProperty } from '../../mol-model-props/common/custom-property';
import { PropertyWrapper } from '../../mol-model-props/common/wrapper';
import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
export type DnatcoSteps = PropertyWrapper<DnatcoTypes.Steps | undefined>;
export const DnatcoParams = {};
export type DnatcoParams = typeof DnatcoParams;
export type DnatcoProps = PD.Values<DnatcoParams>;
export namespace Dnatco {
export const Schema = {
ndb_struct_ntc_step: {
id: Column.Schema.int,
name: Column.Schema.str,
PDB_model_number: Column.Schema.int,
label_entity_id_1: Column.Schema.int,
label_asym_id_1: Column.Schema.str,
label_seq_id_1: Column.Schema.int,
label_comp_id_1: Column.Schema.str,
label_alt_id_1: Column.Schema.str,
label_entity_id_2: Column.Schema.int,
label_asym_id_2: Column.Schema.str,
label_seq_id_2: Column.Schema.int,
label_comp_id_2: Column.Schema.str,
label_alt_id_2: Column.Schema.str,
auth_asym_id_1: Column.Schema.str,
auth_seq_id_1: Column.Schema.int,
auth_asym_id_2: Column.Schema.str,
auth_seq_id_2: Column.Schema.int,
PDB_ins_code_1: Column.Schema.str,
PDB_ins_code_2: Column.Schema.str,
},
ndb_struct_ntc_step_summary: {
step_id: Column.Schema.int,
assigned_CANA: Column.Schema.str,
assigned_NtC: Column.Schema.str,
confal_score: Column.Schema.int,
euclidean_distance_NtC_ideal: Column.Schema.float,
cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
closest_CANA: Column.Schema.str,
closest_NtC: Column.Schema.str,
closest_step_golden: Column.Schema.str
}
};
export type Schema = typeof Schema;
export function getStepsFromCif(
model: Model,
cifSteps: Table<typeof Dnatco.Schema.ndb_struct_ntc_step>,
stepsSummary: StepsSummaryTable
): DnatcoTypes.Steps {
const steps = new Array<DnatcoTypes.Step>();
const mapping = new Array<DnatcoTypes.MappedChains>();
const {
id, PDB_model_number, name,
auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
_rowCount
} = cifSteps;
if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
for (let i = 0; i < _rowCount; i++) {
const {
NtC,
confal_score,
rmsd
} = getSummaryData(id.value(i), i, stepsSummary);
const modelNum = PDB_model_number.value(i);
const chainId = auth_asym_id_1.value(i);
const seqId = auth_seq_id_1.value(i);
const modelIdx = modelNum - 1;
if (mapping.length <= modelIdx || !mapping[modelIdx])
mapping[modelIdx] = new Map<string, DnatcoTypes.MappedResidues>();
const step = {
PDB_model_number: modelNum,
name: name.value(i),
auth_asym_id_1: chainId,
auth_seq_id_1: seqId,
label_comp_id_1: label_comp_id_1.value(i),
label_alt_id_1: label_alt_id_1.value(i),
PDB_ins_code_1: PDB_ins_code_1.value(i),
auth_asym_id_2: auth_asym_id_2.value(i),
auth_seq_id_2: auth_seq_id_2.value(i),
label_comp_id_2: label_comp_id_2.value(i),
label_alt_id_2: label_alt_id_2.value(i),
PDB_ins_code_2: PDB_ins_code_2.value(i),
confal_score,
NtC,
rmsd,
};
steps.push(step);
const mappedChains = mapping[modelIdx];
const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
const stepsForResidue = residuesOnChain.get(seqId) ?? [];
stepsForResidue.push(steps.length - 1);
residuesOnChain.set(seqId, stepsForResidue);
mappedChains.set(chainId, residuesOnChain);
mapping[modelIdx] = mappedChains;
}
return { steps, mapping };
}
export async function fromCif(ctx: CustomProperty.Context, model: Model, props: DnatcoProps): Promise<CustomProperty.Data<DnatcoSteps>> {
const info = PropertyWrapper.createInfo();
const data = getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const fromCif = getStepsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: fromCif } };
}
export function getCifData(model: Model) {
if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
if (!hasNdbStructNtcCategories(model)) return undefined;
return {
steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
};
}
function hasNdbStructNtcCategories(model: Model): boolean {
if (!MmcifFormat.is(model.sourceData)) return false;
const names = (model.sourceData).data.frame.categoryNames;
return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
}
export function isApplicable(model?: Model): boolean {
return !!model && hasNdbStructNtcCategories(model);
}
}
type StepsSummaryTable = Table<typeof Dnatco.Schema.ndb_struct_ntc_step_summary>;
function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
const {
step_id,
confal_score,
assigned_NtC,
cartesian_rmsd_closest_NtC_representative,
} = stepsSummary;
// Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
for (let j = i; j < stepsSummary._rowCount; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
// Safety net for cases where the previous assumption is not met
for (let j = 0; j < i; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
throw new Error('Inconsistent mmCIF data');
}

View File

@@ -1,41 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
export namespace DnatcoTypes {
export const DataTag = 'dnatco-confal-half-step';
export type Step = {
PDB_model_number: number,
name: string,
auth_asym_id_1: string,
auth_seq_id_1: number,
label_comp_id_1: string,
label_alt_id_1: string,
PDB_ins_code_1: string,
auth_asym_id_2: string,
auth_seq_id_2: number,
label_comp_id_2: string,
label_alt_id_2: string,
PDB_ins_code_2: string,
confal_score: number,
NtC: string,
rmsd: number,
}
export type MappedChains = Map<string, MappedResidues>;
export type MappedResidues = Map<number, number[]>;
export interface Steps {
steps: Array<Step>,
mapping: MappedChains[],
}
export interface HalfStep {
step: Step,
isLower: boolean,
}
}

View File

@@ -1,117 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from './types';
import { OrderedSet, Segmentation } from '../../mol-data/int';
import { EmptyLoci } from '../../mol-model/loci';
import { ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
const EmptyStepIndices = new Array<number>();
export namespace DnatcoUtil {
export type Residue = Segmentation.Segment<ResidueIndex>;
export function copyResidue(r?: Residue) {
return r ? { index: r.index, start: r.start, end: r.end } : void 0;
}
export function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string, insCode: string): ElementIndex {
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = loc.unit.elements[eI];
const elName = StructureProperties.atom.label_atom_id(loc);
const elAltId = StructureProperties.atom.label_alt_id(loc);
const elInsCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
if (names.includes(elName) && (elAltId === altId || elAltId.length === 0) && (elInsCode === insCode))
return loc.element;
}
return -1 as ElementIndex;
}
export function getStepIndices(data: DnatcoTypes.Steps, loc: StructureElement.Location, r: DnatcoUtil.Residue) {
loc.element = loc.unit.elements[r.start];
const modelIdx = StructureProperties.unit.model_num(loc) - 1;
const chainId = StructureProperties.chain.auth_asym_id(loc);
const seqId = StructureProperties.residue.auth_seq_id(loc);
const insCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
const chains = data.mapping[modelIdx];
if (!chains) return EmptyStepIndices;
const residues = chains.get(chainId);
if (!residues) return EmptyStepIndices;
const indices = residues.get(seqId);
if (!indices) return EmptyStepIndices;
return insCode !== '' ? indices.filter(idx => data.steps[idx].PDB_ins_code_1 === insCode) : indices;
}
export function residueAltIds(structure: Structure, unit: Unit, residue: Residue) {
const altIds = new Array<string>();
const loc = StructureElement.Location.create(structure, unit);
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = OrderedSet.getAt(unit.elements, eI);
const altId = StructureProperties.atom.label_alt_id(loc);
if (altId !== '' && !altIds.includes(altId))
altIds.push(altId);
}
return altIds;
}
const _loc = StructureElement.Location.create();
export function residueToLoci(asymId: string, seqId: number, altId: string | undefined, insCode: string, loci: StructureElement.Loci, source: 'label' | 'auth') {
_loc.structure = loci.structure;
for (const e of loci.elements) {
_loc.unit = e.unit;
const getAsymId = source === 'label' ? StructureProperties.chain.label_asym_id : StructureProperties.chain.auth_asym_id;
const getSeqId = source === 'label' ? StructureProperties.residue.label_seq_id : StructureProperties.residue.auth_seq_id;
// Walk the entire unit and look for the requested residue
const chainIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.chainAtomSegments, e.unit.elements);
const residueIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.residueAtomSegments, e.unit.elements);
const elemIndex = (idx: number) => OrderedSet.getAt(e.unit.elements, idx);
while (chainIt.hasNext) {
const chain = chainIt.move();
_loc.element = elemIndex(chain.start);
const _asymId = getAsymId(_loc);
if (_asymId !== asymId)
continue; // Wrong chain, skip it
residueIt.setSegment(chain);
while (residueIt.hasNext) {
const residue = residueIt.move();
_loc.element = elemIndex(residue.start);
const _seqId = getSeqId(_loc);
if (_seqId === seqId) {
const _insCode = StructureProperties.residue.pdbx_PDB_ins_code(_loc);
if (_insCode !== insCode)
continue;
if (altId) {
const _altIds = residueAltIds(loci.structure, e.unit, residue);
if (!_altIds.includes(altId))
continue;
}
const start = residue.start as StructureElement.UnitIndex;
const end = residue.end as StructureElement.UnitIndex;
return StructureElement.Loci(
loci.structure,
[{ unit: e.unit, indices: OrderedSet.ofBounds(start, end) }]
);
}
}
}
}
return EmptyLoci;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -13,7 +13,7 @@ import { PLUGIN_VERSION } from '../../mol-plugin/version';
import { RuntimeContext } from '../../mol-task';
import { Color } from '../../mol-util/color/color';
import { fillSerial } from '../../mol-util/array';
import { NumberArray, assertUnreachable } from '../../mol-util/type-helpers';
import { NumberArray } from '../../mol-util/type-helpers';
import { MeshExporter, AddMeshInput, MeshGeoData } from './mesh-exporter';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
@@ -35,15 +35,6 @@ const BIN_CHUNK_TYPE = 0x004E4942;
const JSON_PAD_CHAR = 0x20;
const BIN_PAD_CHAR = 0x00;
function getPrimitiveMode(mode: 'points' | 'lines' | 'triangles'): number {
switch (mode) {
case 'points': return 0;
case 'lines': return 1;
case 'triangles': return 4;
default: assertUnreachable(mode);
}
}
export type GlbData = {
glb: Uint8Array
}
@@ -98,12 +89,12 @@ export class GlbExporter extends MeshExporter<GlbData> {
return accessorOffset;
}
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array | undefined, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
const tmpV = Vec3();
const stride = isGeoTexture ? 4 : 3;
const vertexArray = new Float32Array(vertexCount * 3);
let normalArray: Float32Array | undefined;
const normalArray = new Float32Array(vertexCount * 3);
let indexArray: Uint32Array | undefined;
// position
@@ -113,35 +104,32 @@ export class GlbExporter extends MeshExporter<GlbData> {
}
// normal
if (normals) {
normalArray = new Float32Array(vertexCount * 3);
for (let i = 0; i < vertexCount; ++i) {
v3fromArray(tmpV, normals, i * stride);
v3normalize(tmpV, tmpV);
v3toArray(tmpV, normalArray, i * 3);
}
for (let i = 0; i < vertexCount; ++i) {
v3fromArray(tmpV, normals, i * stride);
v3normalize(tmpV, tmpV);
v3toArray(tmpV, normalArray, i * 3);
}
// face
if (!isGeoTexture && indices) {
indexArray = indices.slice(0, drawCount);
if (!isGeoTexture) {
indexArray = indices!.slice(0, drawCount);
}
const [vertexMin, vertexMax] = GlbExporter.vec3MinMax(vertexArray);
let vertexBuffer = vertexArray.buffer;
let normalBuffer = normalArray?.buffer;
let indexBuffer = (isGeoTexture || !indexArray) ? undefined : indexArray.buffer;
let normalBuffer = normalArray.buffer;
let indexBuffer = isGeoTexture ? undefined : indexArray!.buffer;
if (!IsNativeEndianLittle) {
vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
if (normalBuffer) normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
if (!isGeoTexture) indexBuffer = flipByteOrder(new Uint8Array(indexBuffer!), 4);
}
return {
vertexAccessorIndex: this.addBuffer(vertexBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER, vertexMin, vertexMax),
normalAccessorIndex: normalBuffer ? this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER) : undefined,
indexAccessorIndex: (isGeoTexture || !indexBuffer) ? undefined : this.addBuffer(indexBuffer, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
normalAccessorIndex: this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER),
indexAccessorIndex: isGeoTexture ? undefined : this.addBuffer(indexBuffer!, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
};
}
@@ -170,8 +158,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
}
private addMaterial(metalness: number, roughness: number, doubleSided: boolean, alpha: boolean) {
const hash = `${metalness}|${roughness}|${doubleSided}`;
private addMaterial(metalness: number, roughness: number) {
const hash = `${metalness}|${roughness}`;
if (!this.materialMap.has(hash)) {
this.materialMap.set(hash, this.materials.length);
this.materials.push({
@@ -179,16 +167,14 @@ export class GlbExporter extends MeshExporter<GlbData> {
baseColorFactor: [1, 1, 1, 1],
metallicFactor: metalness,
roughnessFactor: roughness
},
doubleSided,
alphaMode: alpha ? 'BLEND' : 'OPAQUE',
}
});
}
return this.materialMap.get(hash)!;
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
const { mesh, values, isGeoTexture, webgl, ctx } = input;
const t = Mat4();
@@ -200,34 +186,32 @@ export class GlbExporter extends MeshExporter<GlbData> {
const instanceCount = values.uInstanceCount.ref.value;
const metalness = values.uMetalness.ref.value;
const roughness = values.uRoughness.ref.value;
const doubleSided = values.uDoubleSided?.ref.value || values.hasReflection.ref.value;
const alpha = values.uAlpha.ref.value < 1;
const material = this.addMaterial(metalness, roughness, doubleSided, alpha);
const material = this.addMaterial(metalness, roughness);
let interpolatedColors: Uint8Array | undefined;
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
if (colorType === 'volume' || colorType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedColors = GlbExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
interpolatedColors = GlbExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
}
let interpolatedOverpaint: Uint8Array | undefined;
if (webgl && mesh && overpaintType === 'volumeInstance') {
if (overpaintType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
}
let interpolatedTransparency: Uint8Array | undefined;
if (webgl && mesh && transparencyType === 'volumeInstance') {
if (transparencyType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
}
// instancing
const sameGeometryBuffers = mesh !== undefined;
const sameColorBuffer = sameGeometryBuffers && colorType !== 'instance' && !colorType.endsWith('Instance') && !dTransparency;
let vertexAccessorIndex: number;
let normalAccessorIndex: number | undefined;
let normalAccessorIndex: number;
let indexAccessorIndex: number | undefined;
let colorAccessorIndex: number;
let meshIndex: number;
@@ -251,7 +235,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 }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
}
// glTF mesh
@@ -264,8 +248,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
COLOR_0: colorAccessorIndex!
},
indices: indexAccessorIndex,
material,
mode: getPrimitiveMode(mode),
material
}]
});
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -28,32 +28,24 @@ import { Color } from '../../mol-util/color/color';
import { unpackRGBToInt } from '../../mol-util/number-packing';
import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { ValueCell } from '../../mol-util/value-cell';
const GeoExportName = 'geo-export';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3fromArray = Vec3.fromArray;
const v3sub = Vec3.sub;
const v3dot = Vec3.dot;
const v3unitY = Vec3.unitY;
type MeshMode = 'points' | 'lines' | 'triangles'
export interface AddMeshInput {
mesh: {
vertices: Float32Array
normals: Float32Array | undefined
normals: Float32Array
indices: Uint32Array | undefined
groups: Float32Array | Uint8Array
vertexCount: number
drawCount: number
} | undefined
meshes: Mesh[] | undefined
values: BaseValues & { readonly uDoubleSided?: ValueCell<any> }
values: BaseValues
isGeoTexture: boolean
mode: MeshMode
webgl: WebGLContext | undefined
ctx: RuntimeContext
}
@@ -63,8 +55,7 @@ export type MeshGeoData = {
groups: Float32Array | Uint8Array,
vertexCount: number,
instanceIndex: number,
isGeoTexture: boolean,
mode: MeshMode
isGeoTexture: boolean
}
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
@@ -231,7 +222,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
}
protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
const groupCount = values.uGroupCount.ref.value;
const colorType = values.dColorType.ref.value;
const uColor = values.uColor.ref.value;
@@ -240,12 +231,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const dOverpaint = values.dOverpaint.ref.value;
const tOverpaint = values.tOverpaint.ref.value.array;
let vertexCount = geoData.vertexCount;
if (mode === 'lines') {
vertexIndex *= 2;
vertexCount *= 2;
}
let color: Color;
switch (colorType) {
case 'uniform':
@@ -313,18 +298,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
}
protected static getTransparency(vertexIndex: number, geoData: MeshGeoData, interpolatedTransparency?: Uint8Array): number {
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
const groupCount = values.uGroupCount.ref.value;
const dTransparency = values.dTransparency.ref.value;
const tTransparency = values.tTransparency.ref.value.array;
const transparencyType = values.dTransparencyType.ref.value;
let vertexCount = geoData.vertexCount;
if (mode === 'lines') {
vertexIndex *= 2;
vertexCount *= 2;
}
let transparency: number = 0;
if (dTransparency) {
switch (transparencyType) {
@@ -350,7 +329,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
return transparency;
}
protected abstract addMeshWithColors(input: AddMeshInput): Promise<void>;
protected abstract addMeshWithColors(input: AddMeshInput): void;
private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aPosition = values.aPosition.ref.value;
@@ -370,132 +349,36 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
drawCount = values.drawCount.ref.value;
}
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, webgl, ctx });
}
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 vertexCount = (values.uVertexCount.ref.value / 4) * 2;
const drawCount = values.drawCount.ref.value / (2 * 3);
if (this.options.linesAsTriangles) {
const start = Vec3();
const end = Vec3();
const instanceCount = values.instanceCount.ref.value;
const meshes: Mesh[] = [];
const radialSegments = 6;
const topCap = true;
const bottomCap = true;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
for (let i = 0, il = vertexCount * 2; i < il; i += 4) {
v3fromArray(start, aStart, i * 3);
v3fromArray(end, aEnd, i * 3);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
state.currentGroup = aGroup[i];
addCylinder(state, start, end, 1, cylinderProps);
}
meshes.push(MeshBuilder.getMesh(state));
}
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) {
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 });
}
// TODO
}
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
const aPosition = values.aPosition.ref.value;
const aGroup = values.aGroup.ref.value;
const vertexCount = values.uVertexCount.ref.value;
const drawCount = values.drawCount.ref.value;
if (this.options.pointsAsTriangles) {
const center = Vec3();
const instanceCount = values.instanceCount.ref.value;
const meshes: Mesh[] = [];
const detail = 0;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
for (let i = 0; i < vertexCount; ++i) {
v3fromArray(center, aPosition, i * 3);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
state.currentGroup = group;
addSphere(state, center, radius, detail);
}
meshes.push(MeshBuilder.getMesh(state));
}
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 });
}
// TODO
}
private async addSpheres(values: SpheresValues, webgl: WebGLContext, ctx: RuntimeContext) {
const center = Vec3();
const aPosition = values.centerBuffer.ref.value;
const aGroup = values.groupBuffer.ref.value;
const aPosition = values.aPosition.ref.value;
const aGroup = values.aGroup.ref.value;
const instanceCount = values.instanceCount.ref.value;
const vertexCount = values.uVertexCount.ref.value;
const meshes: Mesh[] = [];
const sphereCount = vertexCount / 6 * instanceCount;
const sphereCount = vertexCount / 4 * instanceCount;
let detail: number;
switch (this.options.primitivesQuality) {
case 'auto':
if (sphereCount < 2000) detail = 3;
else if (sphereCount < 20000) detail = 2;
else detail = 1;
break;
case 'high':
detail = 3;
break;
case 'medium':
detail = 2;
break;
case 'low':
detail = 1;
break;
default:
assertUnreachable(this.options.primitivesQuality);
}
if (sphereCount < 2000) detail = 3;
else if (sphereCount < 20000) detail = 2;
else detail = 1;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
for (let i = 0; i < sphereCount; ++i) {
for (let i = 0; i < vertexCount; i += 4) {
v3fromArray(center, aPosition, i * 3);
const group = aGroup[i];
@@ -507,13 +390,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
}
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
const start = Vec3();
const end = Vec3();
const dir = Vec3();
const aStart = values.aStart.ref.value;
const aEnd = values.aEnd.ref.value;
@@ -526,24 +408,9 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const cylinderCount = vertexCount / 6 * instanceCount;
let radialSegments: number;
switch (this.options.primitivesQuality) {
case 'auto':
if (cylinderCount < 2000) radialSegments = 36;
else if (cylinderCount < 20000) radialSegments = 24;
else radialSegments = 12;
break;
case 'high':
radialSegments = 36;
break;
case 'medium':
radialSegments = 24;
break;
case 'low':
radialSegments = 12;
break;
default:
assertUnreachable(this.options.primitivesQuality);
}
if (cylinderCount < 2000) radialSegments = 36;
else if (cylinderCount < 20000) radialSegments = 24;
else radialSegments = 12;
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
const state = MeshBuilder.createState(512, 256);
@@ -551,17 +418,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
for (let i = 0; i < vertexCount; i += 6) {
v3fromArray(start, aStart, i * 3);
v3fromArray(end, aEnd, i * 3);
v3sub(dir, end, start);
const group = aGroup[i];
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
const cap = aCap[i];
let topCap = cap === 1 || cap === 3;
let bottomCap = cap >= 2;
if (v3dot(v3unitY, dir) > 0) {
[bottomCap, topCap] = [topCap, bottomCap];
}
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
const topCap = cap === 1 || cap === 3;
const bottomCap = cap >= 2;
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
state.currentGroup = aGroup[i];
addCylinder(state, start, end, 1, cylinderProps);
}
@@ -569,7 +432,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
meshes.push(MeshBuilder.getMesh(state));
}
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
}
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
@@ -594,13 +457,11 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const vertexCount = values.uVertexCount.ref.value;
const drawCount = values.drawCount.ref.value;
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, mode: 'triangles', webgl, ctx });
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, webgl, ctx });
}
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext) {
if (!renderObject.state.visible && !this.options.includeHidden) return;
if (renderObject.values.drawCount.ref.value === 0) return;
if (renderObject.values.instanceCount.ref.value === 0) return;
if (!renderObject.state.visible) return;
switch (renderObject.type) {
case 'mesh':
@@ -618,13 +479,6 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
}
}
protected options = {
includeHidden: false,
linesAsTriangles: false,
pointsAsTriangles: false,
primitivesQuality: 'auto' as 'auto' | 'high' | 'medium' | 'low',
};
abstract getData(ctx: RuntimeContext): Promise<D>;
abstract getBlob(ctx: RuntimeContext): Promise<Blob>;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -70,8 +70,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
if (mode !== 'triangles') return;
const { mesh, values, isGeoTexture, webgl, ctx } = input;
const obj = this.obj;
const t = Mat4();
@@ -87,19 +86,19 @@ export class ObjExporter extends MeshExporter<ObjData> {
const instanceCount = values.uInstanceCount.ref.value;
let interpolatedColors: Uint8Array | undefined;
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
interpolatedColors = ObjExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
if (colorType === 'volume' || colorType === 'volumeInstance') {
interpolatedColors = ObjExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
}
let interpolatedOverpaint: Uint8Array | undefined;
if (webgl && mesh && overpaintType === 'volumeInstance') {
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
if (overpaintType === 'volumeInstance') {
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
}
let interpolatedTransparency: Uint8Array | undefined;
if (webgl && mesh && transparencyType === 'volumeInstance') {
if (transparencyType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
}
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
@@ -127,7 +126,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
// normal
for (let i = 0; i < vertexCount; ++i) {
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
StringBuilder.writeSafe(obj, 'vn ');
StringBuilder.writeFloat(obj, tmpV[0], 100);
StringBuilder.whitespace1(obj);
@@ -137,7 +136,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
StringBuilder.newline(obj);
}
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
// color
const quantizedColors = new Uint8Array(drawCount * 3);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
*/
@@ -30,8 +30,7 @@ export class StlExporter extends MeshExporter<StlData> {
private centerTransform: Mat4;
protected async addMeshWithColors(input: AddMeshInput) {
const { values, isGeoTexture, mode, ctx } = input;
if (mode !== 'triangles') return;
const { values, isGeoTexture, ctx } = input;
const t = Mat4();
const tmpV = Vec3();

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -61,8 +61,7 @@ def Material "material${materialKey}"
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
if (mode !== 'triangles') return;
const { mesh, values, isGeoTexture, webgl, ctx } = input;
const t = Mat4();
const n = Mat3();
@@ -79,20 +78,20 @@ def Material "material${materialKey}"
const roughness = values.uRoughness.ref.value;
let interpolatedColors: Uint8Array | undefined;
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
if (colorType === 'volume' || colorType === 'volumeInstance') {
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
}
let interpolatedOverpaint: Uint8Array | undefined;
if (webgl && mesh && overpaintType === 'volumeInstance') {
if (overpaintType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
}
let interpolatedTransparency: Uint8Array | undefined;
if (webgl && mesh && transparencyType === 'volumeInstance') {
if (transparencyType === 'volumeInstance') {
const stride = isGeoTexture ? 4 : 3;
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
}
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
@@ -124,7 +123,7 @@ def Material "material${materialKey}"
// normal
for (let i = 0; i < vertexCount; ++i) {
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',(');
StringBuilder.writeFloat(normalBuilder, tmpV[0], 100);
StringBuilder.writeSafe(normalBuilder, ',');
@@ -134,7 +133,7 @@ def Material "material${materialKey}"
StringBuilder.writeSafe(normalBuilder, ')');
}
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
// face
for (let i = 0; i < drawCount; ++i) {

View File

@@ -1,187 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Testing examples for using mesh-extension.ts. */
import { CIF } from '../../mol-io/reader/cif';
import { Volume } from '../../mol-model/volume';
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector } from '../../mol-state';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import { ParamDefinition } from '../../mol-util/param-definition';
import { createMeshFromUrl } from './mesh-extension';
import { MeshServerInfo } from './mesh-streaming/server-info';
import { InitMeshStreaming } from './mesh-streaming/transformers';
import * as MeshUtils from './mesh-utils';
export const DB_URL = '/db'; // local
export async function runMeshExtensionExamples(plugin: PluginContext, db_url: string = DB_URL) {
console.time('TIME MESH EXAMPLES');
// await runIsosurfaceExample(plugin, db_url);
// await runMolsurfaceExample(plugin);
// Focused Ion Beam-Scanning Electron Microscopy of mitochondrial reticulum in murine skeletal muscle: https://www.ebi.ac.uk/empiar/EMPIAR-10070/
// await runMeshExample(plugin, 'all', db_url);
// await runMeshExample(plugin, 'fg', db_url);
// await runMultimeshExample(plugin, 'fg', 'worst', db_url);
// await runCifMeshExample(plugin);
// await runMeshExample2(plugin, 'fg');
await runMeshStreamingExample(plugin);
console.timeEnd('TIME MESH EXAMPLES');
}
/** Example for downloading multiple separate segments, each containing 1 mesh. */
export async function runMeshExample(plugin: PluginContext, segments: 'fg' | 'all', db_url: string = DB_URL) {
const detail = 2;
const segmentIds = (segments === 'all') ?
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
for (const segmentId of segmentIds) {
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-mesh-rounded/segment-${segmentId}/detail-${detail}`, segmentId, detail, true, undefined);
}
}
/** Example for downloading multiple separate segments, each containing 1 mesh. */
export async function runMeshExample2(plugin: PluginContext, segments: 'one' | 'few' | 'fg' | 'all') {
const detail = 1;
const segmentIds = (segments === 'one') ? [15]
: (segments === 'few') ? [1, 4, 7, 10, 16]
: (segments === 'all') ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
for (const segmentId of segmentIds) {
await createMeshFromUrl(plugin, `http://localhost:9000/v2/empiar/empiar-10070/mesh_bcif/${segmentId}/${detail}`, segmentId, detail, false, undefined);
}
}
/** Example for downloading a single segment containing multiple meshes. */
export async function runMultimeshExample(plugin: PluginContext, segments: 'fg' | 'all', detailChoice: 'best' | 'worst', db_url: string = DB_URL) {
const urlDetail = (detailChoice === 'best') ? '2' : 'worst';
const numDetail = (detailChoice === 'best') ? 2 : 1000;
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-multimesh-rounded/segments-${segments}/detail-${urlDetail}`, 0, numDetail, false, undefined);
}
export async function runMeshStreamingExample(plugin: PluginContext, source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070', serverUrl?: string, parent?: StateObjectSelector) {
const params = ParamDefinition.getDefaultValues(MeshServerInfo.Params);
if (serverUrl) params.serverUrl = serverUrl;
params.source = source;
params.entryId = entryId;
await plugin.runTask(plugin.state.data.applyAction(InitMeshStreaming, params, parent?.ref), { useOverlay: false });
}
/** Example for downloading a protein structure and visualizing molecular surface. */
export async function runMolsurfaceExample(plugin: PluginContext) {
const entryId = 'pdb-7etq';
// Node "https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif" ("transformer": "ms-plugin.download") -> var data
const data = await plugin.builders.data.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif', isBinary: true }, { state: { isGhost: false } });
console.log('formats:', plugin.dataFormats.list);
// Node "CIF File" ("transformer": "ms-plugin.parse-cif")
// Node "7ETQ 1 model" ("transformer": "ms-plugin.trajectory-from-mmcif") -> var trajectory
const parsed = await plugin.dataFormats.get('mmcif')!.parse(plugin, data, { entryId });
const trajectory: StateObjectSelector<PluginStateObject.Molecule.Trajectory> = parsed.trajectory;
console.log('parsed', parsed);
console.log('trajectory', trajectory);
// Node "Model 1" ("transformer": "ms-plugin.model-from-trajectory") -> var model
const model = await plugin.build().to(trajectory).apply(StateTransforms.Model.ModelFromTrajectory).commit();
console.log('model:', model);
// Node "Model 91 elements" ("transformer": "ms-plugin.structure-from-model") -> var structure
const structure = await plugin.build().to(model).apply(StateTransforms.Model.StructureFromModel,).commit();
console.log('structure:', structure);
// Node "Molecular Surface" ("transformer": "ms-plugin.structure-representation-3d") -> var repr
const reprParams = createStructureRepresentationParams(plugin, undefined, { type: 'molecular-surface' });
const repr = await plugin.build().to(structure).apply(StateTransforms.Representation.StructureRepresentation3D, reprParams).commit();
console.log('repr:', repr);
}
/** Example for downloading an EMDB density data and visualizing isosurface. */
export async function runIsosurfaceExample(plugin: PluginContext, db_url: string = DB_URL) {
const entryId = 'emd-1832';
const isoLevel = 2.73;
let root = await plugin.build();
const data = await plugin.builders.data.download({ url: `${db_url}/emd-1832-box`, isBinary: true }, { state: { isGhost: false } });
const parsed = await plugin.dataFormats.get('dscif')!.parse(plugin, data, { entryId });
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
const volumeData = volume.cell!.obj!.data;
console.log('data:', data);
console.log('parsed:', parsed);
console.log('volume:', volume);
console.log('volumeData:', volumeData);
root = await plugin.build();
console.log('root:', root);
console.log('to:', root.to(volume));
console.log('toRoot:', root.toRoot());
let volumeParams;
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
type: 'isosurface',
typeParams: {
alpha: 0.5,
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
visuals: ['solid'],
sizeFactor: 1,
},
color: 'uniform',
colorParams: { value: Color(0x00aaaa) },
});
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
type: 'isosurface',
typeParams: {
alpha: 1.0,
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
visuals: ['wireframe'],
sizeFactor: 1,
},
color: 'uniform',
colorParams: { value: Color(0x8800aa) },
});
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
await root.commit();
}
export async function runCifMeshExample(plugin: PluginContext, api: string = 'http://localhost:9000/v2',
source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070',
segmentId: number = 1, detail: number = 10,
) {
const url = `${api}/${source}/${entryId}/mesh_bcif/${segmentId}/${detail}`;
getMeshFromBcif(plugin, url);
}
async function getMeshFromBcif(plugin: PluginContext, url: string) {
const urlAsset = Asset.getUrlAsset(plugin.managers.asset, url);
const asset = await plugin.runTask(plugin.managers.asset.resolve(urlAsset, 'binary'));
const parsed = await plugin.runTask(CIF.parseBinary(asset.data));
if (parsed.isError) {
plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
return;
}
console.log('blocks:', parsed.result.blocks);
const mesh = await MeshUtils.meshFromCif(parsed.result);
console.log(mesh);
}

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Column, Database } from '../../mol-data/db';
import { CifFrame } from '../../mol-io/reader/cif';
import { toDatabase } from '../../mol-io/reader/cif/schema';
const int = Column.Schema.int;
const float = Column.Schema.float;
// TODO in future, move to molstar/src/mol-io/reader/cif/schema/mesh.ts
export const Mesh_Data_Schema = {
mesh: {
id: int,
},
mesh_vertex: {
mesh_id: int,
vertex_id: int,
x: float,
y: float,
z: float,
},
/** Table of triangles, 3 rows per triangle */
mesh_triangle: {
mesh_id: int,
/** Indices of vertices within mesh */
vertex_id: int,
}
};
export type Mesh_Data_Schema = typeof Mesh_Data_Schema;
export interface Mesh_Data_Database extends Database<Mesh_Data_Schema> {}
// TODO in future, move to molstar/src/mol-io/reader/cif.ts: CIF.schema.mesh
export const CIF_schema_mesh = (frame: CifFrame) => toDatabase<Mesh_Data_Schema, Mesh_Data_Database>(Mesh_Data_Schema, frame);

View File

@@ -1,223 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Defines new types of State tree transformers for dealing with mesh data. */
import { BaseGeometry, VisualQuality, VisualQualityOptions } from '../../mol-geo/geometry/base';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { CifFile } from '../../mol-io/reader/cif';
import { Box3D } from '../../mol-math/geometry';
import { Vec3 } from '../../mol-math/linear-algebra';
import { Shape } from '../../mol-model/shape';
import { ShapeProvider } from '../../mol-model/shape/provider';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { Download } from '../../mol-plugin-state/transforms/data';
import { ShapeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectRef, StateObjectSelector, StateTransformer } from '../../mol-state';
import { Task } from '../../mol-task';
import { Color } from '../../mol-util/color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import * as MeshUtils from './mesh-utils';
export const BACKGROUND_OPACITY = 0.2;
export const FOREROUND_OPACITY = 1;
export const VolsegTransform: StateTransformer.Builder.Root = StateTransformer.builderFactory('volseg');
// // // // // // // // // // // // // // // // // // // // // // // //
// Parsed data
/** Data type for `MeshlistStateObject` - list of meshes */
export interface MeshlistData {
segmentId: number,
segmentName: string,
detail: number,
meshIds: number[],
mesh: Mesh,
/** Reference to the object which created this meshlist (e.g. `MeshStreaming.Behavior`) */
ownerId?: string,
}
export namespace MeshlistData {
export function empty(): MeshlistData {
return {
segmentId: 0,
segmentName: 'Empty',
detail: 0,
meshIds: [],
mesh: Mesh.createEmpty(),
};
};
export async function fromCIF(data: CifFile, segmentId: number, segmentName: string, detail: number): Promise<MeshlistData> {
const { mesh, meshIds } = await MeshUtils.meshFromCif(data);
return {
segmentId,
segmentName,
detail,
meshIds,
mesh,
};
}
export function stats(meshListData: MeshlistData): string {
return `Meshlist "${meshListData.segmentName}" (detail ${meshListData.detail}): ${meshListData.meshIds.length} meshes, ${meshListData.mesh.vertexCount} vertices, ${meshListData.mesh.triangleCount} triangles`;
}
export function getShape(data: MeshlistData, color: Color): Shape<Mesh> {
const mesh = data.mesh;
const meshShape: Shape<Mesh> = Shape.create(data.segmentName, data, mesh,
() => color,
() => 1,
// group => `${data.segmentName} | Segment ${data.segmentId} | Detail ${data.detail} | Mesh ${group}`,
group => data.segmentName,
);
return meshShape;
}
export function combineBBoxes(boxes: (Box3D | null)[]): Box3D | null {
let result = null;
for (const box of boxes) {
if (!box) continue;
if (result) {
Vec3.min(result.min, result.min, box.min);
Vec3.max(result.max, result.max, box.max);
} else {
result = Box3D.zero();
Box3D.copy(result, box);
}
}
return result;
}
export function bbox(data: MeshlistData): Box3D | null {
return MeshUtils.bbox(data.mesh);
}
export function allVerticesUsed(data: MeshlistData): boolean {
const unusedVertices = new Set();
for (let i = 0; i < data.mesh.vertexCount; i++) {
unusedVertices.add(i);
}
for (let i = 0; i < 3 * data.mesh.triangleCount; i++) {
const v = data.mesh.vertexBuffer.ref.value[i];
unusedVertices.delete(v);
}
return unusedVertices.size === 0;
}
}
// // // // // // // // // // // // // // // // // // // // // // // //
// Raw Data -> Parsed data
export class MeshlistStateObject extends PluginStateObject.Create<MeshlistData>({ name: 'Parsed Meshlist', typeClass: 'Object' }) { }
export const ParseMeshlistTransformer = VolsegTransform({
name: 'meshlist-from-string',
from: PluginStateObject.Format.Cif,
to: MeshlistStateObject,
params: {
label: PD.Text(MeshlistStateObject.type.name, { isHidden: true }),
segmentId: PD.Numeric(1, {}, { isHidden: true }),
segmentName: PD.Text('Segment'),
detail: PD.Numeric(1, {}, { isHidden: true }),
/** Reference to the object which manages this meshlist (e.g. `MeshStreaming.Behavior`) */
ownerId: PD.Text('', { isHidden: true }),
}
})({
apply({ a, params }, globalCtx) { // `a` is the parent node, params are 2nd argument to To.apply(), `globalCtx` is the plugin
return Task.create('Create Parsed Meshlist', async ctx => {
const meshlistData = await MeshlistData.fromCIF(a.data, params.segmentId, params.segmentName, params.detail);
meshlistData.ownerId = params.ownerId;
const es = meshlistData.meshIds.length === 1 ? '' : 'es';
return new MeshlistStateObject(meshlistData, { label: params.label, description: `${meshlistData.segmentName} (${meshlistData.meshIds.length} mesh${es})` });
});
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
// Parsed data -> Shape
/** Data type for PluginStateObject.Shape.Provider */
type MeshShapeProvider = ShapeProvider<MeshlistData, Mesh, Mesh.Params>;
namespace MeshShapeProvider {
export function fromMeshlistData(meshlist: MeshlistData, color?: Color): MeshShapeProvider {
const theColor = color ?? MeshUtils.ColorGenerator.next().value;
return {
label: 'Mesh',
data: meshlist,
params: meshShapeProviderParams,
geometryUtils: Mesh.Utils,
getShape: (ctx, data: MeshlistData) => MeshlistData.getShape(data, theColor),
};
}
}
const meshShapeProviderParams: Mesh.Params = {
...Mesh.Params,
quality: PD.Select<VisualQuality>('custom', VisualQualityOptions, { isEssential: true, description: 'Visual/rendering quality of the representation.' }), // use 'custom' when wanting to apply doubleSided
doubleSided: PD.Boolean(true, BaseGeometry.CustomQualityParamInfo),
// set `flatShaded`: true to see the real mesh vertices and triangles
transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory), // 'on' means: show backfaces with correct opacity, even when opacity < 1 (requires doubleSided) ¯\_(ツ)_/¯
};
export const MeshShapeTransformer = VolsegTransform({
name: 'shape-from-meshlist',
display: { name: 'Shape from Meshlist', description: 'Create Shape from Meshlist data' },
from: MeshlistStateObject,
to: PluginStateObject.Shape.Provider,
params: {
color: PD.Value<Color | undefined>(undefined), // undefined means random color
},
})({
apply({ a, params }) {
const shapeProvider = MeshShapeProvider.fromMeshlistData(a.data, params.color);
return new PluginStateObject.Shape.Provider(shapeProvider, { label: PluginStateObject.Shape.Provider.type.name, description: a.description });
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
/** Download data and create state tree hierarchy down to visual representation. */
export async function createMeshFromUrl(plugin: PluginContext, meshDataUrl: string, segmentId: number, detail: number,
collapseTree: boolean, color?: Color, parent?: StateObjectSelector | StateObjectRef, transparentIfBboxAbove?: number,
name?: string, ownerId?: string) {
const update = parent ? plugin.build().to(parent) : plugin.build().toRoot();
const rawDataNodeRef = update.apply(Download,
{ url: meshDataUrl, isBinary: true, label: `Downloaded Data ${segmentId}` },
{ state: { isCollapsed: collapseTree } }
).ref;
const parsedDataNode = await update.to(rawDataNodeRef)
.apply(StateTransforms.Data.ParseCif)
.apply(ParseMeshlistTransformer,
{ label: undefined, segmentId: segmentId, segmentName: name ?? `Segment ${segmentId}`, detail: detail, ownerId: ownerId },
{}
)
.commit();
let transparent = false;
if (transparentIfBboxAbove !== undefined && parsedDataNode.data) {
const bbox = MeshlistData.bbox(parsedDataNode.data) || Box3D.zero();
transparent = Box3D.volume(bbox) > transparentIfBboxAbove;
}
await plugin.build().to(parsedDataNode)
.apply(MeshShapeTransformer, { color: color },)
.apply(ShapeRepresentation3D,
{ alpha: transparent ? BACKGROUND_OPACITY : FOREROUND_OPACITY },
{ tags: ['mesh-segment-visual', `segment-${segmentId}`] }
)
.commit();
return rawDataNodeRef;
}

View File

@@ -1,332 +0,0 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { distinctUntilChanged, map } from 'rxjs';
import { CIF } from '../../../mol-io/reader/cif';
import { Box3D } from '../../../mol-math/geometry';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginBehavior } from '../../../mol-plugin/behavior';
import { PluginCommand } from '../../../mol-plugin/command';
import { PluginCommands } from '../../../mol-plugin/commands';
import { PluginContext } from '../../../mol-plugin/context';
import { UUID } from '../../../mol-util';
import { Asset } from '../../../mol-util/assets';
import { Color } from '../../../mol-util/color';
import { ColorNames } from '../../../mol-util/color/names';
import { Choice } from '../../../mol-util/param-choice';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { MetadataWrapper } from '../../volumes-and-segmentations/volseg-api/utils';
import { MeshlistData } from '../mesh-extension';
import { MeshServerInfo } from './server-info';
const DEFAULT_SEGMENT_NAME = 'Untitled segment';
const DEFAULT_SEGMENT_COLOR = ColorNames.lightgray;
export const NO_SEGMENT = -1;
/** Maximum (worst) detail level available in GUI (TODO set actual maximum possible value) */
const MAX_DETAIL = 10;
const DEFAULT_DETAIL = 7; // TODO decide a reasonable default
/** Segments whose bounding box volume is above this value (relative to the overall bounding box) are considered as background segments */
export const BACKGROUND_SEGMENT_VOLUME_THRESHOLD = 0.5;
export class MeshStreaming extends PluginStateObject.CreateBehavior<MeshStreaming.Behavior>({ name: 'Mesh Streaming' }) { }
export namespace MeshStreaming {
export namespace Params {
export const ViewTypeChoice = new Choice({ off: 'Off', select: 'Select', all: 'All' }, 'select'); // TODO add camera target?
export type ViewType = Choice.Values<typeof ViewTypeChoice>;
export function create(options: MeshServerInfo.Data) {
return {
view: PD.MappedStatic('select', {
'off': PD.Group({}),
'select': PD.Group({
baseDetail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the non-selected segments (lower number = better)' }),
focusDetail: PD.Numeric(1, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the selected segment (lower number = better)' }),
selectedSegment: PD.Numeric(NO_SEGMENT, {}, { isHidden: true }),
}, { isFlat: true }),
'all': PD.Group({
detail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for all segments (lower number = better)' })
}, { isFlat: true }),
}, { description: '"Off" hides all segments. \n"Select" shows all segments in lower detail, clicked segment in better detail. "All" shows all segment in the same level.' }),
};
}
export type Definition = ReturnType<typeof create>
export type Values = PD.Values<Definition>
export function copyValues(params: Values): Values {
return {
view: {
name: params.view.name,
params: { ...params.view.params } as any,
}
};
}
export function valuesEqual(p: Values, q: Values): boolean {
if (p.view.name !== q.view.name) return false;
for (const key in p.view.params) {
if ((p.view.params as any)[key] !== (q.view.params as any)[key]) return false;
}
return true;
}
export function detailsEqual(p: Values, q: Values): boolean {
switch (p.view.name) {
case 'off':
return q.view.name === 'off';
case 'select':
return q.view.name === 'select' && p.view.params.baseDetail === q.view.params.baseDetail && p.view.params.focusDetail === q.view.params.focusDetail;
case 'all':
return q.view.name === 'all' && p.view.params.detail === q.view.params.detail;
default:
throw new Error('Not implemented');
}
}
}
export interface VisualInfo {
tag: string, // e.g. high-2, low-1 // ? remove if can be omitted
segmentId: number, // ? remove if unused
segmentName: string, // ? remove if unused
detailType: VisualInfo.DetailType, // ? remove if unused
detail: number, // ? remove if unused
color: Color, // move to MeshlistData?
visible: boolean,
data?: MeshlistData,
}
export namespace VisualInfo {
export type DetailType = 'low' | 'high';
export const DetailTypes: DetailType[] = ['low', 'high'];
export function tagFor(segmentId: number, detail: DetailType) {
return `${detail}-${segmentId}`;
}
}
export class Behavior extends PluginBehavior.WithSubscribers<Params.Values> {
private id: string;
private ref: string = '';
public parentData: MeshServerInfo.Data;
private metadata?: MetadataWrapper;
public visuals?: { [tag: string]: VisualInfo };
public backgroundSegments: { [segmentId: number]: boolean } = {};
private focusObservable = this.plugin.behaviors.interaction.click.pipe( // QUESTION is this OK way to get focused segment?
map(evt => evt.current.loci),
map(loci => (loci.kind === 'group-loci') ? loci.shape.sourceData as MeshlistData : null),
map(data => (data?.ownerId === this.id) ? data : null), // do not process shapes created by others
distinctUntilChanged((old, current) => old?.segmentId === current?.segmentId),
);
private focusSubscription?: PluginCommand.Subscription = undefined;
private backgroundSegmentsInitialized = false;
constructor(plugin: PluginContext, data: MeshServerInfo.Data, params: Params.Values) {
super(plugin, params);
this.id = UUID.create22();
this.parentData = data;
}
register(ref: string): void {
this.ref = ref;
}
unregister(): void {
if (this.focusSubscription) {
this.focusSubscription.unsubscribe();
this.focusSubscription = undefined;
}
// TODO empty cache here (if used)
}
selectSegment(segmentId: number) {
if (this.params.view.name === 'select') {
if (this.params.view.params.selectedSegment === segmentId) return;
const newParams = Params.copyValues(this.params);
if (newParams.view.name === 'select') {
newParams.view.params.selectedSegment = segmentId;
}
const state = this.plugin.state.data;
const update = state.build().to(this.ref).update(newParams);
PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
}
}
async update(params: Params.Values) {
const oldParams = this.params;
this.params = params;
if (!this.metadata) {
const response = await fetch(this.getMetadataUrl());
const rawMetadata = await response.json();
this.metadata = new MetadataWrapper(rawMetadata);
}
if (!this.visuals) {
this.initVisualInfos();
} else if (!Params.detailsEqual(this.params, oldParams)) {
this.updateVisualInfoDetails();
}
switch (params.view.name) {
case 'off':
await this.disableVisuals();
break;
case 'select':
await this.enableVisuals(params.view.params.selectedSegment);
break;
case 'all':
await this.enableVisuals();
break;
default:
throw new Error('Not implemented');
}
if (params.view.name !== 'off' && !this.backgroundSegmentsInitialized) {
this.guessBackgroundSegments();
this.backgroundSegmentsInitialized = true;
}
if (params.view.name === 'select' && !this.focusSubscription) {
this.focusSubscription = this.subscribeObservable(this.focusObservable, data => { this.selectSegment(data?.segmentId ?? NO_SEGMENT); });
} else if (params.view.name !== 'select' && this.focusSubscription) {
this.focusSubscription.unsubscribe();
this.focusSubscription = undefined;
}
return true;
}
private getMetadataUrl() {
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/metadata`;
}
private getMeshUrl(segment: number, detail: number) {
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/mesh_bcif/${segment}/${detail}`;
}
private initVisualInfos() {
const visuals: { [tag: string]: VisualInfo } = {};
for (const segid of this.metadata!.meshSegmentIds) {
const name = this.metadata?.getSegment(segid)?.biological_annotation.name ?? DEFAULT_SEGMENT_NAME;
const color = this.metadata?.getSegmentColor(segid) ?? DEFAULT_SEGMENT_COLOR;
for (const detailType of VisualInfo.DetailTypes) {
const visual: VisualInfo = {
tag: VisualInfo.tagFor(segid, detailType),
segmentId: segid,
segmentName: name,
detailType: detailType,
detail: -1, // to be set at the end
color: color,
visible: false,
data: undefined,
};
visuals[visual.tag] = visual;
}
}
this.visuals = visuals;
this.updateVisualInfoDetails();
}
private updateVisualInfoDetails() {
let highDetail: number | undefined;
let lowDetail: number | undefined;
switch (this.params.view.name) {
case 'off':
lowDetail = undefined;
highDetail = undefined;
break;
case 'select':
lowDetail = this.params.view.params.baseDetail;
highDetail = this.params.view.params.focusDetail;
break;
case 'all':
lowDetail = this.params.view.params.detail;
highDetail = undefined;
break;
}
for (const tag in this.visuals) {
const visual = this.visuals[tag];
const preferredDetail = (visual.detailType === 'high') ? highDetail : lowDetail;
if (preferredDetail !== undefined) {
visual.detail = this.metadata!.getSufficientMeshDetail(visual.segmentId, preferredDetail);
}
}
}
private async enableVisuals(highDetailSegment?: number) {
for (const tag in this.visuals) {
const visual = this.visuals[tag];
const requiredDetailType = visual.segmentId === highDetailSegment ? 'high' : 'low';
if (visual.detailType === requiredDetailType) {
visual.data = await this.getMeshData(visual);
visual.visible = true;
} else {
visual.visible = false;
}
}
}
private async disableVisuals() {
for (const tag in this.visuals) {
const visual = this.visuals[tag];
visual.visible = false;
}
}
/** Fetch data in current `visual.detail`, or return already fetched data (if available in the correct detail). */
private async getMeshData(visual: VisualInfo): Promise<MeshlistData> {
if (visual.data && visual.data.detail === visual.detail) {
// Do not recreate
return visual.data;
}
// TODO cache
const url = this.getMeshUrl(visual.segmentId, visual.detail);
const urlAsset = Asset.getUrlAsset(this.plugin.managers.asset, url);
const asset = await this.plugin.runTask(this.plugin.managers.asset.resolve(urlAsset, 'binary'));
const parsed = await this.plugin.runTask(CIF.parseBinary(asset.data));
if (parsed.isError) {
throw new Error(`Failed parsing CIF file from ${url}`);
}
const meshlistData = await MeshlistData.fromCIF(parsed.result, visual.segmentId, visual.segmentName, visual.detail);
meshlistData.ownerId = this.id;
// const bbox = MeshlistData.bbox(meshlistData);
// const bboxVolume = bbox ? MS.Box3D.volume(bbox) : 0.0;
// console.log(`BBox ${visual.segmentId}: ${Math.round(bboxVolume! / 1e6)} M`, bbox); // DEBUG
return meshlistData;
}
private async guessBackgroundSegments() {
const bboxes: { [segid: number]: Box3D } = {};
for (const tag in this.visuals) {
const visual = this.visuals[tag];
if (visual.detailType === 'low' && visual.data) {
const bbox = MeshlistData.bbox(visual.data);
if (bbox) {
bboxes[visual.segmentId] = bbox;
}
}
}
const totalBbox = MeshlistData.combineBBoxes(Object.values(bboxes));
const totalVolume = totalBbox ? Box3D.volume(totalBbox) : 0.0;
// console.log(`BBox total: ${Math.round(totalVolume! / 1e6)} M`, totalBbox); // DEBUG
const isBgSegment: { [segid: number]: boolean } = {};
for (const segid in bboxes) {
const bbox = bboxes[segid];
const bboxVolume = Box3D.volume(bbox);
isBgSegment[segid] = (bboxVolume > totalVolume * BACKGROUND_SEGMENT_VOLUME_THRESHOLD);
// console.log(`BBox ${segid}: ${Math.round(bboxVolume! / 1e6)} M, ${bboxVolume / totalVolume}`, bbox); // DEBUG
}
this.backgroundSegments = isBgSegment;
}
getDescription() {
return Params.ViewTypeChoice.prettyName(this.params.view.name);
}
}
}

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