mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 14:04:36 +08:00
Compare commits
103 Commits
v5.0.0-dev
...
v5.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc33e44dc | ||
|
|
b0aa889a0a | ||
|
|
4d7bd53231 | ||
|
|
c11cf665c9 | ||
|
|
a4b09d3a0c | ||
|
|
6e488b0f80 | ||
|
|
6164281a50 | ||
|
|
2db7171e2a | ||
|
|
edfc094952 | ||
|
|
b3e1e2900b | ||
|
|
1e498d535a | ||
|
|
6ed969cd1b | ||
|
|
27bb4f4bca | ||
|
|
6ce2139272 | ||
|
|
13cf6613a6 | ||
|
|
c5bb13e295 | ||
|
|
34c8257848 | ||
|
|
fcbf39c935 | ||
|
|
46c8150b2b | ||
|
|
af1a864daa | ||
|
|
3babd9399a | ||
|
|
e57564486f | ||
|
|
464a91ac29 | ||
|
|
27fa50a5de | ||
|
|
1e323f18f7 | ||
|
|
2685b2b77d | ||
|
|
d71b47a515 | ||
|
|
88cc720dd2 | ||
|
|
201433cc91 | ||
|
|
8582303491 | ||
|
|
655c3edadd | ||
|
|
a4323a4bd8 | ||
|
|
1b5a7d9546 | ||
|
|
f165cc4629 | ||
|
|
db247d6fbd | ||
|
|
138796862b | ||
|
|
1b236f1ae5 | ||
|
|
b6c2e25395 | ||
|
|
b7816986aa | ||
|
|
437c70a75a | ||
|
|
de85e0fbae | ||
|
|
c527b59782 | ||
|
|
3bbbac66c7 | ||
|
|
c0980bf18a | ||
|
|
45eab19493 | ||
|
|
1e2a5a5bfd | ||
|
|
45edfa8014 | ||
|
|
899203c855 | ||
|
|
ef823b066b | ||
|
|
33dc2015df | ||
|
|
fcf5ea420b | ||
|
|
8d97327f8d | ||
|
|
abc7ebba3e | ||
|
|
73d593907e | ||
|
|
0dc05e1138 | ||
|
|
dd11cacae4 | ||
|
|
b503259758 | ||
|
|
1e98741e16 | ||
|
|
f879519700 | ||
|
|
c6e175e5da | ||
|
|
add75bf9c9 | ||
|
|
57cbcd5fbf | ||
|
|
50a820b0ae | ||
|
|
0a33936e06 | ||
|
|
7291025e09 | ||
|
|
0cb2c3621b | ||
|
|
86da258280 | ||
|
|
477a80d1ca | ||
|
|
86b68018a9 | ||
|
|
da095d6ef9 | ||
|
|
dc304b9e08 | ||
|
|
c905fa17c4 | ||
|
|
a06c64e8e0 | ||
|
|
f5441290dd | ||
|
|
9f23124317 | ||
|
|
8299cd638c | ||
|
|
50cb08e74d | ||
|
|
89552652ba | ||
|
|
37ce577813 | ||
|
|
4d9a003141 | ||
|
|
6f0311a53f | ||
|
|
bfd2d6b055 | ||
|
|
3072e60709 | ||
|
|
62ed8d10e3 | ||
|
|
13d3c34864 | ||
|
|
cac433efca | ||
|
|
b25ffe7151 | ||
|
|
31074dc74c | ||
|
|
c98c01a076 | ||
|
|
8966fc9396 | ||
|
|
fdbdc551e8 | ||
|
|
bb232ac3a4 | ||
|
|
735c25ef8d | ||
|
|
298043313a | ||
|
|
77cd181b91 | ||
|
|
b5bee042e8 | ||
|
|
4faf17ddc7 | ||
|
|
28774b2277 | ||
|
|
6a7444f44e | ||
|
|
15bfa8416a | ||
|
|
e6895ec833 | ||
|
|
2099ad728a | ||
|
|
6fc04c3294 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -5,12 +5,15 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
- [Breaking] Renamed some color schemes ('inferno' -> 'inferno-no-black', 'magma' -> 'magma-no-black', 'turbo' -> 'turbo-no-black', 'rainbow' -> 'simple-rainbow')
|
||||
- [Breaking] `Box3D.nearestIntersectionWithRay` -> `nearestIntersectionWithRay3D` (use `Ray3D`)
|
||||
- [Breaking] `Box3D.nearestIntersectionWithRay` -> `Ray3D.intersectBox3D`
|
||||
- [Breaking] `Plane3D.distanceToSpher3D` -> `distanceToSphere3D` (fix spelling)
|
||||
- [Breaking] fix typo `MarchinCubes` -> `MarchingCubes`
|
||||
- [Breaking] `PluginContext.initViewer/initContainer/mount` are now async and have been renamed to include `Async` postfix
|
||||
- [Breaking] Add `Volume.instances` support and a `VolumeInstances` transform to dynamically assign it
|
||||
- This change is breaking because all volume objects require the `instances` field now.
|
||||
- [Breaking] `Canvas3D.identify` now expects `Vec2` or `Ray3D`
|
||||
- [Breaking] `TrackballControlsParams.animate.spin.speed` now means "Number of rotations per second" instead of "radians per second"
|
||||
- [Breaking] `PluginStateSnapshotManager.play` now accepts an options object instead of a single boolean value
|
||||
- Update production build to use `esbuild`
|
||||
- Emit explicit paths in `import`s in `lib/`
|
||||
- Fix outlines on opaque elements using illumination mode
|
||||
@@ -18,24 +21,39 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- MolViewSpec extension:
|
||||
- Generic color schemes (`palette` parameter for color_from_* nodes)
|
||||
- Annotation field remapping (`field_remapping` parameter for color_from_* nodes)
|
||||
- Representation node: support custom property `molstar_reprepresentation_params`,
|
||||
- Canvas node: support custom properties `molstar_enable_outline`, `molstar_enable_shadow`, `molstar_enable_ssao`
|
||||
- `representation` node: support custom property `molstar_representation_params`
|
||||
- Add `backbone` and `line` representation types
|
||||
- `primitives` node: support custom property `molstar_mesh/label/line_params`
|
||||
- `canvas` node: support custom property `molstar_postprocessing` with the ability to customize outline, depth of field, bloom, shadow, occlusion (SSAO), and fog
|
||||
- `clip` node support for structure and volume representations
|
||||
- `grid_slice` representation support for volumes
|
||||
- Support tethers and background for primitive labels
|
||||
- Support `snapshot_key` parameter on primitives that enables transition between states via clicking on 3D objects
|
||||
- Inline selectors and MVS annotations support `instance_id`
|
||||
- Support `matrix` on transform params
|
||||
- Support `surface_type` (`molecular` / `gaussian`) on for `surface` representation nodes
|
||||
- Add `instance` node type
|
||||
- Add `transform.rotation_center` property that enables rotating an object around its centroid or a specific point
|
||||
- Support transforming and instancing of structures, components, and volumes
|
||||
- Use params hash for node version for more performant tree diffs
|
||||
- Add `Snapshot.animation` support that enables animating almost every property in a given tree
|
||||
- Add `createMVSX` helper function
|
||||
- Support Mol* trackball animation via `animation.custom.molstar_trackball`
|
||||
- MVSX - use Murmur hash instead of FNV in archive URI
|
||||
- Support additional file formats (pdbqt, gro, xyz, mol, sdf, mol2, xtc, lammpstrj)
|
||||
- Support loading trajectory coordinates from separate nodes
|
||||
- Trigger markdown commands from primitives using `molstar_markdown_commands` custom extensions
|
||||
- Support `molstar_on_load_markdown_commands` custom state on the `root` node
|
||||
- Added new color schemes, synchronized with D3.js ('inferno', 'magma', 'turbo', 'rainbow', 'sinebow', 'warm', 'cool', 'cubehelix-default', 'category-10', 'observable-10', 'tableau-10')
|
||||
- Snapshot Markdown improvements
|
||||
- Add `MarkdownExtensionManager` (`PluginContext.managers.markdownExtensions`)
|
||||
- Support custom markdown commands to control the plugin via the `[link](!command)` pattern
|
||||
- Support rendering custom elements via the `` pattern
|
||||
- Support tables
|
||||
- Support loading images from MVSX files
|
||||
- Support loading images and audio from MVSX files
|
||||
- Indicate external links with ⤴
|
||||
- Audio support
|
||||
- Add `PluginState.Snapshot.onLoadMarkdownCommands`
|
||||
- Avoid calculating rings for coarse-grained structures
|
||||
- Fix isosurface compute shader normals when transformation matrix is applied to volume
|
||||
- Symmetry operator naming for spacegroup symmetry - parenthesize multi-character indices (1_111-1 -> 1_(11)1(-1))
|
||||
@@ -49,7 +67,39 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Add `Ray3D` object and helpers
|
||||
- Volume slice representation: add `relativeX/Y/Z` options for dimension
|
||||
- Add `StructureInstances` transform
|
||||
- Add `story-id` URL arg support to `mvs-stories` app
|
||||
- `mvs-stories` app
|
||||
- Add `story-id` URL arg support
|
||||
- Add `story-session-url` URL arg support
|
||||
- Add "Download MVS State" link
|
||||
- Add "Open in Mol*" link
|
||||
- Add "Edit in MolViewStories" link for story states
|
||||
- Add ray-based picking
|
||||
- Render narrow view of scene scene from ray origin & direction to a few pixel sized viewport
|
||||
- Cast ray on every input as opposed to the standard "whole screen" picking
|
||||
- Can be enabled with new `Canvas3dInteractionHelperParams.convertCoordsToRay` param
|
||||
- Allows to have input methods that are 3D pointers in the scene
|
||||
- Add `ray: Ray3D` property to `DragInput`, `ClickInput`, and `MoveInput`
|
||||
- Add async, non-blocking picking (only WebGL2)
|
||||
- Refactor `Canvas3dInteractionHelper` internals to use async picking for move events
|
||||
- Add `enable` param for post-processing effects. If false, no effects are applied.
|
||||
- Dot volume representation improvements
|
||||
- Add positional perturbation to avoid camera artifacts
|
||||
- Fix handling of negative isoValues by considering only volume cells with values lower than isoValue (#1559)
|
||||
- Fix volume-value size theme
|
||||
- Change the parsing of residue names in PDB files from 3-letter to 4-letter.
|
||||
- Support versioning transform using a hash function in `mol-state`
|
||||
- Support for "state snapshot transitions"
|
||||
- Add `PluginState.Snapshot.transition` that enables associating a state snapshot with a list states that can be animated
|
||||
- Add `AnimateStateSnapshotTransition` animation
|
||||
- Update the snapshots UI to support this feature
|
||||
- Use "proper time" in the animation loop to prevent animation skips during blocking operations (e.g., shader complication)
|
||||
- Add `Hsl` and (normalized) `Rgb` color spaces
|
||||
- Add `Color.interpolateHsl`
|
||||
- Add `rotationCenter` property to `TransformParam`
|
||||
- Add Monolayer transparency (exploiting dpoit).
|
||||
- Add plugin config item ShowReset (shows/hides "Reset Zoom" button)
|
||||
- Fix transform params not being normalized when used together with param hash version
|
||||
- Replace `immer` with `mutative`
|
||||
|
||||
## [v4.18.0] - 2025-06-08
|
||||
- MolViewSpec extension:
|
||||
|
||||
@@ -14,12 +14,25 @@ The main use case of this is enriching [MolViewSpec](`https://molstar.org/mol-vi
|
||||
|
||||
Extends Markdown Hyperlink syntax to support expressions of the form `[title](!c1=v1&c2=v2&...)` into an executable command. The command can be executed either on click, mouse enter, or mouse leave.
|
||||
|
||||
Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `encodeURIComponent`, in Python `urllib.parse.quote_plus/urlencode`).
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
- `center-camera` - Centers the camera
|
||||
- `apply-snapshot=key` - Loads snapshots with the provided key
|
||||
- `next-snapshot[=-1|1]` - Loads next/previous snapshot, the direction is optional and default to `1`
|
||||
- `play-snapshots` - Starts playback of state snapshots
|
||||
- `play-transition` - Plays an animation associated with the given snapshot
|
||||
- `stop-animation` - Stops currently playing animation
|
||||
- `focus-refs=ref1,ref2,...` - On click, focuses nodes with the provided refs
|
||||
- `highlight-refs=ref1,ref2,...` - On mouse over, highlights the provided refs
|
||||
- `query=...&lang=...&action=highlight,focus&focus-radius=...`
|
||||
- `query` is an expression (e.g., `resn HEM` when using PyMol syntax)
|
||||
- (optional) `lang` is one of `mol-script` (default), `pymol`, `vmd`, `jmol`
|
||||
- (optional) `action` is an array of `highlight` (default), `focus` (multiple actions can be specified)
|
||||
- (optional) `focus-radius` is extra distance applied when focusing the selection (default is `3`)
|
||||
- Example: `[HEM](!query=resn%20HEM%26lang=pymol&action=highlight,focus)` highlights or focuses the HEM residue (the query must be URL encoded because it contains spaces and possibly other special characters)
|
||||
- `play-audio=src`, `toggle-audio[=src]`, `stop-audio`, `pause-audio`, `dispose-audio` - Audio playback support
|
||||
|
||||
## Custom Content
|
||||
|
||||
@@ -28,11 +41,11 @@ Extends Markdown Image syntax to support expressions of the form `
|
||||
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
|
||||
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
|
||||
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
|
||||
- `color-palette-discrete` - Renders discrete color list instead of interpolating
|
||||
- `color-palette-name=name` - Renders a gradient with the provided named color palette (see `mol-util/color/lists.ts` for supported color schemes)
|
||||
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
|
||||
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
|
||||
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
|
||||
- `color-palette-discrete` - Renders discrete color list instead of interpolating
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
@@ -39,6 +39,7 @@ export default defineConfig([{
|
||||
"comma-spacing": "off",
|
||||
"space-infix-ops": "off",
|
||||
"comma-dangle": "off",
|
||||
quotes: ["warn", "single", { "allowTemplateLiterals": true, "avoidEscape": true }],
|
||||
eqeqeq: ["error", "smart"],
|
||||
"import/order": "off",
|
||||
"no-eval": "warn",
|
||||
|
||||
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
Binary file not shown.
2838
package-lock.json
generated
2838
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.0",
|
||||
"version": "5.0.0-dev.10",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -121,7 +121,8 @@
|
||||
"Andy Turner <agdturner@gmail.com>",
|
||||
"Lukáš Polák <admin@lukaspolak.cz>",
|
||||
"Chetan Mishra <chetan.s115@gmail.com>",
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>"
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -165,9 +166,9 @@
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^5.1.2",
|
||||
"io-ts": "^2.2.22",
|
||||
"mutative": "^1.2.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
|
||||
const git = require('simple-git');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
const argparse = require('argparse');
|
||||
|
||||
const VERSION = require(path.resolve(__dirname, '../package.json')).version;
|
||||
const MVS_STORIES_VERSION = require(path.resolve(__dirname, '../src/apps/mvs-stories/version.ts')).VERSION;
|
||||
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git";
|
||||
const remoteUrl = 'https://github.com/molstar/molstar.github.io.git';
|
||||
const dataDir = path.resolve(__dirname, '../data/');
|
||||
const buildDir = path.resolve(__dirname, '../build/');
|
||||
const deployDir = path.resolve(__dirname, '../deploy/');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -147,6 +147,7 @@ export class MesoscaleExplorer {
|
||||
behaviors: [
|
||||
PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
|
||||
PluginSpec.Behavior(PluginBehaviors.Camera.CameraControls),
|
||||
PluginSpec.Behavior(PluginBehaviors.State.SnapshotControls),
|
||||
|
||||
PluginSpec.Behavior(MesoFocusLoci),
|
||||
PluginSpec.Behavior(MesoSelectLoci),
|
||||
@@ -261,7 +262,6 @@ export class MesoscaleExplorer {
|
||||
image: true,
|
||||
componentManager: false,
|
||||
structureSelection: true,
|
||||
behavior: true,
|
||||
});
|
||||
|
||||
plugin.managers.lociLabels.clearProviders();
|
||||
|
||||
@@ -16,6 +16,7 @@ export class MVSStoriesContext {
|
||||
commands = new BehaviorSubject<MVSStoriesCommand | undefined>(undefined);
|
||||
state = {
|
||||
viewers: new BehaviorSubject<{ name?: string, model: MVSStoriesViewerModel }[]>([]),
|
||||
currentStoryData: new BehaviorSubject<string | Uint8Array | undefined>(undefined),
|
||||
isLoading: new BehaviorSubject(false),
|
||||
};
|
||||
|
||||
@@ -27,7 +28,7 @@ export class MVSStoriesContext {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMVSStoriesContext(options?: { name?: string, container?: object }) {
|
||||
export function getMVSStoriesContext(options?: { name?: string, container?: object }): MVSStoriesContext {
|
||||
const container: any = options?.container ?? window;
|
||||
container.componentContexts ??= {};
|
||||
const name = options?.name ?? '<default>';
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
import { MolViewSpec } from '../../../extensions/mvs/behavior';
|
||||
import { loadMVSData } from '../../../extensions/mvs/components/formats';
|
||||
import { MVSData } from '../../../extensions/mvs/mvs-data';
|
||||
import { StringLike } from '../../../mol-io/common/string-like';
|
||||
import { PluginComponent } from '../../../mol-plugin-state/component';
|
||||
import { createPluginUI } from '../../../mol-plugin-ui';
|
||||
import { renderReact18 } from '../../../mol-plugin-ui/react18';
|
||||
@@ -56,11 +58,17 @@ export class MVSStoriesViewerModel extends PluginComponent {
|
||||
try {
|
||||
this.context.state.isLoading.next(true);
|
||||
if (cmd.kind === 'load-mvs') {
|
||||
let loadedData: MVSData | StringLike | Uint8Array | undefined;
|
||||
if (cmd.url) {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url: cmd.url, type: cmd.format === 'mvsx' ? 'binary' : 'string' }));
|
||||
await loadMVSData(this.plugin, data, cmd.format ?? 'mvsj', { sourceUrl: cmd.url });
|
||||
loadedData = await loadMVSData(this.plugin, data, cmd.format ?? 'mvsj', { sourceUrl: cmd.url });
|
||||
} else if (cmd.data) {
|
||||
await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
|
||||
loadedData = await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
|
||||
}
|
||||
if (StringLike.is(loadedData) || loadedData instanceof Uint8Array) {
|
||||
this.context.state.currentStoryData.next(loadedData as string | Uint8Array);
|
||||
} else if (loadedData) {
|
||||
this.context.state.currentStoryData.next(JSON.stringify(loadedData));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -38,6 +38,25 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#links {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 8px;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 0.6rem;
|
||||
z-index: -1;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#links a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#links .sep {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
#viewer {
|
||||
position: absolute;
|
||||
@@ -68,16 +87,23 @@
|
||||
<body>
|
||||
<!-- the context-name parameter is optional and useful when embedding multiple stories in a single page -->
|
||||
<div id="viewer">
|
||||
<mvs-stories-viewer context-name="story1" />
|
||||
<mvs-stories-viewer context-name="story1" ></mvs-stories-viewer>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<mvs-stories-snapshot-markdown context-name="story1" style="flex-grow: 1;" />
|
||||
<mvs-stories-snapshot-markdown context-name="story1" style="flex-grow: 1;" ></mvs-stories-snapshot-markdown>
|
||||
</div>
|
||||
|
||||
<div id="links">
|
||||
<span id="open-in-stories"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a> <span class="sep">•</span></span>
|
||||
<span id="open-in-molstar"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a> <span class="sep">•</span></span>
|
||||
<a href="#" id="mvs-data" title="MolViewSpec State for this story. Can be opened in the Mol* app.">Download MVS</a> <span class="sep">•</span> <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var storyId = urlParams.get('story-id');
|
||||
var storyUrl = urlParams.get('story-url');
|
||||
var storySessionUrl = urlParams.get('story-session-url');
|
||||
var format = urlParams.get('data-format');
|
||||
|
||||
// For testing purposes:
|
||||
@@ -85,11 +111,36 @@
|
||||
// storyUrl = 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/kinase-story.mvsj';
|
||||
// }
|
||||
|
||||
var molstarDataLink = storyUrl;
|
||||
var editInStoriesUrl = undefined;
|
||||
|
||||
if (storyId) {
|
||||
mvsStories.loadFromID(storyId, { contextName: 'story1' });
|
||||
mvsStories.loadFromID(storyId, { format: format || 'mvsj', contextName: 'story1' });
|
||||
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?published-session-id=' + storyId;
|
||||
molstarDataLink = 'https://stories.molstar.org/api/story/' + storyId + '/data';
|
||||
} else if (storyUrl) {
|
||||
mvsStories.loadFromURL(storyUrl, { format: format || 'mvsj', contextName: 'story1' });
|
||||
}
|
||||
|
||||
if (!editInStoriesUrl && storySessionUrl) {
|
||||
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?session-url=' + encodeURIComponent(storySessionUrl);
|
||||
}
|
||||
|
||||
if (molstarDataLink) {
|
||||
var molstarLink = 'https://molstar.org/viewer?mvs-url=' + encodeURIComponent(molstarDataLink) + '&mvs-format=' + encodeURIComponent(format || 'mvsj');
|
||||
document.getElementById('open-in-molstar-link').setAttribute('href', molstarLink);
|
||||
document.getElementById('open-in-molstar').style.display = 'inline';
|
||||
}
|
||||
|
||||
if (editInStoriesUrl) {
|
||||
document.getElementById('open-in-stories-link').setAttribute('href', editInStoriesUrl);
|
||||
document.getElementById('open-in-stories').style.display = 'inline';
|
||||
}
|
||||
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
mvsStories.downloadCurrentStory({ contextName: 'story1' });
|
||||
});
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { getMVSStoriesContext } from './context';
|
||||
import './elements';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { download } from '../../mol-util/download';
|
||||
|
||||
import './favicon.ico';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
@@ -48,4 +49,16 @@ export function loadFromID(id: string, options?: { format?: 'mvsx' | 'mvsj', con
|
||||
);
|
||||
}
|
||||
|
||||
export function downloadCurrentStory(options?: { contextName?: string, filename?: string }) {
|
||||
const story = getContext(options?.contextName).state.currentStoryData.value;
|
||||
if (!story) return;
|
||||
|
||||
const isMVSJ = typeof story === 'string';
|
||||
const filename = `${options?.filename ?? 'story'}.${isMVSJ ? 'mvsj' : 'mvsx'}`;
|
||||
download(
|
||||
new Blob([typeof story === 'string' ? story : story.buffer], { type: isMVSJ ? 'application/json' : 'application/octet-stream' }),
|
||||
filename
|
||||
);
|
||||
};
|
||||
|
||||
export { MVSData };
|
||||
@@ -182,6 +182,15 @@
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1d4ed7;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
|
||||
@@ -109,8 +109,10 @@ const DefaultViewerOptions = {
|
||||
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
|
||||
illumination: false,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
|
||||
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
@@ -187,8 +189,10 @@ export class Viewer {
|
||||
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
|
||||
[PluginConfig.General.PowerPreference, o.powerPreference],
|
||||
[PluginConfig.General.ResolutionMode, o.resolutionMode],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowReset, o.viewportShowReset],
|
||||
[PluginConfig.Viewport.ShowScreenshotControls, o.viewportShowScreenshotControls],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
|
||||
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
|
||||
|
||||
@@ -30,8 +30,8 @@ import { ExampleMol } from './example-data';
|
||||
import './index.html';
|
||||
import { jsonCifToMolfile } from './molfile';
|
||||
import { RGroupName } from './r-groups';
|
||||
import { SingleTaskQueue } from './utils';
|
||||
import { molfileToJSONCif } from '../../extensions/json-cif/utils';
|
||||
import { SingleTaskQueue } from '../../mol-util/single-task-queue';
|
||||
|
||||
async function init(target: HTMLElement | string, molfile: string = ExampleMol) {
|
||||
const root = typeof target === 'string' ? document.getElementById(target)! : target;
|
||||
|
||||
@@ -98,14 +98,14 @@
|
||||
|
||||
<body>
|
||||
<div id="viewer">
|
||||
<mvs-stories-viewer />
|
||||
<mvs-stories-viewer></mvs-stories-viewer>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<div id="select-story" class="select-story"></div>
|
||||
<mvs-stories-snapshot-markdown style="flex-grow: 1;" />
|
||||
<mvs-stories-snapshot-markdown style="flex-grow: 1;"></mvs-stories-snapshot-markdown>
|
||||
</div>
|
||||
<div id="links">
|
||||
<a href="#" id="mvs-data" filename="kinase-story.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
<a href="#" id="mvs-data">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
360
src/examples/mvs-stories/stories/animation.ts
Normal file
360
src/examples/mvs-stories/stories/animation.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder, Structure as MVSStructure, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../../../extensions/mvs/tree/mvs/param-types';
|
||||
import { Mat4 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
const Colors = {
|
||||
'1cbs': '#4577B2' as ColorT,
|
||||
|
||||
'ligand-away': '#F3794C' as ColorT,
|
||||
'ligand-docked': '#B9E3A0' as ColorT,
|
||||
};
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'Animation Demo',
|
||||
key: 'intro',
|
||||
description: `### Molecular Animation
|
||||
A story showcasing MolViewSpec animation capabilities.
|
||||
|
||||
[\[**🔄 Replay Intro**\]](!play-transition)
|
||||
[\[**⏵ Play Snapshots**\]](!play-snapshots)
|
||||
[\[**⏹ Stop Animation**\]](!stop-animation)
|
||||
|
||||
[\[**➡️ Next Snapshot**\]](!next-snapshot)
|
||||
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
const prims = _1cbs.primitives({
|
||||
ref: 'prims',
|
||||
label_opacity: 0,
|
||||
});
|
||||
prims.label({ text: 'Animation Demo', position: { label_asym_id: 'A' }, label_size: 10 });
|
||||
|
||||
const anim = builder.animation({
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'rock',
|
||||
params: { speed: 0.5 },
|
||||
}
|
||||
}
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
start: 1,
|
||||
end: 0.66,
|
||||
});
|
||||
|
||||
|
||||
// Uncomment this to make 2nd frame render much faster
|
||||
// It will cause shader compilation to happen during the 1st snapshot
|
||||
|
||||
// const surface = poly.representation({
|
||||
// type: 'surface',
|
||||
// surface_type: 'gaussian',
|
||||
// }).opacity({ opacity: 0 });
|
||||
|
||||
// _1cbs.component({ selector: 'ligand' })
|
||||
// .representation({ type: 'ball_and_stick' })
|
||||
// .opacity({ opacity: 0 });
|
||||
|
||||
// surface.clip({
|
||||
// ref: 'clip',
|
||||
// type: 'plane',
|
||||
// point: [22.0, 15, 0],
|
||||
// normal: [0, 0, 1],
|
||||
// });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-11.49, -37.05, 15.78],
|
||||
target: [15.85, 17.26, 24.32],
|
||||
up: [-0.88, 0.4, 0.26],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Ligand Docking',
|
||||
description: `Animate ligand moving to the binding site`,
|
||||
linger_duration_ms: 2500,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
const [poly,] = polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
const surface = poly.representation({
|
||||
type: 'surface',
|
||||
surface_type: 'gaussian',
|
||||
});
|
||||
|
||||
_1cbs.component({ selector: 'ligand' })
|
||||
.transform({
|
||||
ref: 'xform',
|
||||
translation: [5, 20, -20],
|
||||
rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1],
|
||||
rotation_center: 'centroid',
|
||||
})
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({ ref: 'ligand-color', color: 'red' });
|
||||
|
||||
surface.clip({
|
||||
ref: 'clip',
|
||||
type: 'plane',
|
||||
point: [22.0, 15, 0],
|
||||
normal: [0, 0, 1],
|
||||
});
|
||||
|
||||
const anim = builder.animation();
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
easing: 'sin-in',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 600,
|
||||
duration_ms: 800,
|
||||
property: ['point', 2],
|
||||
end: 0,
|
||||
easing: 'sin-out',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'vec3',
|
||||
target_ref: 'xform',
|
||||
duration_ms: 2000,
|
||||
property: 'translation',
|
||||
end: [0, 0, 0],
|
||||
noise_magnitude: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'rotation_matrix',
|
||||
target_ref: 'xform',
|
||||
duration_ms: 2000,
|
||||
property: 'rotation',
|
||||
noise_magnitude: 0.2,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'ligand-color',
|
||||
duration_ms: 2000,
|
||||
property: 'color',
|
||||
end: Colors['ligand-docked'],
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-30.63, 77.29, 2.28],
|
||||
target: [19.16, 26.15, 22.82],
|
||||
up: [0.69, 0.71, 0.09],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Highlight & Opacity',
|
||||
description: `Animate emissive, opacity and transform properties`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 0,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
const [poly,] = polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
poly.representation({
|
||||
type: 'surface',
|
||||
surface_type: 'gaussian'
|
||||
}).opacity({ ref: 'opacity', opacity: 1 }).color({ ref: 'surface-color', color: 'white' });
|
||||
|
||||
_1cbs.component({ selector: 'ligand' })
|
||||
.transform({ ref: 'xform', translation: [0, 0, 0] })
|
||||
.representation({
|
||||
ref: 'repr',
|
||||
type: 'ball_and_stick',
|
||||
custom: {
|
||||
molstar_representation_params: {
|
||||
emissive: 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
.color({ color: Colors['ligand-docked'] });
|
||||
|
||||
const primitives = builder.primitives({
|
||||
ref: 'primitives',
|
||||
instances: [
|
||||
Mat4.identity()
|
||||
],
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
primitives.ellipsoid({
|
||||
center: [0, 0, 0],
|
||||
radius: [2, 3, 2.5],
|
||||
color: 'red'
|
||||
});
|
||||
|
||||
const anim = builder.animation();
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'repr',
|
||||
duration_ms: 1000,
|
||||
property: ['custom', 'molstar_representation_params', 'emissive'],
|
||||
end: 0.2,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'opacity',
|
||||
duration_ms: 1000,
|
||||
frequency: 2,
|
||||
alternate_direction: true,
|
||||
property: 'opacity',
|
||||
end: 0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'transform_matrix',
|
||||
target_ref: 'primitives',
|
||||
property: ['instances', 0],
|
||||
translation_start: [20.24, 29.64, 14.85],
|
||||
translation_end: [21.84, 21.71, 27.04],
|
||||
translation_frequency: 4,
|
||||
pivot: [0, 0, 0],
|
||||
rotation_noise_magnitude: 0.2,
|
||||
scale_end: [0.01, 0.01, 0.01],
|
||||
duration_ms: 1000,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'primitives',
|
||||
duration_ms: 1000,
|
||||
property: 'opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'surface-color',
|
||||
duration_ms: 2000,
|
||||
property: 'color',
|
||||
palette: {
|
||||
kind: 'continuous',
|
||||
colors: ['white', Colors['1cbs'], 'white'],
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [6.92, 47.17, 10.68],
|
||||
target: [21.79, 22.2, 23.43],
|
||||
up: [0.8, 0.57, 0.2],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}
|
||||
];
|
||||
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
return builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
}
|
||||
|
||||
function polymer(structure: MVSStructure, options: { color: ColorT }) {
|
||||
const component = structure.component({ selector: { label_asym_id: 'A' } });
|
||||
const reprensentation = component.representation({ type: 'cartoon' });
|
||||
reprensentation.color({ color: options.color });
|
||||
return [component, reprensentation] as const;
|
||||
}
|
||||
|
||||
function pdbUrl(id: string) {
|
||||
return `https://www.ebi.ac.uk/pdbe/entry-files/download/${id.toLowerCase()}.bcif`;
|
||||
}
|
||||
|
||||
export function buildStory(): MVSData_States {
|
||||
const snapshots = Steps.map((s, i) => {
|
||||
const builder = s.state();
|
||||
if (s.camera) builder.camera(s.camera);
|
||||
|
||||
builder.canvas({
|
||||
custom: {
|
||||
molstar_postprocessing: {
|
||||
enable_outline: true,
|
||||
enable_ssao: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const description = i > 0 ? `${s.description}\n\n[Go to start](#intro)` : s.description;
|
||||
|
||||
return builder.getSnapshot({
|
||||
title: s.header,
|
||||
key: s.key,
|
||||
description,
|
||||
description_format: 'markdown',
|
||||
linger_duration_ms: s.linger_duration_ms ?? 500,
|
||||
transition_duration_ms: s.transition_duration_ms ?? 1000,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'Animation Showcase',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
186
src/examples/mvs-stories/stories/audio.ts
Normal file
186
src/examples/mvs-stories/stories/audio.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -6,8 +6,14 @@
|
||||
|
||||
import { buildStory as kinase } from './kinase';
|
||||
import { buildStory as tbp } from './tbp';
|
||||
import { buildStory as animation } from './animation';
|
||||
import { buildStory as audio } from './audio';
|
||||
import { buildStory as motm1 } from './motm1';
|
||||
|
||||
export const Stories = [
|
||||
{ id: 'kinase', name: 'BCR-ABL: A Kinase Out of Control', buildStory: kinase },
|
||||
{ id: 'tata', name: 'TATA-Binding Protein and its Role in Transcription Initiation ', buildStory: tbp },
|
||||
{ id: 'motm1', name: 'RCSB Molecule of the Month #1', buildStory: motm1 },
|
||||
{ id: 'animation-example', name: 'Molecular Animation Example', buildStory: animation },
|
||||
{ id: 'audio-example', name: 'Audio Playback Example', buildStory: audio },
|
||||
] as const;
|
||||
958
src/examples/mvs-stories/stories/motm1.ts
Normal file
958
src/examples/mvs-stories/stories/motm1.ts
Normal file
@@ -0,0 +1,958 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
*/
|
||||
|
||||
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
|
||||
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder, Structure as MVSStructure, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
import { Mat4 } from '../../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
|
||||
import { formatMolScript } from '../../../mol-script/language/expression-formatter';
|
||||
|
||||
// 1pmb->1mbn
|
||||
const align = Mat4.fromArray(Mat4.zero(), [0.4634187130865737, -0.7131589697034304, 0.5259728687171936, 0, -0.22944227902330105, -0.6698811108214233, -0.7061273127008398, 0, 0.8559202154942049, 0.2065522332899299, -0.4740643150728161, 0, -52.54880970106205, 37.49099778180445, -6.133850309914719, 1], 0);
|
||||
// 1mbo->1myf
|
||||
const alignmbo = Mat4.fromArray(Mat4.zero(), [-0.8334619943964441, -0.512838061396133, -0.20576353166796402, 0, -0.20145089001561267, 0.628743285359846, -0.7510655776229758, 0, 0.5145474196737698, -0.5845332204089626, -0.6273453801378679, 0, 11.864847328611186, -1.5261713438028912, 23.638919347623467, 1], 0);
|
||||
|
||||
const ill_color = (color: string, carbonLightness: number) => ({
|
||||
molstar_color_theme_name: 'illustrative',
|
||||
molstar_color_theme_params: {
|
||||
style: {
|
||||
name: 'uniform',
|
||||
params: {
|
||||
value: decodeColor(color),
|
||||
saturation: 0,
|
||||
lightness: 0,
|
||||
}
|
||||
},
|
||||
carbonLightness: carbonLightness // required parameter
|
||||
}
|
||||
});
|
||||
|
||||
const GColors2 = ill_color('#947c7c', 0.8);
|
||||
|
||||
/* from David Goodsell style
|
||||
in his illustrate software
|
||||
HETATM-H-------- 0,9999, 1.1,1.1,1.1, 0.0
|
||||
HETATMH--------- 0,9999, 1.0,1.0,1.0, 0.0
|
||||
ATOM -H-------- 0,9999, 1.0,1.0,1.0, 0.0
|
||||
ATOM H--------- 0,9999, 1.0,1.0,1.0, 0.0
|
||||
HETATM-----HOH-- 0,9999, 1.0,1.0,0.0, 0.0
|
||||
ATOM -OD--ASP A 0,9999 1.00, 0.20, 0.20, 1.6
|
||||
ATOM -OE--GLU A 0,9999 1.00, 0.20, 0.20, 1.6
|
||||
ATOM -NZ--LYS A 0,9999 0.10, 0.70, 1.00, 1.6
|
||||
ATOM -NH--ARG A 0,9999 0.10, 0.70, 1.00, 1.6
|
||||
ATOM -NE--ARG A 0,9999 0.10, 0.70, 1.00, 1.6
|
||||
ATOM -ND--HIS A 0,9999 0.10, 0.70, 1.00, 1.6
|
||||
ATOM -NE--HIS A 0,9999 0.10, 0.70, 1.00, 1.6
|
||||
ATOM -N------ A 0,9999 0.80, 0.90, 1.00, 1.5
|
||||
ATOM -O------ A 0,9999 1.00, 0.80, 0.80, 1.5
|
||||
ATOM -C------ A 0,9999 1.00, 1.00, 1.00, 1.6
|
||||
ATOM -S------ A 0,9999 1.00, 0.90, 0.50, 1.8
|
||||
HETATM-C------ - 0,9999 0.60, 0.90, 0.60, 1.5
|
||||
HETATM-------- - 0,9999 0.40, 0.90, 0.40, 1.5
|
||||
*/
|
||||
const GColors3 = {
|
||||
schema: 'all_atomic', // or maybe just 'atom'
|
||||
category_name: 'atom_site',
|
||||
field_name: 'type_symbol',
|
||||
palette: {
|
||||
kind: 'categorical',
|
||||
// missing_color: ...
|
||||
colors: {
|
||||
'C': '#FFFFFF',
|
||||
'N': '#CCE6FF',
|
||||
'O': '#FFCCCC',
|
||||
'S': '#FFE680',
|
||||
}
|
||||
}
|
||||
} as unknown as MVSNodeParams<'color_from_source'>;
|
||||
|
||||
const audioPathBase = 'https://raw.githubusercontent.com/molstar/molstar/master';
|
||||
// For local debug
|
||||
// const audioPathBase = '';
|
||||
const _Audio1 = audioPathBase + '/examples/audio/AudioMOM1_A.mp3';
|
||||
const _Audio2 = audioPathBase + '/examples/audio/AudioMOM1_B.mp3';
|
||||
const _Audio3 = audioPathBase + '/examples/audio/AudioMOM1_C.mp3';
|
||||
const _Audio4 = audioPathBase + '/examples/audio/AudioMOM1_D.mp3';
|
||||
|
||||
const q = (expr: string, lang = 'pymol') =>
|
||||
`!query=${encodeURIComponent(expr)}&lang=${lang}&action=highlight,focus`;
|
||||
|
||||
const description_intro = `
|
||||
# Molecule of the Month: Myoglobin
|
||||
A story based on the orginal [first Molecule of the Month](https://pdb101.rcsb.org/motm/1) made by David Goodsell in January 2000.
|
||||
|
||||
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio1}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
|
||||
Myoglobin was the first protein to have its atomic structure determined, revealing how it stores oxygen in muscle cells.
|
||||
|
||||
---
|
||||
|
||||
## The First Protein Structure
|
||||
|
||||
Any discussion of protein structure must necessarily begin with **myoglobin**, because it is where the science of protein structure began. After years of arduous work, *John Kendrew* and his coworkers determined the atomic structure of myoglobin, laying the foundation for an era of biological understanding.
|
||||
|
||||
You can take a close look at this protein structure yourself, in **PDB entry [1mbn](https://www.rcsb.org/structure/1MBN)**. You will be amazed—just like the world was in 1960—at the beautiful intricacy of this protein.
|
||||
|
||||
---
|
||||
|
||||
## Myoglobin and Muscles
|
||||
|
||||
[Myoglobin](!query%3Dchain%20A%26lang%3Dpymol%26action%3Dhighlight%2Cfocus) is a **small, bright red protein**. It is very common in muscle cells and gives meat much of its red color. Its job is to **store oxygen**, for use when muscles are hard at work.
|
||||
|
||||
To do this, it uses a special chemical tool to capture slippery oxygen molecules: a **[heme group](!query%3Dresn%20HEM%26lang%3Dpymol%26action%3Dhighlight%2Cfocus)**. Heme is a disk-shaped molecule with a hole in the center that is perfect for holding an iron ion. The iron then forms a strong interaction with the **[oxygen molecule](!query%3Dresn%20OH%26lang%3Dpymol%26action%3Dhighlight%2Cfocus)**. As you can see in the structure, the heme group is held tightly in a deep pocket on one side of the protein.
|
||||
|
||||
---
|
||||
|
||||
## Visualizing Protein Structure
|
||||
|
||||
When the structure of myoglobin was solved, it posed a great challenge. The structure is so complex that **new methods** needed to be developed to display and understand it.
|
||||
|
||||
- *John Kendrew* used a huge wire model to build the structure based on the experimental electron density.
|
||||
- Then, the artist *Irving Geis* was employed to create a picture of myoglobin for a prominent article in *Scientific American*.
|
||||
- Computer graphics were still many years in the future, so he created this illustration entirely by hand—one atom at a time.
|
||||
|
||||
You can learn more about the work of Irving Geis at the **[Geis Archive on PDB-101](https://pdb101.rcsb.org/learn/GeisArchive)**.
|
||||
|
||||

|
||||
|
||||
*Illustration of myoglobin by Irving Geis. You can learn more about this painting at the Geis Archive on PDB-101.
|
||||
Used with permission from the Howard Hughes Medical Institute, Copyright 2015.*
|
||||
`;
|
||||
|
||||
|
||||
const query1 = MS.struct.generator.atomGroups({
|
||||
'entity-test': MS.core.rel.eq([
|
||||
MS.struct.atomProperty.core.modelEntryId(),
|
||||
'1MBN'
|
||||
])
|
||||
});
|
||||
const firstEntity1 = q(formatMolScript(query1), 'mol-script');
|
||||
const query2 = MS.struct.generator.atomGroups({
|
||||
'entity-test': MS.core.rel.eq([
|
||||
MS.struct.atomProperty.core.modelEntryId(),
|
||||
'1PMB'
|
||||
])
|
||||
});
|
||||
const firstEntity2 = q(formatMolScript(query2), 'mol-script');
|
||||
|
||||
const query3 = MS.struct.generator.atomGroups({
|
||||
'entity-test': MS.core.rel.eq([
|
||||
MS.struct.atomProperty.core.modelEntryId(),
|
||||
'1MBN'
|
||||
]),
|
||||
'residue-test': MS.core.set.has([
|
||||
MS.set(12, 140, 87),
|
||||
MS.struct.atomProperty.macromolecular.auth_seq_id()
|
||||
])
|
||||
});
|
||||
const charged_residues = q(formatMolScript(query3), 'mol-script');
|
||||
|
||||
const description_p1 = `
|
||||
# Myoglobin and Whales
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio2}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
|
||||
If you look at John Kendrew's PDB file, you'll notice that the myoglobin he used was taken
|
||||
from sperm whale muscles. Whales and dolphin have a great need for myoglobin, so that they can
|
||||
store extra oxygen for use in their deep undersea dives. Typically, they have about 30
|
||||
times more than in animals that live on land. A recent study revealed that a few special
|
||||
modifications are needed to make this possible.
|
||||
Comparing [whale myoglobin](${firstEntity1})
|
||||
(PDB entry [1mbn](https://www.rcsb.org/structure/1mbn)) with
|
||||
[pig myoglobin](${firstEntity2})
|
||||
(PDB entry [1pmb](https://www.rcsb.org/structure/1pmb)), we find that there are
|
||||
several mutations that add [extra positively-charged
|
||||
amino acids](${charged_residues}) to the surface. Marine animals typically have these extra charges on
|
||||
the surface of their myoglobin
|
||||
to help repel neighboring molecules and prevent aggregation when myoglobin is at
|
||||
high concentrations.
|
||||
`;
|
||||
|
||||
const description_p2 = `
|
||||
# Oxygen Bound to Myoglobin
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio3}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
|
||||
A later structure of myoglobin, PDB entry [1mbo](https://www.rcsb.org/structure/1mbo),
|
||||
shows that [oxygen](${q('index 1276+1277')}) binds to
|
||||
the [iron](${q('index 1275')}) atom deep inside the protein.
|
||||
So how does it get in and out? The
|
||||
answer is that the structure in the PDB is only one snapshot of the
|
||||
protein, caught when it is in a tightly-closed form. In reality,
|
||||
myoglobin (and all other proteins) is constantly in motion, performing
|
||||
small flexing and breathing motions (illustrated here by PDB entry [1myf](https://www.rcsb.org/structure/1myf)). So, temporary openings constantly
|
||||
appear and disappear, allowing oxygen in and out.
|
||||
`;
|
||||
|
||||
const description_p3 = `
|
||||
# Molecule of the Month: Myoglobin
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio1}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
|
||||
The atomic structure of myoglobin revealed many of the basic principles
|
||||
of protein structure and stability. For instance, the structure showed
|
||||
that when the protein chain folds into a globular structure,
|
||||
[carbon-rich amino acids](${q('resn ALA+VAL+LEU+ILE+MET+PHE+TRP+PRO')}) are sheltered
|
||||
inside and charged amino acids [positively](${q('resn LYS+ARG+HIS')})
|
||||
and [negatively](${q('resn GLU+ASP')}) are most often found on the surface,
|
||||
occasionally forming [salt bridges](${q('chain A and resi 44+47+77+18')})
|
||||
that pair two opposite charges (shown here with circles).
|
||||
|
||||
To explore some of these principles, eplxore freely in the interactive view.
|
||||
|
||||
# Topics for Further Discussion
|
||||
You can use the sequence comparison tool to align the sequences of different
|
||||
myoglobins, looking for mutations. For instance, [here is the alignment of whale and pig myoglobin](https://www.rcsb.org/alignment?request-body=eJyljrsOwjAMRf%2FFcxgqxNKN7kXsqKpC4pZIeZTEVamq%2FDtOGRAzm%2BPje3I3eM4YV6g3CBOZ4FMZI9IcfZ%2BQoVfYa0kS6kHahFmACp7wReXQBY1QwyRNXExCEOCQHkEX5qUrjNxBWjN64GSiOCtWI%2F9y2wA9xbU3fA1V21w4ndCiKjWKQKbVfeiZ0R3HUmhfVIKz%2Bvs8HXMWv75r2%2Fzn61gJg7F71y6%2FARZEZL8%3D&response-body=eJzlU11vmzAU%2FS88Q2RsjKFv7iCA7GRJQFRVVUU0uClSQjo%2BtkVV%2FvuuyUdTadMeurcJIXF97j33HN%2FLm1HVzzvj5s3o%2B6o0bgzlUaZoyaynwvUshznPll8UpVVgRUr1hAkrmWEabVd0fQv5X75OZjLMQuNgGlvVFZqq2FTreqvqbrndlQqSXouq%2BVG1CgqvMNW97HTLbmsNp5qiUW2%2F6YD44Q16NP2q6%2BFoCKGm2S8HkfbkdqpFqI1addWuHpq2%2B%2B0R5QA9qfWyVd%2BGA9uE2vI9pORwMD%2FyzSa3n%2BN7NN%2FlLi8eB92NWgOl9vDwF1YdVnWpfho3yDQ2ql53L0f%2BR%2FMTtaCta4q6fd4126I7a7FNdHp%2B%2BwUd0chxCXY9xjA2LTRiNkWMONT2TDSimDi%2Bz1yHQDaAmGCbeTbxXR25rodsG%2FvHiCGGEHE9D2vukUcpdgnBlEGAEfU9RrFPdKbDqOMT2x8yLYpH1MWOQxzP9U3CRi5lBCGKoKlFR77j2Rg5iNkgVw%2Bg326LZq%2BH165257X5Xmx62EFo68I97F%2F1PsK99apeKasqYUpVtzf0QlwyXeeSuZikwUfQ9y5gNrGGRoYefw1jr5fQtSp7tdQb3w73rxH9G2xUeUa1MI3Aq2XDEHUpzOxUoKPV7rtqirXSqQjmgdDjcctO0v%2B4ZJ%2FYsQs5lOYyDaPwbi5zGed3XOQhD3IexdE8SGSykGORxrMwk6EYB4uxiKXIQh5OBE%2FDQAoRR3mWy4zLiCcQiiiOAZZiJvk8jXkmYpHMEnEvw3GShjxJYuiTLuJZFIwjHvB5xCdTwWUoxwsRJJyLexHK6H4eDdP453YjmQYnu9P8LrqyG%2BZHu9HFrhjsgs9ru9OT3ejabvbBbn5ld57LeSo%2B2E1PdqfB5Gx3rO3%2BH5t9%2BAU7Kf5z&encoded=true) used to create the illustration in this column.
|
||||
PDB entry [2jho](https://www.rcsb.org/structure/2jho) includes myoglobin poisoned by cyanide. Take a look and you'll see that the cyanide blocks the binding site for oxygen.
|
||||
|
||||
|
||||
# References
|
||||
|
||||
- 1mbn: J. C. Kendrew, R. E. Dickerson, B. E. Strandberg, R. G. Hart, D. R. Davies, D. C. Phillips & V. C. Shore (1960) Structure of Myoglobin. Nature 185, 422-427.
|
||||
- J. C. Kendrew (1961) The three-dimensional structure of a protein molecule. Scientific American 205(6), 96-110.
|
||||
- 1mbo: S. E. Phillips (1980) Structure and refinement of oxymyoglobin at 1.6 A resolution. Journal of Molecular Biology 142, 531-554.
|
||||
- 1pmb: S. J. Smerdon, T. J. Oldfield, E. J. Dodson, G. G. Dodson, R. E. Hubbard & A. J. Wilkinson (1990) Determination of the crystal structure of recombinant pig myoglobin by molecular replacement and its refinement. Acta Crystallographica B, 46, 370-377.
|
||||
- 1myf: Osapay K, Theriault Y, Wright PE, Case DA. Solution structure of carbonmonoxy myoglobin determined from nuclear magnetic resonance distance and chemical shift constraints. J Mol Biol. 1994;244(2):183-197. doi:10.1006/jmbi.1994.1718
|
||||
- S. Mirceta, A. V. Signore, J. M. Burns, A. R. Cossins, K. L. Campbell & M. Berenbrink (2013) Evolution of mammalian diving capacity traced by myoglobin net surface charge. Science 340, 1234192.
|
||||
|
||||
`;
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'Molecule of the Month: Myoglobin',
|
||||
key: 'intro',
|
||||
description: description_intro,
|
||||
linger_duration_ms: 45000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
// no outline here
|
||||
|
||||
builder.canvas({ custom: { molstar_postprocessing: { enable_outline: false } } });
|
||||
|
||||
const _1mbn = structure(builder, '1MBN');
|
||||
|
||||
_1mbn.component({ selector: 'ligand' })
|
||||
.representation({ ref: 'ligand', type: 'ball_and_stick' })
|
||||
.color({ color: 'orange' });
|
||||
// FE and O should be spacefill
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 155, label_atom_id: 'F' } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'yellow' });
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 154 } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'blue' });
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 154 } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'blue' });
|
||||
|
||||
const chA = _1mbn.component({ selector: { label_asym_id: 'A' } });
|
||||
chA.representation({ type: 'surface', surface_type: 'gaussian' })
|
||||
.color({ color: '#ff0303' })
|
||||
.opacity({ ref: 'surfopa', opacity: 0.0 });
|
||||
|
||||
chA.representation({ type: 'line' })
|
||||
.color({ custom: { molstar_color_theme_name: 'element-symbol' } })
|
||||
.opacity({ ref: 'lineopa', opacity: 0.0 });
|
||||
|
||||
chA.representation({ type: 'cartoon' })
|
||||
.color({ custom: { molstar_color_theme_name: 'secondary-structure' } });
|
||||
|
||||
// whale
|
||||
_1mbn.component({ selector: { label_asym_id: 'A' } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.colorFromSource({
|
||||
schema: 'all_atomic',
|
||||
category_name: 'atom_site',
|
||||
field_name: 'type_symbol',
|
||||
palette: {
|
||||
kind: 'categorical',
|
||||
colors: {
|
||||
'C': '#FFFFFF',
|
||||
'N': '#CCE6FF',
|
||||
'O': '#FFCCCC',
|
||||
'S': '#FFE680',
|
||||
}
|
||||
}
|
||||
}).opacity({ ref: 'cpkopa1', opacity: 0.0 });
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 155 } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.color({ custom: GColors2 }).opacity({ ref: 'cpkopa2', opacity: 0.0 });
|
||||
|
||||
const prims = _1mbn.primitives({
|
||||
ref: 'prims',
|
||||
label_opacity: 1,
|
||||
label_background_color: 'grey',
|
||||
custom: {
|
||||
molstar_markdown_commands: {
|
||||
// 'apply-snapshot': 'interlude',
|
||||
'play-audio': _Audio1,
|
||||
}
|
||||
}
|
||||
});
|
||||
prims.label({
|
||||
text: 'Start Comments',
|
||||
position: [13.5, 45.1, 7.7],
|
||||
label_size: 5
|
||||
});
|
||||
addNextButton(builder, 'whale', [13.5, 0, 7.7]);
|
||||
|
||||
// doesnt work for first slide, but work afterward
|
||||
builder.extendRootCustomState({
|
||||
molstar_on_load_markdown_commands: {
|
||||
'play-audio': _Audio1,
|
||||
}
|
||||
});
|
||||
const anim = builder.animation(
|
||||
{
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'spin',
|
||||
params: { speed: -0.05 },
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'lineopa',
|
||||
duration_ms: 2000,
|
||||
start_ms: 0,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'ligand',
|
||||
start_ms: 22000,
|
||||
duration_ms: 10000,
|
||||
frequency: 6,
|
||||
alternate_direction: true,
|
||||
property: ['custom', 'molstar_representation_params', 'emissive'],
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'cpkopa1',
|
||||
duration_ms: 5000,
|
||||
start_ms: 40000,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'cpkopa2',
|
||||
duration_ms: 5000,
|
||||
start_ms: 40000,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'next',
|
||||
duration_ms: 2000,
|
||||
start_ms: 43000,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [13.5, 21.1, 73.1],
|
||||
target: [13.5, 21.1, 7.7],
|
||||
up: [0, 1, 0],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Myoglobin and Whales',
|
||||
key: 'whale',
|
||||
description: description_p1,
|
||||
linger_duration_ms: 41000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1mbn = structure(builder, '1mbn').transform({ ref: 'whalex', translation: [-30, 0, 0] });
|
||||
|
||||
// whale
|
||||
_1mbn.component({ selector: { label_asym_id: 'A' } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.colorFromSource({
|
||||
schema: 'all_atomic', // or maybe just 'atom'
|
||||
category_name: 'atom_site',
|
||||
field_name: 'type_symbol',
|
||||
palette: {
|
||||
kind: 'categorical',
|
||||
colors: {
|
||||
'C': '#FFFFFF',
|
||||
'N': '#CCE6FF',
|
||||
'O': '#FFCCCC',
|
||||
'S': '#FFE680',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 155 } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.color({ custom: GColors2 });
|
||||
|
||||
_1mbn.primitives({
|
||||
ref: 'prims',
|
||||
label_opacity: 1,
|
||||
label_attachment: 'top-center',
|
||||
label_show_tether: true,
|
||||
label_tether_length: 1.0,
|
||||
})
|
||||
.label({
|
||||
text: 'whale',
|
||||
position: { label_asym_id: 'A', auth_seq_id: 8 },
|
||||
label_size: 10
|
||||
});
|
||||
|
||||
_1mbn.primitives({
|
||||
ref: 'startres',
|
||||
label_opacity: 0,
|
||||
})
|
||||
.label({
|
||||
text: '★', label_offset: 4,
|
||||
position: { label_asym_id: 'A', auth_seq_id: 12, atom_id: 96 }, label_size: 5
|
||||
})
|
||||
.label({
|
||||
text: '★', label_offset: 4,
|
||||
position: { label_asym_id: 'A', auth_seq_id: 140, auth_atom_id: 'NZ' }, label_size: 5
|
||||
})
|
||||
.label({
|
||||
text: '★', label_offset: 4,
|
||||
position: { label_asym_id: 'A', auth_seq_id: 87, auth_atom_id: 'NZ' }, label_size: 5
|
||||
});
|
||||
|
||||
// the following doesnt work
|
||||
const seld = _1mbn.component({
|
||||
selector: [
|
||||
{ label_asym_id: 'A', auth_seq_id: 12 },
|
||||
{ label_asym_id: 'A', auth_seq_id: 140 },
|
||||
{ label_asym_id: 'A', auth_seq_id: 87 }
|
||||
]
|
||||
});
|
||||
|
||||
seld.representation({ ref: 'scharged', type: 'surface', surface_type: 'gaussian', custom: { molstar_representation_params: { emissive: 0.0, ignoreLight: true } } })
|
||||
.colorFromSource(GColors3);
|
||||
|
||||
// pig
|
||||
const _1pmb = structure(builder, '1pmb').transform({ ref: 'pig', matrix: align });
|
||||
|
||||
_1pmb.component({ selector: { label_asym_id: 'A' } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.colorFromSource(GColors3);
|
||||
|
||||
_1pmb.component({ selector: { label_asym_id: 'C', auth_seq_id: 154 } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.color({ custom: GColors2 });
|
||||
|
||||
|
||||
_1pmb.primitives({
|
||||
ref: 'labelpig',
|
||||
label_opacity: 1,
|
||||
label_attachment: 'top-center',
|
||||
label_show_tether: true,
|
||||
label_tether_length: 1.0,
|
||||
})
|
||||
.label({
|
||||
text: 'pig',
|
||||
position: { label_asym_id: 'A', auth_seq_id: 8 },
|
||||
label_size: 10
|
||||
});
|
||||
|
||||
builder.extendRootCustomState({
|
||||
molstar_on_load_markdown_commands: {
|
||||
'play-audio': _Audio2,
|
||||
}
|
||||
});
|
||||
|
||||
const anim = builder.animation(
|
||||
{
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'spin',
|
||||
params: { speed: -0.05 },
|
||||
}
|
||||
}
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'vec3',
|
||||
target_ref: 'whalex',
|
||||
duration_ms: 10000,
|
||||
start_ms: 16000,
|
||||
property: 'translation',
|
||||
start: [-30, 0, 0],
|
||||
end: [-60, 0, 0],
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'startres',
|
||||
duration_ms: 1000,
|
||||
start_ms: 20000,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
// pig appear at 18s
|
||||
anim.interpolate({
|
||||
kind: 'transform_matrix',
|
||||
target_ref: 'pig',
|
||||
duration_ms: 5000,
|
||||
start_ms: 18000,
|
||||
property: 'matrix',
|
||||
translation_start: [-82.54880970106205, 37.49099778180445, -6.133850309914719],
|
||||
translation_end: [-52.54880970106205, 37.49099778180445, -6.133850309914719],
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'labelpig',
|
||||
duration_ms: 2000,
|
||||
start_ms: 18000,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
addNextButton(builder, 'oxygen', [-18.9, 10, 7.3]);
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'next',
|
||||
duration_ms: 2000,
|
||||
start_ms: 38000,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'scharged',
|
||||
start_ms: 20000,
|
||||
duration_ms: 6000,
|
||||
frequency: 6,
|
||||
alternate_direction: true,
|
||||
property: ['custom', 'molstar_representation_params', 'emissive'],
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-14.6, 116.1, 66.5],
|
||||
target: [-18.9, 21.1, 7.3],
|
||||
up: [-0.0, 0.5, -0.8],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Oxygen Bound',
|
||||
key: 'oxygen',
|
||||
description: description_p2,
|
||||
linger_duration_ms: 18000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
// NMR 1MYF
|
||||
// 1A6N unbound
|
||||
// 1A6M bound
|
||||
// series 2G0R
|
||||
const _1mbo = structure(builder, '1mbo')
|
||||
.transform({ matrix: alignmbo });
|
||||
|
||||
const _1myf = builder
|
||||
.download({ url: pdbUrl('1myf') })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure({ ref: '1myf' });
|
||||
|
||||
const red1 = '#d3a4a6';
|
||||
const red2 = '#d75354';
|
||||
|
||||
const blue1 = '#02d1d1';
|
||||
_1myf.component({ selector: { label_asym_id: 'A' } })
|
||||
.transform({ translation: [0, 0, 0] })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: red1 })
|
||||
.opacity({ ref: 'spo', opacity: 1.0 });
|
||||
|
||||
// OXYY
|
||||
// should animate in-out in loop
|
||||
_1mbo.component({ selector: { label_asym_id: 'C', auth_seq_id: 155 } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: {
|
||||
carbonColor: {
|
||||
name: 'uniform',
|
||||
params: { value: decodeColor(red2) }
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_1myf.component({ selector: { label_asym_id: 'A' } })
|
||||
.representation({ type: 'backbone' })
|
||||
.color({ color: red1 });
|
||||
|
||||
_1mbo.component({ selector: { label_asym_id: 'D', auth_seq_id: 555 } })
|
||||
.representation({
|
||||
ref: 'oxy', type: 'spacefill', custom: {
|
||||
molstar_representation_params: {
|
||||
emissive: 0.0
|
||||
}
|
||||
}
|
||||
})
|
||||
.color({ color: blue1 });
|
||||
|
||||
_1mbo.component({ selector: { label_asym_id: 'D', auth_seq_id: 555 } })
|
||||
.transform({ ref: 'oxyy', translation: [0, 0, 0] })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: blue1 })
|
||||
.opacity({ ref: 'oxop', opacity: 0.0 });
|
||||
|
||||
builder.extendRootCustomState({
|
||||
molstar_on_load_markdown_commands: {
|
||||
'play-audio': _Audio3,
|
||||
}
|
||||
});
|
||||
const anim = builder.animation(
|
||||
{
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'spin',
|
||||
params: { speed: -0.05 },
|
||||
}
|
||||
}
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'spo',
|
||||
duration_ms: 5000,
|
||||
start_ms: 0,
|
||||
property: 'opacity',
|
||||
start: 1.0,
|
||||
end: 0.05,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: '1myf',
|
||||
start_ms: 11000,
|
||||
duration_ms: 10000,
|
||||
frequency: 4,
|
||||
alternate_direction: true,
|
||||
property: 'model_index',
|
||||
discrete: true,
|
||||
start: 0,
|
||||
end: 11,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'oxy',
|
||||
start_ms: 3000,
|
||||
duration_ms: 10000,
|
||||
frequency: 7,
|
||||
alternate_direction: true,
|
||||
property: ['custom', 'molstar_representation_params', 'emissive'],
|
||||
end: 1.0,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'vec3',
|
||||
target_ref: 'oxyy',
|
||||
duration_ms: 5000,
|
||||
start_ms: 16000,
|
||||
property: 'translation',
|
||||
frequency: 4,
|
||||
alternate_direction: false,
|
||||
start: [5, -5, -20],
|
||||
end: [0, 0, 0],
|
||||
noise_magnitude: 1,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'oxop',
|
||||
duration_ms: 1000,
|
||||
start_ms: 15000,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
addNextButton(builder, 'end', [0, -25, 0.0]);
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'next',
|
||||
duration_ms: 2000,
|
||||
start_ms: 18000,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-2.2, 0.7, -78.5],
|
||||
target: [-0.1, 0.7, 0.6],
|
||||
up: [0, 1, 0],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Conclusion',
|
||||
key: 'end',
|
||||
description: description_p3,
|
||||
linger_duration_ms: 20000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
const _1mbn = structure(builder, '1mbn');
|
||||
// resn ALA+VAL+LEU+ILE+MET+PHE+TRP+PRO
|
||||
const carb = ['ALA', 'VAL', 'LEU', 'ILE', 'MET', 'PHE', 'TRP', 'PRO'].map(amk => ({ label_comp_id: amk }));
|
||||
// resn LYS+ARG+HIS+ASP+GLU
|
||||
const chargedp = ['LYS', 'ARG', 'HIS'].map(amk => ({ label_comp_id: amk }));
|
||||
const chargedn = ['ASP', 'GLU'].map(amk => ({ label_comp_id: amk }));
|
||||
|
||||
// salt bridge
|
||||
// ASP44-OD1-356-LYS47-NZ-388
|
||||
// LYS77-NZ-613-GLU18-OE1-149
|
||||
// use primitve distance_measurement
|
||||
// and ellipse or ellipsoid with transparancy
|
||||
_1mbn.primitives({ ref: 'dist', label_opacity: 0.0 })
|
||||
.distance({
|
||||
start: { label_asym_id: 'A', auth_seq_id: 44, atom_id: 356 },
|
||||
end: { label_asym_id: 'A', auth_seq_id: 47, atom_id: 388 },
|
||||
radius: 0.1, dash_length: 0.1,
|
||||
label_size: 2
|
||||
})
|
||||
.distance({
|
||||
start: { label_asym_id: 'A', auth_seq_id: 77, atom_id: 613 },
|
||||
end: { label_asym_id: 'A', auth_seq_id: 18, atom_id: 149 },
|
||||
radius: 0.1, dash_length: 0.1,
|
||||
label_size: 2
|
||||
});
|
||||
// 44 OD1 22.300 33.300 -6.200
|
||||
// 47 NZ 23.200 32.000 -8.400
|
||||
const r44 = Vec3.create(22.300, 33.300, -6.200);
|
||||
const r47 = Vec3.create(23.200, 32.000, -8.400);
|
||||
getEllipse(builder, r44, r47, 'salt1');
|
||||
|
||||
// 18 OE1 16.600 22.500 20.500
|
||||
// 77 NZ 14.100 23.600 22.200
|
||||
const r18 = Vec3.create(16.600, 22.500, 20.500);
|
||||
const r77 = Vec3.create(14.100, 23.600, 22.200);
|
||||
getEllipse(builder, r18, r77, 'salt2');
|
||||
|
||||
const a = _1mbn.component({ selector: carb });
|
||||
a.representation({ type: 'ball_and_stick' })
|
||||
.color({ color: '#bec0f2' })
|
||||
.opacity({ ref: 'carb', opacity: 1.0 });
|
||||
|
||||
const b = _1mbn.component({ selector: chargedp });
|
||||
b.representation({ type: 'ball_and_stick' })
|
||||
.color({ custom: ill_color('blue', 3.0) })
|
||||
.opacity({ ref: 'chargedp', opacity: 1.0 });
|
||||
|
||||
const c = _1mbn.component({ selector: chargedn });
|
||||
c.representation({ type: 'ball_and_stick' })
|
||||
.color({ custom: ill_color('red', 3.0) })
|
||||
.opacity({ ref: 'chargedn', opacity: 1.0 });
|
||||
|
||||
_1mbn.component({ selector: { label_asym_id: 'A' } })
|
||||
.representation({ type: 'backbone' })
|
||||
.color({ color: '#919191' });
|
||||
|
||||
_1mbn.component({ selector: 'ligand' })
|
||||
.representation({
|
||||
ref: 'ligand', type: 'ball_and_stick',
|
||||
custom: {
|
||||
molstar_representation_params: {
|
||||
emissive: 0.0
|
||||
}
|
||||
}
|
||||
})
|
||||
.color({ color: 'orange' });
|
||||
|
||||
builder.extendRootCustomState({
|
||||
molstar_on_load_markdown_commands: {
|
||||
'play-audio': _Audio4,
|
||||
}
|
||||
});
|
||||
|
||||
const anim = builder.animation({});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'carb',
|
||||
duration_ms: 2000,
|
||||
start_ms: 8000,
|
||||
frequency: 2,
|
||||
alternate_direction: true,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'chargedp',
|
||||
duration_ms: 1000,
|
||||
start_ms: 10000,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'chargedn',
|
||||
duration_ms: 1000,
|
||||
start_ms: 10000,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
// show salt bridge
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'salt1',
|
||||
duration_ms: 1000,
|
||||
start_ms: 11000,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 0.3,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'salt2',
|
||||
duration_ms: 1000,
|
||||
start_ms: 11000,
|
||||
property: 'opacity',
|
||||
start: 0.0,
|
||||
end: 0.3,
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'dist',
|
||||
duration_ms: 1000,
|
||||
start_ms: 11000,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
addNextButton(builder, 'intro', [13.5, -10.0, 7.7]);
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'next',
|
||||
duration_ms: 2000,
|
||||
start_ms: 20000,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [16.0, 47.2, 67.8],
|
||||
target: [13.6, 21.1, 7.6],
|
||||
up: [0.1, 0.9, -0.4],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
];
|
||||
|
||||
function addNextButton(builder: any, snapshotKey: string, position: [number, number, number]) {
|
||||
builder.primitives({
|
||||
ref: 'next',
|
||||
tooltip: 'Click for next part',
|
||||
label_opacity: 0,
|
||||
label_background_color: 'grey',
|
||||
snapshot_key: snapshotKey
|
||||
})
|
||||
.label({
|
||||
ref: 'next_label',
|
||||
position: position,
|
||||
text: 'Click me to go next',
|
||||
label_color: 'white',
|
||||
label_size: 5
|
||||
});
|
||||
}
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
return builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
}
|
||||
|
||||
function getEllipse(builder: Root, pos1: Vec3, pos2: Vec3, ref: string) {
|
||||
const center = Vec3.add(Vec3(), pos1, pos2);
|
||||
Vec3.scale(center, center, 0.5);
|
||||
const major_axis = Vec3.sub(Vec3(), pos2, pos1);
|
||||
const z_axis = Vec3.create(0, 0, 1);
|
||||
// cross to get minor
|
||||
const minor_axis = Vec3.cross(Vec3(), major_axis, z_axis);
|
||||
return builder.primitives({ ref: ref, opacity: 0.33 }).ellipsoid({
|
||||
center: center as any,
|
||||
major_axis: major_axis as any,
|
||||
minor_axis: minor_axis as any,
|
||||
radius: [5.0, 3.0, 3.0],
|
||||
color: '#cccccc',
|
||||
});
|
||||
}
|
||||
|
||||
function pdbUrl(id: string) {
|
||||
return `https://www.ebi.ac.uk/pdbe/entry-files/download/${id.toLowerCase()}.bcif`;
|
||||
}
|
||||
|
||||
export function buildStory(): MVSData_States {
|
||||
const snapshots = Steps.map((s, i) => {
|
||||
const builder = s.state();
|
||||
if (s.camera) builder.camera(s.camera);
|
||||
|
||||
const description = i > 0 ? `${s.description}\n\n[Go to start](#intro)` : s.description;
|
||||
|
||||
return builder.getSnapshot({
|
||||
title: s.header,
|
||||
key: s.key,
|
||||
description,
|
||||
description_format: 'markdown',
|
||||
linger_duration_ms: s.linger_duration_ms ?? 500,
|
||||
transition_duration_ms: s.transition_duration_ms ?? 1000,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'RCSB Molecule of the Month 1',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
|
||||
import { CameraFogParams, Canvas3DParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { TrackballControlsParams } from '../../mol-canvas3d/controls/trackball';
|
||||
import { BloomParams } from '../../mol-canvas3d/passes/bloom';
|
||||
import { DofParams } from '../../mol-canvas3d/passes/dof';
|
||||
import { OutlineParams } from '../../mol-canvas3d/passes/outline';
|
||||
import { ShadowParams } from '../../mol-canvas3d/passes/shadow';
|
||||
import { SsaoParams } from '../../mol-canvas3d/passes/ssao';
|
||||
@@ -22,6 +25,7 @@ import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { decodeColor } from './helpers/utils';
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { SnapshotMetadata } from './mvs-data';
|
||||
import { MVSAnimationNode } from './tree/animation/animation-tree';
|
||||
import { MolstarNode, MolstarNodeParams } from './tree/molstar/molstar-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
@@ -119,37 +123,93 @@ export function createPluginStateSnapshotCamera(plugin: PluginContext, context:
|
||||
return camera;
|
||||
}
|
||||
|
||||
/** Set canvas properties based on a canvas node. */
|
||||
export function setCanvas(plugin: PluginContext, node: MolstarNode<'canvas'> | undefined) {
|
||||
plugin.canvas3d?.setProps(old => modifyCanvasProps(old, node));
|
||||
function optionalParams(enable: boolean | undefined, values: any, params: ParamDefinition.Params, fallback: any) {
|
||||
if (typeof enable === 'boolean') {
|
||||
return enable
|
||||
? { name: 'on', params: { ...ParamDefinition.getDefaultValues(params), ...values } }
|
||||
: { name: 'off', params: {} };
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** Create a deep copy of `oldCanvasProps` with values modified according to a canvas node params. */
|
||||
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, custom?: Record<string, any>): Canvas3DProps {
|
||||
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, animationNode: MVSAnimationNode<'animation'> | undefined): Canvas3DProps {
|
||||
const params = canvasNode?.params;
|
||||
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
|
||||
|
||||
const outline = !!canvasNode?.custom?.molstar_enable_outline;
|
||||
const shadow = !!canvasNode?.custom?.molstar_enable_shadow;
|
||||
const occlusion = !!canvasNode?.custom?.molstar_enable_ssao;
|
||||
const molstar_postprocessing = canvasNode?.custom?.molstar_postprocessing;
|
||||
|
||||
const outline = molstar_postprocessing?.enable_outline;
|
||||
const outlineParams = molstar_postprocessing?.outline_params;
|
||||
|
||||
const shadow = molstar_postprocessing?.enable_shadow;
|
||||
const shadowParams = molstar_postprocessing?.shadow_params;
|
||||
|
||||
const occlusion = molstar_postprocessing?.enable_ssao;
|
||||
const occlusionParams = molstar_postprocessing?.ssao_params;
|
||||
|
||||
const fog = molstar_postprocessing?.enable_fog;
|
||||
const fogParams = molstar_postprocessing?.fog_params;
|
||||
|
||||
const dof = molstar_postprocessing?.enable_depth_of_field;
|
||||
const dofParams = molstar_postprocessing?.depth_of_field_params;
|
||||
|
||||
const bloom = molstar_postprocessing?.enable_bloom;
|
||||
const bloomParams = molstar_postprocessing?.bloom_params;
|
||||
|
||||
const trackballAnimation = animationNode?.custom?.molstar_trackball;
|
||||
const trackballAnimationName = trackballAnimation?.name;
|
||||
const trackballAnimationParams = trackballAnimation?.params ?? {};
|
||||
|
||||
return {
|
||||
...oldCanvasProps,
|
||||
postprocessing: {
|
||||
...oldCanvasProps.postprocessing,
|
||||
outline: outline
|
||||
? { name: 'on', params: ParamDefinition.getDefaultValues(OutlineParams) }
|
||||
: oldCanvasProps.postprocessing.outline,
|
||||
shadow: shadow
|
||||
? { name: 'on', params: ParamDefinition.getDefaultValues(ShadowParams) }
|
||||
: oldCanvasProps.postprocessing.shadow,
|
||||
occlusion: occlusion
|
||||
? { name: 'on', params: ParamDefinition.getDefaultValues(SsaoParams) }
|
||||
: oldCanvasProps.postprocessing.occlusion,
|
||||
outline: optionalParams(outline, outlineParams, OutlineParams, oldCanvasProps.postprocessing.outline),
|
||||
shadow: optionalParams(shadow, shadowParams, ShadowParams, oldCanvasProps.postprocessing.shadow),
|
||||
occlusion: optionalParams(occlusion, occlusionParams, SsaoParams, oldCanvasProps.postprocessing.occlusion),
|
||||
dof: optionalParams(dof, dofParams, DofParams, oldCanvasProps.postprocessing.dof),
|
||||
bloom: optionalParams(bloom, bloomParams, BloomParams, oldCanvasProps.postprocessing.bloom),
|
||||
},
|
||||
cameraFog: optionalParams(fog, fogParams, CameraFogParams, oldCanvasProps.cameraFog),
|
||||
renderer: {
|
||||
...oldCanvasProps.renderer,
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
trackball: {
|
||||
...oldCanvasProps?.trackball,
|
||||
...(trackballAnimationName
|
||||
? {
|
||||
animate: {
|
||||
name: trackballAnimationName,
|
||||
params: {
|
||||
...TrackballControlsParams.animate.map(trackballAnimationName)?.defaultValue,
|
||||
...trackballAnimationParams
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resetCanvasProps(plugin: PluginContext) {
|
||||
const old = plugin.canvas3d?.props;
|
||||
plugin.canvas3d?.setProps({
|
||||
...old,
|
||||
postprocessing: {
|
||||
...old,
|
||||
outline: DefaultCanvas3DParams.postprocessing.outline,
|
||||
shadow: DefaultCanvas3DParams.postprocessing.shadow,
|
||||
occlusion: DefaultCanvas3DParams.postprocessing.occlusion,
|
||||
dof: DefaultCanvas3DParams.postprocessing.dof,
|
||||
bloom: DefaultCanvas3DParams.postprocessing.bloom,
|
||||
},
|
||||
cameraFog: DefaultCanvas3DParams.cameraFog,
|
||||
trackball: {
|
||||
...old?.trackball,
|
||||
animate: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ function makeNumericPaletteScale(props: MVSContinuousPaletteProps | MVSDiscreteP
|
||||
}
|
||||
}
|
||||
|
||||
function makeContinuousPaletteCheckpoints(props: MVSContinuousPaletteProps) {
|
||||
export function makeContinuousPaletteCheckpoints(props: MVSContinuousPaletteProps) {
|
||||
if (props.colors.colors.every(x => Array.isArray(x))) {
|
||||
// Explicit checkpoints
|
||||
const sorted = props.colors.colors.sort((a, b) => a[1] - b[1]);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { murmurHash3_128_fromBytes } from '../../../mol-data/util';
|
||||
import { StringLike } from '../../../mol-io/common/string-like';
|
||||
import { DataFormatProvider } from '../../../mol-plugin-state/formats/provider';
|
||||
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
|
||||
@@ -118,7 +118,7 @@ export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext
|
||||
// states.
|
||||
clearMVSXFileAssets(plugin);
|
||||
|
||||
const archiveId = `ni,fnv1a;${hashFnv32a(data)}`;
|
||||
const archiveId = `ni,MurmurHash3_128;${murmurHash3_128_fromBytes(data, 42)}`;
|
||||
let files: { [path: string]: Uint8Array };
|
||||
try {
|
||||
files = await unzip(runtimeCtx, data) as typeof files;
|
||||
@@ -166,6 +166,8 @@ export async function loadMVSData(plugin: PluginContext, data: MVSData | StringL
|
||||
} else {
|
||||
throw new Error(`Unknown MolViewSpec format: ${format}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function clearMVSXFileAssets(plugin: PluginContext) {
|
||||
@@ -180,7 +182,7 @@ function tryGetDownloadUrl(pso: SO.Data.String, plugin: PluginContext): string |
|
||||
}
|
||||
|
||||
/** Return a URI referencing a file within an archive, using ARCP scheme (https://arxiv.org/pdf/1809.06935.pdf).
|
||||
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,fnv1a;938511930')
|
||||
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,MurmurHash3_128;e6494f6be71f34c556f3de73d306780c')
|
||||
* `path` corresponds to the path to a file within the archive */
|
||||
function arcpUri(archiveId: string, path: string): string {
|
||||
return new URL(path, `arcp://${archiveId}/`).href;
|
||||
|
||||
@@ -39,8 +39,9 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { capitalize } from '../../../mol-util/string';
|
||||
import { rowsToExpression, rowToExpression } from '../helpers/selections';
|
||||
import { collectMVSReferences, decodeColor, isDefined } from '../helpers/utils';
|
||||
import { MolstarNode, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSNode } from '../tree/mvs/mvs-tree';
|
||||
import { addParamDefaults } from '../tree/generic/params-schema';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSNode, MVSTreeSchema } from '../tree/mvs/mvs-tree';
|
||||
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
@@ -97,6 +98,16 @@ export const MVSDownloadPrimitiveData = MVSTransform({
|
||||
},
|
||||
});
|
||||
|
||||
/* Cannot use MolstarSubtree<'primitives'>> because information about type of children would be lost and cause TypeScript errors in dependent code */
|
||||
interface PrimitivesSubtree {
|
||||
kind: 'primitives',
|
||||
params: MolstarNodeParams<'primitives'>,
|
||||
children?: {
|
||||
kind: 'primitive',
|
||||
params: MolstarNodeParams<'primitive'>,
|
||||
}[],
|
||||
}
|
||||
|
||||
export type MVSInlinePrimitiveData = typeof MVSInlinePrimitiveData
|
||||
export const MVSInlinePrimitiveData = MVSTransform({
|
||||
name: 'mvs-inline-primitive-data',
|
||||
@@ -104,7 +115,10 @@ export const MVSInlinePrimitiveData = MVSTransform({
|
||||
from: [SO.Root, SO.Molecule.Structure],
|
||||
to: MVSPrimitivesData,
|
||||
params: {
|
||||
node: PD.Value<MolstarSubtree<'primitives'>>(undefined as any, { isHidden: true }),
|
||||
node: PD.Value<PrimitivesSubtree>({
|
||||
kind: 'primitives',
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitives.params, {}),
|
||||
}, { isHidden: true }),
|
||||
},
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
@@ -135,17 +149,20 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
const context: PrimitiveBuilderContext = { ...a.data, structureRefs };
|
||||
|
||||
const snapshotKey = { snapshotKey: { ...SnapshotKey, defaultValue: a.data.options?.snapshot_key ?? '' } };
|
||||
const markdownCommands = { markdownCommands: { ...MarkdownCommands, defaultValue: a.data.node?.custom?.molstar_markdown_commands } };
|
||||
|
||||
const label = capitalize(params.kind);
|
||||
if (params.kind === 'mesh') {
|
||||
if (!hasPrimitiveKind(a.data, 'mesh')) return StateObject.Null;
|
||||
|
||||
const customMeshParams = a.data.node.custom?.molstar_mesh_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: {
|
||||
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, ...customMeshParams }),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveMesh(data, prev?.geometry),
|
||||
geometryUtils: Mesh.Utils,
|
||||
@@ -155,6 +172,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
|
||||
const options = a.data.options;
|
||||
const bgColor = options?.label_background_color;
|
||||
const customLabelParams = a.data.node.custom?.molstar_label_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
@@ -166,8 +184,10 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
tetherLength: options?.label_tether_length ?? 1,
|
||||
background: isDefined(bgColor),
|
||||
backgroundColor: isDefined(bgColor) ? decodeColor(bgColor) : undefined,
|
||||
...customLabelParams,
|
||||
}),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, props, prev: any) => buildPrimitiveLabels(data, prev?.geometry, props),
|
||||
geometryUtils: Text.Utils,
|
||||
@@ -175,12 +195,14 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
} else if (params.kind === 'lines') {
|
||||
if (!hasPrimitiveKind(a.data, 'line')) return StateObject.Null;
|
||||
|
||||
const customLineParams = a.data.node.custom?.molstar_line_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: {
|
||||
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, ...customLineParams }),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveLines(data, prev?.geometry),
|
||||
geometryUtils: Lines.Utils,
|
||||
@@ -209,7 +231,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
|
||||
const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils);
|
||||
await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
|
||||
|
||||
const pickable = !!(params as any).snapshotKey?.trim();
|
||||
const pickable = !!(params as any).snapshotKey?.trim() || !!(params as any).markdownCommands;
|
||||
if (pickable) {
|
||||
repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
|
||||
}
|
||||
@@ -223,7 +245,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
|
||||
await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx);
|
||||
b.data.sourceData = a.data;
|
||||
|
||||
const pickable = !!(newParams as any).snapshotKey?.trim();
|
||||
const pickable = !!(newParams as any).snapshotKey?.trim() || !!(newParams as any).markdownCommands;
|
||||
if (pickable) {
|
||||
b.data.repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
|
||||
}
|
||||
@@ -234,6 +256,7 @@ export const MVSShapeRepresentation3D = MVSTransform({
|
||||
});
|
||||
|
||||
const SnapshotKey = PD.Text('', { isEssential: true, disableInteractiveUpdates: true, description: 'Activate the snapshot with the provided key when clicking on the label' });
|
||||
const MarkdownCommands = PD.Value<any>(undefined, { isHidden: true });
|
||||
|
||||
/* **************************************************** */
|
||||
|
||||
|
||||
36
src/extensions/mvs/components/trajectory.ts
Normal file
36
src/extensions/mvs/components/trajectory.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { getTrajectory } from '../../../mol-plugin-state/transforms/model';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { ParamDefinition } from '../../../mol-util/param-definition';
|
||||
import { getMVSReferenceObject } from '../helpers/utils';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
export const MVSTrajectoryWithCoordinates = MVSTransform({
|
||||
name: 'trajectory-with-coordinates',
|
||||
display: { name: 'Trajectory with Coordinates', description: 'Create a trajectory from existing model and the provided coordinates.' },
|
||||
from: PluginStateObject.Molecule.Model,
|
||||
to: PluginStateObject.Molecule.Trajectory,
|
||||
params: {
|
||||
coordinatesRef: ParamDefinition.Text('', { isHidden: true }),
|
||||
}
|
||||
})({
|
||||
apply({ a, params, dependencies }) {
|
||||
return Task.create('Create trajectory from model/topology and coordinates', async ctx => {
|
||||
const coordinates = getMVSReferenceObject([PluginStateObject.Molecule.Coordinates], dependencies, params.coordinatesRef);
|
||||
|
||||
if (!coordinates) {
|
||||
throw new Error('Coordinates not found.');
|
||||
}
|
||||
|
||||
const trajectory = await getTrajectory(ctx, a, coordinates.data);
|
||||
const props = { label: 'Trajectory', description: `${trajectory.frameCount} model${trajectory.frameCount === 1 ? '' : 's'}` };
|
||||
return new PluginStateObject.Molecule.Trajectory(trajectory, props);
|
||||
});
|
||||
}
|
||||
});
|
||||
26
src/extensions/mvs/export.ts
Normal file
26
src/extensions/mvs/export.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Zip } from '../../mol-util/zip/zip';
|
||||
import { MVSData } from './mvs-data';
|
||||
|
||||
/**
|
||||
* Creates an MVSX zip file with from the provided data and assets
|
||||
*/
|
||||
export async function createMVSX(data: MVSData, assets: { name: string, content: string | Uint8Array }[]) {
|
||||
const encoder = new TextEncoder();
|
||||
const files: Record<string, Uint8Array> = {
|
||||
'index.mvsj': encoder.encode(JSON.stringify(data)),
|
||||
};
|
||||
for (const asset of assets) {
|
||||
files[asset.name] = typeof asset.content === 'string'
|
||||
? encoder.encode(asset.content)
|
||||
: asset.content;
|
||||
}
|
||||
|
||||
const zip = await Zip(files).run();
|
||||
return new Uint8Array(zip);
|
||||
}
|
||||
605
src/extensions/mvs/helpers/animation.ts
Normal file
605
src/extensions/mvs/helpers/animation.ts
Normal file
@@ -0,0 +1,605 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import * as EasingFns from '../../../mol-math/easing';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { EPSILON, Mat3, Mat4, Quat, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { produce } from '../../../mol-util/produce';
|
||||
import { makeContinuousPaletteCheckpoints, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from '../components/annotation-color-theme';
|
||||
import { palettePropsFromMVSPalette } from '../load-helpers';
|
||||
import { Snapshot } from '../mvs-data';
|
||||
import { MVSAnimationEasing, MVSAnimationNode, MVSAnimationSchema } from '../tree/animation/animation-tree';
|
||||
import { Tree } from '../tree/generic/tree-schema';
|
||||
import { addDefaults } from '../tree/generic/tree-utils';
|
||||
import { MVSTree } from '../tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
|
||||
export async function generateStateTransition(ctx: RuntimeContext, snapshot: Snapshot, snapshotIndex: number, snapshotCount: number) {
|
||||
if (!snapshot.animation) return undefined;
|
||||
|
||||
const tree = addDefaults(snapshot.animation, MVSAnimationSchema);
|
||||
const transitions = tree.children?.filter(child => child.kind === 'interpolate');
|
||||
if (!transitions?.length) return undefined;
|
||||
|
||||
const duration = Math.max(
|
||||
snapshot.animation.params?.duration_ms ?? 0,
|
||||
...transitions.map(t => (t.params.start_ms ?? 0) + t.params.duration_ms)
|
||||
);
|
||||
|
||||
const frames: [tree: MVSTree, time: number][] = [];
|
||||
const dt = tree.params?.frame_time_ms ?? (1000 / 60);
|
||||
const N = Math.ceil(duration / dt);
|
||||
|
||||
const nodeMap = makeNodeMap(snapshot.root, new Map(), []);
|
||||
const cache = new Map<any, InterpolationCacheEntry>();
|
||||
|
||||
const transitionGroups = groupTranstions(transitions);
|
||||
|
||||
let prevRoot: MVSTree | undefined;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
const t = i * dt;
|
||||
const root = createSnapshot(snapshot.root, transitionGroups, t, cache, nodeMap);
|
||||
|
||||
if (root === prevRoot || (prevRoot && deepEqual(root, prevRoot))) {
|
||||
frames[frames.length - 1][1] += dt;
|
||||
} else {
|
||||
frames.push([root, dt]);
|
||||
}
|
||||
|
||||
prevRoot = root;
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Generating transition for snapshot ${snapshotIndex + 1}/${snapshotCount}`, current: i + 1, max: N });
|
||||
}
|
||||
}
|
||||
|
||||
return { tree, frametimeMs: dt, frames };
|
||||
}
|
||||
|
||||
const EasingFnMap: Record<MVSAnimationEasing, (t: number) => number> = {
|
||||
'linear': t => t,
|
||||
'bounce-in': EasingFns.bounceIn,
|
||||
'bounce-out': EasingFns.bounceOut,
|
||||
'bounce-in-out': EasingFns.bounceInOut,
|
||||
'circle-in': EasingFns.circleIn,
|
||||
'circle-out': EasingFns.circleOut,
|
||||
'circle-in-out': EasingFns.circleInOut,
|
||||
'cubic-in': EasingFns.cubicIn,
|
||||
'cubic-out': EasingFns.cubicOut,
|
||||
'cubic-in-out': EasingFns.cubicInOut,
|
||||
'exp-in': EasingFns.expIn,
|
||||
'exp-out': EasingFns.expOut,
|
||||
'exp-in-out': EasingFns.expInOut,
|
||||
'quad-in': EasingFns.quadIn,
|
||||
'quad-out': EasingFns.quadOut,
|
||||
'quad-in-out': EasingFns.quadInOut,
|
||||
'sin-in': EasingFns.sinIn,
|
||||
'sin-out': EasingFns.sinOut,
|
||||
'sin-in-out': EasingFns.sinInOut,
|
||||
};
|
||||
|
||||
interface InterpolationCacheEntry {
|
||||
paletteFn?: (value: number) => Color,
|
||||
rotation?: { axis: Vec3, angle: number, start: Quat, end: Quat },
|
||||
}
|
||||
|
||||
function getTransitionKey(transition: MVSAnimationNode<'interpolate'>) {
|
||||
const prop = transition.params.property;
|
||||
if (Array.isArray(prop)) {
|
||||
return `${transition.params.target_ref}:${prop.join('.')}`;
|
||||
}
|
||||
return `${transition.params.target_ref}:${prop}`;
|
||||
}
|
||||
|
||||
function groupTranstions(transitions: MVSAnimationNode<'interpolate'>[]) {
|
||||
const map = new Map<string, MVSAnimationNode<'interpolate'>[]>();
|
||||
const groups: MVSAnimationNode<'interpolate'>[][] = [];
|
||||
for (const t of transitions) {
|
||||
const key = getTransitionKey(t);
|
||||
if (!map.has(key)) {
|
||||
const group: MVSAnimationNode<'interpolate'>[] = [];
|
||||
map.set(key, group);
|
||||
groups.push(group);
|
||||
}
|
||||
map.get(key)!.push(t);
|
||||
}
|
||||
for (const group of groups) {
|
||||
group.sort((a, b) => {
|
||||
const s = (a.params.start_ms ?? 0) - (b.params.start_ms ?? 0);
|
||||
if (s !== 0) return s;
|
||||
return a.params.duration_ms - b.params.duration_ms;
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function createSnapshot(tree: MVSTree, transitionGroups: MVSAnimationNode<'interpolate'>[][], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
|
||||
let modified = false;
|
||||
const ret = produce(tree, (draft) => {
|
||||
for (const transitionGroup of transitionGroups) {
|
||||
|
||||
const pivot = transitionGroup[0];
|
||||
const nodePath = nodeMap.get(pivot.params.target_ref);
|
||||
if (!nodePath) continue;
|
||||
|
||||
const node = select(draft, nodePath, 0);
|
||||
const target = pivot.params.property[0] === 'custom' ? node?.custom : node?.params;
|
||||
if (!target) continue;
|
||||
|
||||
|
||||
const offset = pivot.params.property[0] === 'custom' ? 1 : 0;
|
||||
|
||||
let transition: MVSAnimationNode<'interpolate'> = pivot;
|
||||
let previous: MVSAnimationNode<'interpolate'> | undefined;
|
||||
|
||||
for (let i = transitionGroup.length - 1; i > 0; i--) {
|
||||
const current = transitionGroup[i];
|
||||
const currentStart = current.params.start_ms ?? 0;
|
||||
if (time >= currentStart) {
|
||||
transition = current;
|
||||
previous = i > 0 ? transitionGroup[i - 1] : undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.has(transition)) {
|
||||
cache.set(transition, {});
|
||||
}
|
||||
|
||||
const cacheEntry: InterpolationCacheEntry = cache.get(transition)!;
|
||||
|
||||
const startTime: number = transition.params.start_ms ?? 0;
|
||||
const durationMs: number = transition.params.duration_ms ?? 0;
|
||||
const t = (time - startTime) / durationMs;
|
||||
|
||||
let next: any;
|
||||
if (transition.params.kind === 'transform_matrix') {
|
||||
next = processTransformMatrix(transition, target, clamp(t, 0, 1), cacheEntry, offset, previous);
|
||||
} else {
|
||||
next = processScalarLike(transition, target, t, cacheEntry, offset, previous);
|
||||
}
|
||||
|
||||
if (next === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modified = true;
|
||||
assign(target, transition.params.property, next, offset);
|
||||
}
|
||||
});
|
||||
return modified ? ret : tree;
|
||||
}
|
||||
|
||||
function applyFrequency(t: number, frequency: number, alternate: boolean) {
|
||||
let v = (t * (frequency || 1));
|
||||
if (v < 1) return v;
|
||||
|
||||
if (!alternate) {
|
||||
v = (v % 1);
|
||||
if (v === 0) return 1;
|
||||
return v;
|
||||
}
|
||||
|
||||
if (Math.abs(v - 1) < EPSILON) return 1;
|
||||
v = v % 2;
|
||||
if (v > 1) return 2 - v;
|
||||
return v;
|
||||
}
|
||||
|
||||
function getPreviousScalarEnd(previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (!previous || previous.params.kind === 'transform_matrix') return undefined;
|
||||
return previous.params.end;
|
||||
}
|
||||
|
||||
function processScalarLike(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cacheEntry: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind === 'transform_matrix') return;
|
||||
if (previous && previous.params.kind === 'transform_matrix') return;
|
||||
|
||||
const startBase = transition.params.start ?? getPreviousScalarEnd(previous) ?? select(target, transition.params.property, offset);
|
||||
if (transition.params.kind === 'color' && !cacheEntry.paletteFn) {
|
||||
cacheEntry.paletteFn = makePaletteFunction(transition, startBase, transition.params.end as ColorT | undefined);
|
||||
}
|
||||
|
||||
const paletteFn = cacheEntry.paletteFn!;
|
||||
|
||||
const startValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(0))
|
||||
: startBase;
|
||||
const endValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(1))
|
||||
: transition.params.end;
|
||||
|
||||
if (time <= 0) return startValue;
|
||||
else if (time >= 1 - EPSILON && !transition.params.alternate_direction) return endValue;
|
||||
|
||||
let t = clamp(time, 0, 1);
|
||||
t = applyFrequency(t, transition.params.frequency ?? 1, !!transition.params.alternate_direction);
|
||||
|
||||
const easing = EasingFnMap[transition.params.easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
t = easing(t);
|
||||
|
||||
if (transition.params.kind === 'scalar') {
|
||||
return interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.discrete);
|
||||
} else if (transition.params.kind === 'vec3') {
|
||||
return interpolateVectors(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.spherical);
|
||||
} else if (transition.params.kind === 'rotation_matrix') {
|
||||
return interpolateRotation(startValue, endValue, t, transition.params.noise_magnitude ?? 0, cacheEntry);
|
||||
} else if (transition.params.kind === 'color') {
|
||||
const color = paletteFn(t);
|
||||
return Color.toHexStyle(color);
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviousMatrixEnd(previous: MVSAnimationNode<'interpolate'> | undefined, prop: 'rotation_start' | 'translation_start' | 'scale_start') {
|
||||
if (!previous || previous.params.kind !== 'transform_matrix') return undefined;
|
||||
return previous.params[prop];
|
||||
}
|
||||
|
||||
const TransformState = {
|
||||
pivotTranslation: Mat4(),
|
||||
pivotTranslationInv: Mat4(),
|
||||
rotation: Mat4(),
|
||||
scale: Mat4(),
|
||||
translation: Mat4(),
|
||||
pivotNeg: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cache: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind !== 'transform_matrix') return;
|
||||
if (previous && previous.params.kind !== 'transform_matrix') return;
|
||||
|
||||
const transform = select(target, transition.params.property, offset) ?? Mat4.identity();
|
||||
|
||||
const startRotation = transition.params.rotation_start ?? getPreviousMatrixEnd(previous, 'rotation_start') ?? Mat3.fromMat4(Mat3(), transform);
|
||||
const startTranslation = transition.params.translation_start ?? getPreviousMatrixEnd(previous, 'translation_start') ?? Mat4.getTranslation(Vec3(), transform);
|
||||
const startScale = transition.params.scale_start ?? getPreviousMatrixEnd(previous, 'scale_start') ?? Mat4.getScaling(Vec3(), transform);
|
||||
|
||||
const endRotation = transition.params.rotation_end;
|
||||
const endTranslation = transition.params.translation_end;
|
||||
const endScale = transition.params.scale_end;
|
||||
|
||||
let rotation, translation, scale;
|
||||
|
||||
if (time <= 0) {
|
||||
rotation = startRotation as Mat3;
|
||||
translation = startTranslation as Vec3;
|
||||
scale = startScale as Vec3;
|
||||
} else {
|
||||
const clampedTime = clamp(time, 0, 1);
|
||||
|
||||
let t = applyFrequency(clampedTime, transition.params.rotation_frequency ?? 1, !!transition.params.rotation_alternate_direction);
|
||||
let easing = EasingFnMap[transition.params.rotation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
rotation = interpolateRotation(startRotation as Mat3, endRotation as Mat3, easing(t), transition.params.rotation_noise_magnitude ?? 0, cache);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.translation_frequency ?? 1, !!transition.params.translation_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.translation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
translation = interpolateVec3(startTranslation as Vec3, endTranslation as Vec3 | undefined, easing(t), transition.params.translation_noise_magnitude ?? 0, false);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.scale_frequency ?? 1, !!transition.params.scale_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.scale_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
scale = interpolateVec3(startScale as Vec3, endScale as Vec3 | undefined, easing(t), transition.params.scale_noise_magnitude ?? 0, false);
|
||||
}
|
||||
|
||||
const pivot = transition.params.pivot ?? Vec3.zero();
|
||||
|
||||
Mat4.fromTranslation(TransformState.translation, translation);
|
||||
Mat4.fromScaling(TransformState.scale, scale);
|
||||
Mat4.setIdentity(TransformState.rotation);
|
||||
Mat4.fromMat3(TransformState.rotation, rotation);
|
||||
Mat4.fromTranslation(TransformState.pivotTranslation, pivot as Vec3);
|
||||
Mat4.fromTranslation(TransformState.pivotTranslationInv, Vec3.negate(TransformState.pivotNeg, pivot as Vec3));
|
||||
|
||||
// translation . pivot . rotation . scale . pivotInv
|
||||
const result = Mat4();
|
||||
Mat4.mul(result, TransformState.scale, TransformState.pivotTranslationInv);
|
||||
Mat4.mul(result, TransformState.rotation, result);
|
||||
Mat4.mul(result, TransformState.translation, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function interpolateScalars(start: number | number[], end: number | number[] | undefined, t: number, noise: number, discrete: boolean) {
|
||||
if (Array.isArray(start)) {
|
||||
const ret = Array.from<number>({ length: start.length }).fill(0.1);
|
||||
if (!end || !Array.isArray(end)) {
|
||||
for (let i = 0; i < start.length; i++) {
|
||||
ret[i] = interpolateScalar(start[i], end, t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let i = 0; i < start.length; i++) {
|
||||
ret[i] = interpolateScalar(start[i], end[i], t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (Array.isArray(end)) {
|
||||
const ret = Array.from<number>({ length: end.length }).fill(0.1);
|
||||
for (let i = 0; i < end.length; i++) {
|
||||
ret[i] = interpolateScalar(start, end[i], t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return interpolateScalar(start, end, t, noise, discrete);
|
||||
}
|
||||
|
||||
function interpolateScalar(start: number, end: number | undefined, t: number, noise: number, discrete: boolean) {
|
||||
let v = typeof end === 'number' ? lerp(start, end, t) : start;
|
||||
if (noise) {
|
||||
v += (Math.random() - 0.5) * noise;
|
||||
}
|
||||
if (discrete) {
|
||||
v = Math.round(v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const InterpolateVectorsState = {
|
||||
start: Vec3(),
|
||||
end: Vec3(),
|
||||
v: Vec3(),
|
||||
};
|
||||
function interpolateVectors(start: number[], end: number[] | undefined, t: number, noise: number, isSpherical: boolean) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
const ret: number[] = Array.from<number>({ length: start.length }).fill(0.1);
|
||||
|
||||
for (let i = 0; i < start.length; i += 3) {
|
||||
const s = Vec3.fromArray(InterpolateVectorsState.start, start, i);
|
||||
|
||||
let v: Vec3;
|
||||
if (end) {
|
||||
const e = Vec3.fromArray(InterpolateVectorsState.end, end, i);
|
||||
v = isSpherical
|
||||
? Vec3.slerp(InterpolateVectorsState.v, s, e, t)
|
||||
: Vec3.lerp(InterpolateVectorsState.v, s, e, t);
|
||||
} else {
|
||||
v = Vec3.clone(s);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(Vec3Noise, noise);
|
||||
Vec3.add(v, v, Vec3Noise);
|
||||
}
|
||||
|
||||
Vec3.toArray(v, ret, i);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const Vec3Noise = Vec3();
|
||||
function interpolateVec3(start: Vec3, end: Vec3 | undefined, t: number, noise: number, isSpherical: boolean) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
let v: Vec3;
|
||||
|
||||
if (end) {
|
||||
v = isSpherical
|
||||
? Vec3.slerp(Vec3(), start, end, t)
|
||||
: Vec3.lerp(Vec3(), start, end, t);
|
||||
} else {
|
||||
v = Vec3.clone(start);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(Vec3Noise, noise);
|
||||
Vec3.add(v, v, Vec3Noise);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const RotationState = {
|
||||
start: Quat(),
|
||||
end: Quat(),
|
||||
v: Quat(),
|
||||
noise: Quat(),
|
||||
axis: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function interpolateRotation(start: Mat3, end: Mat3 | undefined, t: number, noise: number, cache: InterpolationCacheEntry) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
if (end) {
|
||||
if (!cache.rotation) {
|
||||
cache.rotation = {
|
||||
...relativeAxisAngle(start, end),
|
||||
start: Quat.fromMat3(Quat(), start),
|
||||
end: Quat.fromMat3(Quat(), end),
|
||||
};
|
||||
}
|
||||
|
||||
const { axis, angle, start: startQ, end: endQ } = cache.rotation;
|
||||
|
||||
if (angle < 1e-6) {
|
||||
// start ≈ end: make a clean spin about the detected (or default) axis
|
||||
Quat.setAxisAngle(RotationState.v, axis, t * 2 * Math.PI); // Make a full turn
|
||||
} else {
|
||||
// Normal case: stick with your existing slerp between start/end
|
||||
Quat.slerp(RotationState.v, startQ, endQ, t);
|
||||
}
|
||||
} else {
|
||||
Quat.fromMat3(RotationState.v, start);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(RotationState.axis, 1);
|
||||
Quat.setAxisAngle(RotationState.noise, RotationState.axis, 2 * Math.PI * noise * (Math.random() - 0.5));
|
||||
Quat.multiply(RotationState.v, RotationState.noise, RotationState.v);
|
||||
}
|
||||
Mat4.fromQuat(RotationState.temp, RotationState.v);
|
||||
return Mat3.fromMat4(Mat3(), RotationState.temp);
|
||||
}
|
||||
|
||||
function select(params: any, path: string | (string | number)[], offset: number) {
|
||||
if (typeof path === 'string') {
|
||||
return params?.[path];
|
||||
}
|
||||
|
||||
let f = params;
|
||||
for (let i = offset; i < path.length; i++) {
|
||||
if (!f) break;
|
||||
f = f[path[i]];
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
function assign(params: any, path: string | (string | number)[], value: any, offset: number) {
|
||||
if (!params) return;
|
||||
|
||||
if (typeof path === 'string') {
|
||||
params[path] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
let f = params;
|
||||
for (let i = offset; i < path.length; i++) {
|
||||
if (!f) break;
|
||||
if (i === path.length - 1) {
|
||||
f[path[i]] = value;
|
||||
} else {
|
||||
f = f[path[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeNodeMap(tree: Tree, map: Map<string, (string | number)[]>, currentPath: (string | number)[]) {
|
||||
if (tree.ref) {
|
||||
map.set(tree.ref, [...currentPath]);
|
||||
}
|
||||
|
||||
if (!tree.children) return map;
|
||||
|
||||
currentPath.push('children');
|
||||
for (let i = 0; i < tree.children.length; i++) {
|
||||
const child = tree.children[i];
|
||||
currentPath.push(i);
|
||||
makeNodeMap(child, map, currentPath);
|
||||
currentPath.pop();
|
||||
}
|
||||
currentPath.pop();
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function makePaletteFunction(props: MVSAnimationNode<'interpolate'>, start: ColorT | undefined | null, end: ColorT | undefined | null): ((value: number) => Color) | undefined {
|
||||
if (props.params.kind !== 'color') return undefined;
|
||||
|
||||
const params = props.params.palette
|
||||
? palettePropsFromMVSPalette(props.params.palette)
|
||||
: palettePropsFromMVSPalette({ kind: 'continuous', colors: [start ?? 'black', end ?? start ?? 'black'] });
|
||||
if (params.name === 'discrete') return makePaletteFunctionDiscrete(params.params);
|
||||
if (params.name === 'continuous') return makePaletteFunctionContinuous(params.params);
|
||||
throw new Error(`NotImplementedError: makePaletteFunction for ${(props as any).name}`);
|
||||
}
|
||||
|
||||
|
||||
function makePaletteFunctionDiscrete(props: MVSDiscretePaletteProps): (value: number) => Color {
|
||||
const defaultColor = Color(0x0);
|
||||
if (props.colors.length === 0) return () => defaultColor;
|
||||
|
||||
return (value: number) => {
|
||||
const x = clamp(value, 0, 1);
|
||||
for (let i = props.colors.length - 1; i >= 0; i--) {
|
||||
const { color, fromValue, toValue } = props.colors[i];
|
||||
if (fromValue <= x && x <= toValue) return color;
|
||||
}
|
||||
return defaultColor;
|
||||
};
|
||||
}
|
||||
|
||||
function makePaletteFunctionContinuous(props: MVSContinuousPaletteProps): (value: number) => Color {
|
||||
const defaultColor = Color(0x0);
|
||||
const { colors, checkpoints } = makeContinuousPaletteCheckpoints(props);
|
||||
if (colors.length === 0) return () => defaultColor;
|
||||
|
||||
const underflowColor = props.setUnderflowColor ? props.underflowColor : defaultColor;
|
||||
const overflowColor = props.setOverflowColor ? props.overflowColor : defaultColor;
|
||||
|
||||
return (value: number) => {
|
||||
const x = clamp(value, 0, 1);
|
||||
const gteIdx = SortedArray.findPredecessorIndex(checkpoints, x); // Index of the first greater or equal checkpoint
|
||||
if (gteIdx === 0) {
|
||||
if (x === checkpoints[0]) return colors[0];
|
||||
else return underflowColor;
|
||||
}
|
||||
if (gteIdx === checkpoints.length) {
|
||||
return overflowColor;
|
||||
}
|
||||
const q = (x - checkpoints[gteIdx - 1]) / (checkpoints[gteIdx] - checkpoints[gteIdx - 1]);
|
||||
return Color.interpolate(colors[gteIdx - 1], colors[gteIdx], q);
|
||||
};
|
||||
}
|
||||
|
||||
const RelativeAxisAngleState = {
|
||||
Rt: Mat3(),
|
||||
R: Mat3(),
|
||||
};
|
||||
function relativeAxisAngle(start: Mat3, end: Mat3): { axis: Vec3, angle: number } {
|
||||
// R_rel = end * start^T
|
||||
const R0 = start, R1 = end;
|
||||
const Rt = Mat3.transpose(RelativeAxisAngleState.Rt, R0);
|
||||
const R = Mat3.mul(RelativeAxisAngleState.R, R1, Rt);
|
||||
|
||||
const tr = R[0] + R[4] + R[8]; // trace
|
||||
let angle = Math.acos(clamp((tr - 1) * 0.5, -1, 1)); // in [0, π]
|
||||
const axis = Vec3();
|
||||
|
||||
const eps = 1e-6;
|
||||
const sinA = Math.sin(angle);
|
||||
|
||||
if (angle < eps) {
|
||||
// Near identity: axis undefined; return any unit axis (choose something stable)
|
||||
Vec3.set(axis, 0, 0, 1);
|
||||
angle = 0.0;
|
||||
return { axis, angle };
|
||||
}
|
||||
|
||||
if (Math.PI - angle > 1e-4) {
|
||||
// General case
|
||||
axis[0] = (R[5] - R[7]) / (2 * sinA); // (r32 - r23)
|
||||
axis[1] = (R[6] - R[2]) / (2 * sinA); // (r13 - r31)
|
||||
axis[2] = (R[1] - R[3]) / (2 * sinA); // (r21 - r12)
|
||||
Vec3.normalize(axis, axis);
|
||||
return { axis, angle };
|
||||
}
|
||||
|
||||
// angle ~ π: use diagonal-based extraction for stability
|
||||
// Compute squared components then pick the largest to avoid precision loss
|
||||
const xx = Math.max(0, (R[0] + 1) * 0.5);
|
||||
const yy = Math.max(0, (R[4] + 1) * 0.5);
|
||||
const zz = Math.max(0, (R[8] + 1) * 0.5);
|
||||
|
||||
let x = Math.sqrt(xx), y = Math.sqrt(yy), z = Math.sqrt(zz);
|
||||
|
||||
if (x >= y && x >= z) {
|
||||
x = Math.max(x, 1e-8);
|
||||
y = (R[1] + R[3]) / (4 * x);
|
||||
z = (R[2] + R[6]) / (4 * x);
|
||||
Vec3.set(axis, x, y, z);
|
||||
} else if (y >= x && y >= z) {
|
||||
y = Math.max(y, 1e-8);
|
||||
x = (R[1] + R[3]) / (4 * y);
|
||||
z = (R[5] + R[7]) / (4 * y);
|
||||
Vec3.set(axis, x, y, z);
|
||||
} else {
|
||||
z = Math.max(z, 1e-8);
|
||||
x = (R[2] + R[6]) / (4 * z);
|
||||
y = (R[5] + R[7]) / (4 * z);
|
||||
Vec3.set(axis, x, y, z);
|
||||
}
|
||||
|
||||
Vec3.normalize(axis, axis);
|
||||
return { axis, angle: Math.PI };
|
||||
}
|
||||
@@ -100,7 +100,10 @@ export type ElementOfSet<S> = S extends Set<infer T> ? T : never
|
||||
|
||||
/** Convert `colorString` (either X11 color name like 'magenta' or hex code like '#ff00ff') to Color.
|
||||
* Return `undefined` if `colorString` cannot be converted. */
|
||||
export function decodeColor(colorString: string | undefined | null): Color | undefined {
|
||||
export function decodeColor(colorString: string | number | undefined | null): Color | undefined {
|
||||
if (typeof colorString === 'number') {
|
||||
return Color(colorString);
|
||||
}
|
||||
return _decodeColor(colorString);
|
||||
}
|
||||
|
||||
@@ -149,4 +152,25 @@ export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], depe
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getMVSReferenceObject<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject> | undefined, ref: string): StateObject | undefined {
|
||||
if (!dependencies) return undefined;
|
||||
|
||||
for (const key of Object.keys(dependencies)) {
|
||||
const o = dependencies[key];
|
||||
let okType = false;
|
||||
for (const t of type) {
|
||||
if (t.is(o)) {
|
||||
okType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!okType || !o.tags) continue;
|
||||
for (const tag of o.tags) {
|
||||
if (tag.startsWith('mvs-ref:')) {
|
||||
if (tag.substring(8) === ref) return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export function loadTreeVirtual<TTree extends Tree, TContext>(
|
||||
) {
|
||||
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
|
||||
loadTreeInUpdate(updateRoot, tree, loadingActions, context, options);
|
||||
const stateTree: StateTree = updateRoot.update.getTree();
|
||||
const stateTree: StateTree = updateRoot.update.getTree({ useHashVersion: true });
|
||||
const stateSnapshot: State.Snapshot = { tree: StateTree.toJSON(stateTree) };
|
||||
const pluginStateSnapshot: PluginState.Snapshot = { id: UUID.create22(), data: stateSnapshot };
|
||||
return pluginStateSnapshot;
|
||||
|
||||
@@ -62,6 +62,19 @@ export function transformFromRotationTranslation(rotation: number[] | null | und
|
||||
return T;
|
||||
}
|
||||
|
||||
export function decomposeRotationMatrix(rotation: number[] | null | undefined) {
|
||||
if (rotation && rotation.length !== 9) throw new Error(`'rotation' param for 'transform' node must be array of 9 elements, found ${rotation}`);
|
||||
if (rotation) {
|
||||
const rotMatrix = Mat3.fromArray(Mat3(), rotation, 0);
|
||||
ensureRotationMatrix(rotMatrix, rotMatrix);
|
||||
const quat = Quat.fromMat3(Quat(), rotMatrix);
|
||||
const axis = Vec3();
|
||||
const angle = Quat.getAxisAngle(axis, quat) * 180 / Math.PI;
|
||||
return { axis, angle };
|
||||
}
|
||||
return { axis: Vec3.create(1, 0, 0), angle: 0 };
|
||||
}
|
||||
|
||||
/** Adjust values in a close-to-rotation matrix `a` to ensure it is a proper rotation matrix
|
||||
* (i.e. its columns and rows are orthonormal and determinant equal to 1, within available precission). */
|
||||
function ensureRotationMatrix(out: Mat3, a: Mat3) {
|
||||
@@ -111,11 +124,32 @@ function transformProps(node: MolstarSubtree, kind: 'transform' | 'instance') {
|
||||
for (const transform of transforms) {
|
||||
let matrix: Mat4 | undefined = transform.params.matrix as Mat4 | undefined;
|
||||
if (!matrix) {
|
||||
const { rotation, translation } = transform.params;
|
||||
const { rotation, translation, rotation_center } = transform.params;
|
||||
if (rotation_center) {
|
||||
const axisAngle = decomposeRotationMatrix(rotation);
|
||||
result.push({
|
||||
params: {
|
||||
transform: {
|
||||
name: 'components',
|
||||
params: {
|
||||
translation: translation ? Vec3.fromArray(Vec3(), translation, 0) : Vec3.create(0, 0, 0),
|
||||
angle: axisAngle.angle,
|
||||
axis: axisAngle.axis,
|
||||
rotationCenter: rotation_center === 'centroid'
|
||||
? { name: 'centroid', params: {} }
|
||||
: { name: 'point', params: { point: Vec3.fromArray(Vec3(), rotation_center, 0) } }
|
||||
}
|
||||
}
|
||||
},
|
||||
ref: transform.ref
|
||||
});
|
||||
continue;
|
||||
}
|
||||
matrix = transformFromRotationTranslation(rotation, translation);
|
||||
}
|
||||
result.push({ params: { transform: { name: 'matrix', params: { data: matrix, transpose: false } } }, ref: transform.ref });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -335,10 +369,20 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
type: { name: 'cartoon', params: { alpha, tubularHelices: params.tubular_helices } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'backbone':
|
||||
return {
|
||||
type: { name: 'backbone', params: { alpha } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'ball_and_stick':
|
||||
return {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: (params.size_factor ?? 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
};
|
||||
case 'line':
|
||||
return {
|
||||
type: { name: 'line', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'spacefill':
|
||||
return {
|
||||
type: { name: 'spacefill', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
@@ -348,11 +392,15 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
return {
|
||||
type: { name: 'carbohydrate', params: { alpha, sizeFactor: params.size_factor ?? 1 } },
|
||||
};
|
||||
case 'surface':
|
||||
case 'surface': {
|
||||
return {
|
||||
type: { name: 'molecular-surface', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
type: {
|
||||
name: params.surface_type === 'gaussian' ? 'gaussian-surface' : 'molecular-surface',
|
||||
params: { alpha, ignoreHydrogens: params.ignore_hydrogens }
|
||||
},
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
@@ -364,8 +412,8 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
|
||||
if (clip) {
|
||||
base.type!.params = { ...base.type?.params, clip };
|
||||
}
|
||||
if (node.custom?.molstar_reprepresentation_params) {
|
||||
base.type!.params = { ...base.type!.params, ...node.custom.molstar_reprepresentation_params };
|
||||
if (node.custom?.molstar_representation_params) {
|
||||
base.type!.params = { ...base.type!.params, ...node.custom.molstar_representation_params };
|
||||
}
|
||||
return base;
|
||||
}
|
||||
@@ -509,7 +557,7 @@ function appliesColorToWholeRepr(node: MolstarNode<'color' | 'color_from_uri' |
|
||||
|
||||
const FALLBACK_COLOR = decodeColor(DefaultColor)!;
|
||||
|
||||
function palettePropsFromMVSPalette(palette: MolstarNode<'color_from_uri' | 'color_from_source'>['params']['palette']): MVSAnnotationColorThemeProps['palette'] {
|
||||
export function palettePropsFromMVSPalette(palette: MolstarNode<'color_from_uri' | 'color_from_source'>['params']['palette']): MVSAnnotationColorThemeProps['palette'] {
|
||||
if (!palette) {
|
||||
return { name: 'direct', params: {} };
|
||||
}
|
||||
|
||||
@@ -8,15 +8,17 @@
|
||||
|
||||
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { Download, ParseCcp4, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB } from '../../mol-plugin-state/transforms/model';
|
||||
import { Download, ParseCif, ParseCcp4 } from '../../mol-plugin-state/transforms/data';
|
||||
import { CoordinatesFromLammpstraj, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { VolumeFromCcp4, VolumeFromDensityServerCif } from '../../mol-plugin-state/transforms/volume';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectSelector, StateTree } from '../../mol-state';
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { MolViewSpec } from './behavior';
|
||||
import { createPluginStateSnapshotCamera, modifyCanvasProps } from './camera';
|
||||
import { createPluginStateSnapshotCamera, modifyCanvasProps, resetCanvasProps } from './camera';
|
||||
import { MVSAnnotationsProvider } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponent } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
|
||||
@@ -24,15 +26,18 @@ import { CustomLabelProps, CustomLabelRepresentationProvider } from './component
|
||||
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
|
||||
import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-prop';
|
||||
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData, MVSShapeRepresentation3D } from './components/primitives';
|
||||
import { MVSTrajectoryWithCoordinates } from './components/trajectory';
|
||||
import { generateStateTransition } from './helpers/animation';
|
||||
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
|
||||
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
|
||||
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, MVSData_States, SnapshotMetadata } from './mvs-data';
|
||||
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
|
||||
import { MVSAnimationNode } from './tree/animation/animation-tree';
|
||||
import { validateTree } from './tree/generic/tree-schema';
|
||||
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
|
||||
import { type MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
export interface MVSLoadOptions {
|
||||
@@ -49,12 +54,24 @@ export interface MVSLoadOptions {
|
||||
doNotReportErrors?: boolean
|
||||
}
|
||||
|
||||
export function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
const task = Task.create('Load MVS', ctx => _loadMVS(ctx, plugin, data, options));
|
||||
return plugin.runTask(task);
|
||||
}
|
||||
|
||||
/** Load a MolViewSpec (MVS) state(s) into the Mol* plugin as plugin state snapshots. */
|
||||
export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
plugin.errorContext.clear('mvs');
|
||||
try {
|
||||
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
|
||||
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
|
||||
|
||||
// Stop any currently running audio
|
||||
plugin.managers.markdownExtensions.audio.dispose();
|
||||
|
||||
// Reset canvas props to default so that modifyCanvasProps works as expected
|
||||
resetCanvasProps(plugin);
|
||||
|
||||
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
|
||||
const multiData: MVSData_States = data.kind === 'multiple' ? data : MVSData.stateToStates(data);
|
||||
const entries: PluginStateSnapshotManager.Entry[] = [];
|
||||
@@ -68,11 +85,16 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVS
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
snapshot.root,
|
||||
snapshot.animation,
|
||||
{ ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms },
|
||||
options
|
||||
);
|
||||
await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length);
|
||||
entries.push(entry);
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'Loading MVS...', current: i, max: multiData.snapshots.length });
|
||||
}
|
||||
}
|
||||
if (!options.appendSnapshots) {
|
||||
plugin.managers.snapshot.clear();
|
||||
@@ -80,6 +102,7 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVS
|
||||
for (const entry of entries) {
|
||||
plugin.managers.snapshot.add(entry);
|
||||
}
|
||||
|
||||
if (entries.length > 0) {
|
||||
await PluginCommands.State.Snapshots.Apply(plugin, { id: entries[0].snapshot.id });
|
||||
}
|
||||
@@ -101,24 +124,65 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVS
|
||||
}
|
||||
}
|
||||
|
||||
async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext, parentEntry: PluginStateSnapshotManager.Entry, parent: Snapshot, options: MVSLoadOptions, snapshotIndex: number, snapshotCount: number) {
|
||||
const transitions = await generateStateTransition(ctx, parent, snapshotIndex, snapshotCount);
|
||||
if (!transitions?.frames.length) return;
|
||||
|
||||
const animation: PluginState.StateTransition = {
|
||||
autoplay: !!transitions.tree.params?.autoplay,
|
||||
loop: !!transitions.tree.params?.loop,
|
||||
frames: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < transitions.frames.length; i++) {
|
||||
const frame = transitions.frames[i];
|
||||
const molstarTree = convertMvsToMolstar(frame[0], options.sourceUrl);
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
parent.animation,
|
||||
{ ...parent.metadata, previousTransitionDurationMs: transitions.frametimeMs },
|
||||
options
|
||||
);
|
||||
|
||||
StateTree.reuseTransformParams(entry.snapshot.data!.tree, parentEntry.snapshot.data!.tree);
|
||||
|
||||
animation.frames.push({
|
||||
durationInMs: frame[1],
|
||||
data: entry.snapshot.data!,
|
||||
camera: transitions.tree.params?.include_camera ? entry.snapshot.camera : undefined,
|
||||
canvas3d: transitions.tree.params?.include_canvas ? entry.snapshot.canvas3d : undefined,
|
||||
});
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Loading animation for snapshot ${snapshotIndex + 1}/${snapshotCount}...`, current: i + 1, max: transitions.frames.length });
|
||||
}
|
||||
}
|
||||
|
||||
parentEntry.snapshot.transition = animation;
|
||||
}
|
||||
|
||||
function molstarTreeToEntry(
|
||||
plugin: PluginContext,
|
||||
tree: MolstarTree,
|
||||
mvsTree: MVSTree,
|
||||
animation: MVSAnimationNode<'animation'> | undefined,
|
||||
metadata: SnapshotMetadata & { previousTransitionDurationMs?: number },
|
||||
options: { keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }
|
||||
) {
|
||||
const context = MolstarLoadingContext.create();
|
||||
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { replaceExisting: true, extensions: options?.extensions ?? BuiltinLoadingExtensions });
|
||||
snapshot.canvas3d = {
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, mvsTree.custom) : undefined,
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, animation) : undefined,
|
||||
};
|
||||
if (!options?.keepCamera) {
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
|
||||
}
|
||||
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
|
||||
|
||||
if (tree.custom?.molstar_on_load_markdown_commands) {
|
||||
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
|
||||
}
|
||||
|
||||
const entryParams: PluginStateSnapshotManager.EntryParams = {
|
||||
key: metadata.key,
|
||||
name: metadata.title,
|
||||
@@ -165,31 +229,72 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
},
|
||||
parse(updateParent: UpdateTarget, node: MolstarNode<'parse'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
} else if (format === 'pdb') {
|
||||
return updateParent;
|
||||
} else if (format === 'map') {
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
switch (format) {
|
||||
case 'cif':
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
case 'gro':
|
||||
case 'xyz':
|
||||
case 'mol':
|
||||
case 'sdf':
|
||||
case 'mol2':
|
||||
case 'xtc':
|
||||
case 'lammpstrj':
|
||||
return updateParent;
|
||||
case 'map':
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
default:
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
coordinates(updateParent: UpdateTarget, node: MolstarNode<'coordinates'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
switch (format) {
|
||||
case 'xtc':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromXtc);
|
||||
case 'lammpstrj':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromLammpstraj);
|
||||
default:
|
||||
console.error(`Unknown format in "coordinates" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
trajectory(updateParent: UpdateTarget, node: MolstarNode<'trajectory'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
|
||||
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
|
||||
blockIndex: node.params.block_index ?? undefined,
|
||||
});
|
||||
} else if (format === 'pdb') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "trajectory" node: "${format}"`);
|
||||
return undefined;
|
||||
switch (format) {
|
||||
case 'cif':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
|
||||
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
|
||||
blockIndex: node.params.block_index ?? undefined,
|
||||
});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { isPdbqt: format === 'pdbqt' });
|
||||
case 'gro':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromGRO);
|
||||
case 'xyz':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromXYZ);
|
||||
case 'mol':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMOL);
|
||||
case 'sdf':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromSDF);
|
||||
case 'mol2':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMOL2);
|
||||
case 'lammpstrj':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromLammpsTrajData);
|
||||
default:
|
||||
console.error(`Unknown format in "trajectory" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
trajectory_with_coordinates(updateParent: UpdateTarget, node: MolstarNode<'trajectory_with_coordinates'>): UpdateTarget | undefined {
|
||||
const result = UpdateTarget.apply(updateParent, MVSTrajectoryWithCoordinates, {
|
||||
coordinatesRef: node.params.coordinates_ref,
|
||||
});
|
||||
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
|
||||
},
|
||||
model(updateParent: UpdateTarget, node: MolstarSubtree<'model'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const annotations = collectAnnotationReferences(node, context);
|
||||
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {
|
||||
@@ -312,7 +417,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
},
|
||||
primitives(updateParent: UpdateTarget, tree: MolstarSubtree<'primitives'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const refs = getPrimitiveStructureRefs(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree });
|
||||
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree as any });
|
||||
return applyPrimitiveVisuals(data, refs);
|
||||
},
|
||||
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { treeValidationIssues } from './tree/generic/tree-schema';
|
||||
import { treeToString } from './tree/generic/tree-utils';
|
||||
import { MVSAnimationSchema, MVSAnimationTree } from './tree/animation/animation-tree';
|
||||
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
|
||||
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
@@ -53,6 +55,8 @@ export interface Snapshot {
|
||||
root: MVSTree,
|
||||
/** Associated metadata */
|
||||
metadata: SnapshotMetadata,
|
||||
/** Optional animation */
|
||||
animation?: MVSAnimationTree,
|
||||
}
|
||||
|
||||
/** MVSData with a single state */
|
||||
@@ -189,7 +193,14 @@ function majorVersion(semanticVersion: string | number): number | undefined {
|
||||
|
||||
function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: { noExtra?: boolean } = {}): string[] | undefined {
|
||||
if (snapshot.root === undefined) return [`"root" missing in snapshot`];
|
||||
return treeValidationIssues(MVSTreeSchema, snapshot.root, options);
|
||||
const state = treeValidationIssues(MVSTreeSchema, snapshot.root, options);
|
||||
const animation = 'animation' in snapshot && snapshot.animation !== undefined
|
||||
? treeValidationIssues(MVSAnimationSchema, snapshot.animation, options)
|
||||
: undefined;
|
||||
if (state && animation) return [...state, ...animation];
|
||||
if (state) return state;
|
||||
if (animation) return animation;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
|
||||
|
||||
143
src/extensions/mvs/tree/animation/animation-tree.ts
Normal file
143
src/extensions/mvs/tree/animation/animation-tree.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, int, list, OptionalField, RequiredField, str, union, nullable, literal, ValueFor } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
|
||||
import { ColorT, ContinuousPalette, DiscretePalette, Matrix, Vector3 } from '../mvs/param-types';
|
||||
|
||||
const Easing = literal(
|
||||
'linear',
|
||||
'bounce-in', 'bounce-out', 'bounce-in-out',
|
||||
'circle-in', 'circle-out', 'circle-in-out',
|
||||
'cubic-in', 'cubic-out', 'cubic-in-out',
|
||||
'exp-in', 'exp-out', 'exp-in-out',
|
||||
'quad-in', 'quad-out', 'quad-in-out',
|
||||
'sin-in', 'sin-out', 'sin-in-out',
|
||||
);
|
||||
|
||||
export type MVSAnimationEasing = ValueFor<typeof Easing>;
|
||||
|
||||
const _Noise = {
|
||||
noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the interpolated value.')
|
||||
// support cummulative noise?
|
||||
};
|
||||
|
||||
const _Common = {
|
||||
target_ref: RequiredField(str, 'Reference to the node.'),
|
||||
property: RequiredField(union(str, list(union(str, int))), 'Value accessor.'),
|
||||
start_ms: OptionalField(float, 0, 'Start time of the transition in milliseconds.'),
|
||||
duration_ms: RequiredField(float, 'Duration of the transition in milliseconds.'),
|
||||
};
|
||||
|
||||
const _Frequency = {
|
||||
frequency: OptionalField(int, 1, 'Determines how many times the interpolation loops. Current T = frequency * t mod 1.'),
|
||||
alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
};
|
||||
|
||||
const _Easing = {
|
||||
easing: OptionalField(Easing, 'linear', 'Easing function to use for the transition.'),
|
||||
};
|
||||
|
||||
const ScalarInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
start: OptionalField(nullable(union(float, list(float))), null, 'Start value. If a list of values is provided, each element will be interpolated separately. If unset, parent state value is used.'),
|
||||
end: OptionalField(nullable(union(float, list(float))), null, 'End value. If a list of values is provided, each element will be interpolated separately. If unset, only noise is applied.'),
|
||||
discrete: OptionalField(bool, false, 'Whether to round the values to the closest integer. Useful for example for trajectory animation.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const Vec3Interpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
start: OptionalField(nullable(list(float)), null, 'Start value. If unset, parent state value is used. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...).'),
|
||||
end: OptionalField(nullable(list(float)), null, 'End value. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...). If unset, only noise is applied.'),
|
||||
spherical: OptionalField(bool, false, 'Whether to use spherical interpolation.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const RotationMatrixInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
start: OptionalField(nullable(Matrix), null, 'Start value. If unset, parent state value is used.'),
|
||||
end: OptionalField(nullable(Matrix), null, 'End value. If unset, only noise is applied.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const ColorInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
start: OptionalField(nullable(ColorT), null, 'Start value. If unset, parent state value is used.'),
|
||||
end: OptionalField(nullable(ColorT), null, 'End value.'),
|
||||
palette: OptionalField(nullable(union(DiscretePalette, ContinuousPalette)), null, 'Palette to sample colors from. Overrides start and end values.'),
|
||||
};
|
||||
|
||||
const TransformationMatrixInterpolation = {
|
||||
..._Common,
|
||||
pivot: OptionalField(nullable(Vector3), null, 'Pivot point for rotation and scale.'),
|
||||
rotation_start: OptionalField(nullable(Matrix), null, 'Start rotation value. If unset, parent state value is used.'),
|
||||
rotation_end: OptionalField(nullable(Matrix), null, 'End rotation value. If unset, only noise is applied.'),
|
||||
rotation_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the rotation.'),
|
||||
rotation_easing: OptionalField(Easing, 'linear', 'Easing function to use for the rotation.'),
|
||||
rotation_frequency: OptionalField(int, 1, 'Determines how many times the rotation interpolation loops. Current T = frequency * t mod 1.'),
|
||||
rotation_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
translation_start: OptionalField(nullable(Vector3), null, 'Start translation value. If unset, parent state value is used.'),
|
||||
translation_end: OptionalField(nullable(Vector3), null, 'End translation value. If unset, only noise is applied.'),
|
||||
translation_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the translation.'),
|
||||
translation_easing: OptionalField(Easing, 'linear', 'Easing function to use for the translation.'),
|
||||
translation_frequency: OptionalField(int, 1, 'Determines how many times the translation interpolation loops. Current T = frequency * t mod 1.'),
|
||||
translation_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
scale_start: OptionalField(nullable(Vector3), null, 'Start scale value. If unset, parent state value is used.'),
|
||||
scale_end: OptionalField(nullable(Vector3), null, 'End scale value. If unset, only noise is applied.'),
|
||||
scale_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the scale.'),
|
||||
scale_easing: OptionalField(Easing, 'linear', 'Easing function to use for the scale.'),
|
||||
scale_frequency: OptionalField(int, 1, 'Determines how many times the scale interpolation loops. Current T = frequency * t mod 1.'),
|
||||
scale_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
};
|
||||
|
||||
export const MVSAnimationSchema = TreeSchema({
|
||||
rootKind: 'animation',
|
||||
nodes: {
|
||||
animation: {
|
||||
description: 'Animation root node',
|
||||
parent: [],
|
||||
params: SimpleParamsSchema({
|
||||
frame_time_ms: OptionalField(float, 1000 / 60, 'Frame time in milliseconds'),
|
||||
duration_ms: OptionalField(nullable(float), null, 'Total duration of the animation. If not specified, computed as maximum of all transitions.'),
|
||||
autoplay: OptionalField(bool, true, 'Determines whether the animation should autoplay when a snapshot is loaded'),
|
||||
loop: OptionalField(bool, false, 'Determines whether the animation should loop when it reaches the end'),
|
||||
include_camera: OptionalField(bool, false, 'Determines whether the camera state should be included in the animation'),
|
||||
include_canvas: OptionalField(bool, false, 'Determines whether the canvas state should be included in the animation'),
|
||||
}),
|
||||
},
|
||||
interpolate: {
|
||||
description: 'This node enables interpolating between values',
|
||||
parent: ['animation'],
|
||||
params: UnionParamsSchema(
|
||||
'kind',
|
||||
'Interpolation kind',
|
||||
{
|
||||
scalar: SimpleParamsSchema(ScalarInterpolation),
|
||||
vec3: SimpleParamsSchema(Vec3Interpolation),
|
||||
rotation_matrix: SimpleParamsSchema(RotationMatrixInterpolation),
|
||||
transform_matrix: SimpleParamsSchema(TransformationMatrixInterpolation),
|
||||
color: SimpleParamsSchema(ColorInterpolation),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type MVSAnimationKind = keyof typeof MVSAnimationSchema.nodes
|
||||
export type MVSAnimationNode<TKind extends MVSAnimationKind = MVSAnimationKind> = NodeFor<typeof MVSAnimationSchema, TKind>
|
||||
export type MVSAnimationTree = TreeFor<typeof MVSAnimationSchema>
|
||||
export type MVSAnimationNodeParams<TKind extends MVSAnimationKind> = ParamsOfKind<MVSAnimationTree, TKind>
|
||||
export type MVSAnimationSubtree<TKind extends MVSAnimationKind = MVSAnimationKind> = SubtreeOfKind<MVSAnimationTree, TKind>
|
||||
@@ -6,10 +6,8 @@
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
import { PathReporter } from 'io-ts/PathReporter';
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
|
||||
|
||||
/** All types that can be used in tree node params.
|
||||
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
|
||||
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {};
|
||||
@@ -142,6 +140,41 @@ export function fieldValidationIssues<F extends Field>(field: F, value: any): st
|
||||
if (validation._tag === 'Right') {
|
||||
return undefined;
|
||||
} else {
|
||||
return PathReporter.report(validation);
|
||||
return reportErrors(validation.left);
|
||||
}
|
||||
}
|
||||
|
||||
// Inlining `reportErrors` instead of `import { PathReporter } from 'io-ts/PathReporter'`;
|
||||
// because it breaks Deno usage.
|
||||
|
||||
function reportErrors(errors: iots.Errors): string[] | undefined {
|
||||
if (errors.length === 0) return undefined;
|
||||
return errors.map(getMessage);
|
||||
}
|
||||
|
||||
function getMessage(e: iots.ValidationError) {
|
||||
return e.message !== undefined
|
||||
? e.message
|
||||
: `Invalid value ${stringifyError(e.value)} supplied to ${getContextPath(e.context)}`;
|
||||
}
|
||||
|
||||
function getContextPath(context: iots.ValidationError['context']) {
|
||||
return context.map(a => `${a.key}: ${a.type.name}`).join('/');
|
||||
}
|
||||
|
||||
function getFunctionName(f: Function & { displayName?: string }) {
|
||||
return f.displayName || f.name || `<function ${f.length}>`;
|
||||
}
|
||||
|
||||
function stringifyError(v: any) {
|
||||
if (typeof v === 'function') {
|
||||
return getFunctionName(v);
|
||||
}
|
||||
if (typeof v === 'number' && !isFinite(v)) {
|
||||
if (isNaN(v)) {
|
||||
return 'NaN';
|
||||
}
|
||||
return v > 0 ? 'Infinity' : '-Infinity';
|
||||
}
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export function copyTree<T extends Tree>(root: T): T {
|
||||
* nodes of kind `C` will be converted to `Y` with a child `Z` (original children moved to `Z`),
|
||||
* nodes of other kinds will just be copied. */
|
||||
export type ConversionRules<A extends Tree, B extends Tree> = {
|
||||
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => Subtree<B>[]
|
||||
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => { subtree: Subtree<B>[] }
|
||||
};
|
||||
|
||||
/** Apply a set of conversion rules to a tree to change to a different schema. */
|
||||
@@ -94,12 +94,12 @@ export function convertTree<A extends Tree, B extends Tree>(root: A, conversions
|
||||
const mapping = new Map<Subtree<A>, Subtree<B>>();
|
||||
let convertedRoot: Subtree<B>;
|
||||
dfs<A>(root, (node, parent) => {
|
||||
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => Subtree<B>[]) | undefined;
|
||||
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => { subtree: Subtree<B>[] }) | undefined;
|
||||
if (conversion) {
|
||||
const convertidos = conversion(node, parent);
|
||||
if (!parent && convertidos.length === 0) throw new Error('Cannot convert root to empty path');
|
||||
const converted = conversion(node, parent);
|
||||
if (!parent && converted?.subtree.length === 0) throw new Error('Cannot convert root to empty path');
|
||||
let convParent = parent ? mapping.get(parent) : undefined;
|
||||
for (const conv of convertidos) {
|
||||
for (const conv of converted.subtree) {
|
||||
if (convParent) {
|
||||
(convParent.children ??= []).push(conv);
|
||||
} else {
|
||||
@@ -153,12 +153,14 @@ export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, treeSchema:
|
||||
type TTree = TreeFor<S>;
|
||||
const rules: ConversionRules<TTree, TTree> = {};
|
||||
for (const kind in treeSchema.nodes) {
|
||||
rules[kind as Kind<Subtree<TTree>>] = node => [{
|
||||
kind: node.kind,
|
||||
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
|
||||
custom: node.custom,
|
||||
ref: node.ref,
|
||||
} as Node as any];
|
||||
rules[kind as Kind<Subtree<TTree>>] = node => ({
|
||||
subtree: [{
|
||||
kind: node.kind,
|
||||
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
|
||||
custom: node.custom,
|
||||
ref: node.ref,
|
||||
} as Node as any]
|
||||
});
|
||||
}
|
||||
return convertTree(tree, rules) as any;
|
||||
}
|
||||
|
||||
@@ -15,37 +15,76 @@ import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
|
||||
/** Convert `format` parameter of `parse` node in `MolstarTree`
|
||||
* into `format` and `is_binary` parameters in `MolstarTree` */
|
||||
export const ParseFormatMvsToMolstar = {
|
||||
// trajectory
|
||||
mmcif: { format: 'cif', is_binary: false },
|
||||
bcif: { format: 'cif', is_binary: true },
|
||||
pdb: { format: 'pdb', is_binary: false },
|
||||
pdbqt: { format: 'pdbqt', is_binary: false },
|
||||
gro: { format: 'gro', is_binary: false },
|
||||
xyz: { format: 'xyz', is_binary: false },
|
||||
mol: { format: 'mol', is_binary: false },
|
||||
sdf: { format: 'sdf', is_binary: false },
|
||||
mol2: { format: 'mol2', is_binary: false },
|
||||
lammpstrj: { format: 'lammpstrj', is_binary: false },
|
||||
// coordinates
|
||||
xtc: { format: 'xtc', is_binary: true },
|
||||
// maps
|
||||
map: { format: 'map', is_binary: true },
|
||||
} satisfies { [p in ParseFormatT]: { format: MolstarParseFormatT, is_binary: boolean } };
|
||||
|
||||
|
||||
/** Conversion rules for conversion from `MVSTree` (with all parameter values) to `MolstarTree` */
|
||||
const mvsToMolstarConversionRules: ConversionRules<FullMVSTree, MolstarTree> = {
|
||||
'download': node => [],
|
||||
'download': node => ({ subtree: [] }),
|
||||
'parse': (node, parent) => {
|
||||
const { format, is_binary } = ParseFormatMvsToMolstar[node.params.format];
|
||||
const convertedNode: MolstarNode<'parse'> = { kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref };
|
||||
if (parent?.kind === 'download') {
|
||||
return [
|
||||
{ kind: 'download', params: { ...parent.params, is_binary }, custom: parent.custom, ref: parent.ref },
|
||||
convertedNode,
|
||||
] satisfies MolstarNode[];
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'download', params: { ...parent.params, is_binary }, custom: parent.custom, ref: parent.ref },
|
||||
{ kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref }
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
} else {
|
||||
console.warn('"parse" node is not being converted, this is suspicious');
|
||||
return [convertedNode] satisfies MolstarNode[];
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref }
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
}
|
||||
},
|
||||
'coordinates': (node, parent) => {
|
||||
if (parent?.kind !== 'parse') throw new Error(`Parent of "coordinates" must be "parse", not "${parent?.kind}".`);
|
||||
const { format } = ParseFormatMvsToMolstar[parent.params.format];
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'coordinates', params: { format }, custom: node.custom, ref: node.ref }
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
},
|
||||
'structure': (node, parent) => {
|
||||
if (parent?.kind !== 'parse') throw new Error(`Parent of "structure" must be "parse", not "${parent?.kind}".`);
|
||||
const { format } = ParseFormatMvsToMolstar[parent.params.format];
|
||||
return [
|
||||
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index']), custom: node.custom, ref: node.ref },
|
||||
] satisfies MolstarNode[];
|
||||
|
||||
if (node.params.coordinates_ref) {
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
|
||||
{ kind: 'model', params: { model_index: 0 } },
|
||||
{ kind: 'trajectory_with_coordinates', params: { coordinates_ref: node.params.coordinates_ref } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index', 'coordinates_ref']), custom: node.custom, ref: node.ref },
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index', 'coordinates_ref']), custom: node.custom, ref: node.ref },
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -70,9 +109,20 @@ function fileExtensionMatches(filename: string, extensions: (FileExtension | '*'
|
||||
}
|
||||
|
||||
const StructureFormatExtensions: Record<ParseFormatT, (FileExtension | '*')[]> = {
|
||||
// trajectory
|
||||
mmcif: ['.cif', '.mmif'],
|
||||
bcif: ['.bcif'],
|
||||
pdb: ['.pdb', '.ent'],
|
||||
pdbqt: ['.pdbqt'],
|
||||
gro: ['.gro'],
|
||||
xyz: ['.xyz'],
|
||||
mol: ['.mol'],
|
||||
sdf: ['.sdf'],
|
||||
mol2: ['.mol2'],
|
||||
lammpstrj: ['.lammpstrj'],
|
||||
// coordinates
|
||||
xtc: ['.xtc'],
|
||||
// volumes
|
||||
map: ['.map', '.ccp4', '.mrc', '.mrcs'],
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { RequiredField, bool } from '../generic/field-schema';
|
||||
import { RequiredField, bool, str } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
|
||||
import { FullMVSTreeSchema } from '../mvs/mvs-tree';
|
||||
@@ -30,6 +30,14 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
}),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's CoordinatesFrom*. */
|
||||
coordinates: {
|
||||
description: "Auxiliary node corresponding to Molstar's CoordinatesFrom*.",
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
}),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
|
||||
trajectory: {
|
||||
description: "Auxiliary node corresponding to Molstar's TrajectoryFrom*.",
|
||||
@@ -39,10 +47,18 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index'] as const),
|
||||
}),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
|
||||
trajectory_with_coordinates: {
|
||||
description: 'Auxiliary node corresponding to assigning a separate coordinates to a trajectory.',
|
||||
parent: ['model'],
|
||||
params: SimpleParamsSchema({
|
||||
coordinates_ref: RequiredField(str, 'Coordinates reference'),
|
||||
}),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's ModelFromTrajectory. */
|
||||
model: {
|
||||
description: "Auxiliary node corresponding to Molstar's ModelFromTrajectory.",
|
||||
parent: ['trajectory'],
|
||||
parent: ['trajectory', 'trajectory_with_coordinates'],
|
||||
params: SimpleParamsSchema(
|
||||
pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['model_index'] as const)
|
||||
),
|
||||
@@ -52,7 +68,7 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
...FullMVSTreeSchema.nodes.structure,
|
||||
parent: ['model'],
|
||||
params: SimpleParamsSchema(
|
||||
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index'] as const)
|
||||
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index', 'coordinates_ref'] as const)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { deepClone, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { GlobalMetadata, MVSData_State, Snapshot, SnapshotMetadata } from '../../mvs-data';
|
||||
import { CustomProps } from '../generic/tree-schema';
|
||||
import { MVSAnimationNodeParams, MVSAnimationSubtree } from '../animation/animation-tree';
|
||||
import { MVSKind, MVSNode, MVSNodeParams, MVSSubtree } from './mvs-tree';
|
||||
|
||||
|
||||
@@ -50,6 +51,8 @@ class _Base<TKind extends MVSKind> {
|
||||
|
||||
/** MVS builder pointing to the 'root' node */
|
||||
export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
protected _animation: Animation | undefined = undefined;
|
||||
|
||||
constructor(params_: CustomAndRef) {
|
||||
const { custom, ref } = params_;
|
||||
const node: MVSNode<'root'> = { kind: 'root', custom, ref };
|
||||
@@ -69,6 +72,7 @@ export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
return {
|
||||
root: deepClone(this._node),
|
||||
metadata: { ...metadata },
|
||||
animation: this?._animation ? deepClone(this._animation.node) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,6 +93,43 @@ export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
|
||||
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
|
||||
|
||||
animation(params: MVSAnimationNodeParams<'animation'> & CustomAndRef = {}): Animation {
|
||||
this._animation ??= new Animation(params);
|
||||
return this._animation;
|
||||
}
|
||||
|
||||
/** Modifies custom state of the root */
|
||||
extendRootCustomState(custom: Record<string, any>): this {
|
||||
this._node.custom = { ...this._node.custom, ...custom };
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class Animation {
|
||||
private _node: MVSAnimationSubtree<'animation'>;
|
||||
constructor(
|
||||
parameters: MVSAnimationNodeParams<'animation'> & CustomAndRef
|
||||
) {
|
||||
this._node = {
|
||||
kind: 'animation',
|
||||
children: [],
|
||||
...splitParams<MVSAnimationNodeParams<'animation'>>(parameters),
|
||||
};
|
||||
}
|
||||
|
||||
get node(): MVSAnimationSubtree<'animation'> {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
interpolate(params: MVSAnimationNodeParams<'interpolate'> & CustomAndRef): Animation {
|
||||
const node = {
|
||||
kind: 'interpolate',
|
||||
...splitParams<MVSAnimationNodeParams<'interpolate'>>(params)
|
||||
} as MVSAnimationSubtree<'interpolate'>;
|
||||
this._node.children!.push(node);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,10 +144,10 @@ export class Download extends _Base<'download'> {
|
||||
|
||||
/** Subsets of 'structure' node params which will be passed to individual builder functions. */
|
||||
const StructureParamsSubsets = {
|
||||
model: ['block_header', 'block_index', 'model_index'],
|
||||
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id'],
|
||||
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max'],
|
||||
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius'],
|
||||
model: ['block_header', 'block_index', 'model_index', 'coordinates_ref'],
|
||||
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id', 'coordinates_ref'],
|
||||
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max', 'coordinates_ref'],
|
||||
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius', 'coordinates_ref'],
|
||||
} satisfies { [kind in MVSNodeParams<'structure'>['type']]: (keyof MVSNodeParams<'structure'>)[] };
|
||||
|
||||
|
||||
@@ -156,6 +197,11 @@ export class Parse extends _Base<'parse'> {
|
||||
volume(params: MVSNodeParams<'volume'> & CustomAndRef = {}): Volume {
|
||||
return new Volume(this._root, this.addChild('volume', params));
|
||||
}
|
||||
/** Add a 'coordinates' node indicating the parsed data type */
|
||||
coordinates(params: MVSNodeParams<'coordinates'> & CustomAndRef = {}): Parse {
|
||||
this.addChild('coordinates', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ const Cartoon = {
|
||||
tubular_helices: OptionalField(bool, false, 'Simplify corkscrew helices to tubes.'),
|
||||
};
|
||||
|
||||
const Backbone = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
};
|
||||
|
||||
const BallAndStick = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
@@ -22,6 +27,13 @@ const BallAndStick = {
|
||||
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
|
||||
};
|
||||
|
||||
const Line = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls whether hydrogen atoms are drawn. */
|
||||
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
|
||||
};
|
||||
|
||||
const Spacefill = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
@@ -35,6 +47,8 @@ const Carbohydrate = {
|
||||
};
|
||||
|
||||
const Surface = {
|
||||
/** Type of surface representation. (Default is 'molecular') */
|
||||
surface_type: OptionalField(literal('molecular', 'gaussian'), 'molecular', `Type of surface representation. (Default is 'molecular')`),
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls whether hydrogen atoms are drawn. */
|
||||
@@ -46,7 +60,9 @@ export const MVSRepresentationParams = UnionParamsSchema(
|
||||
'Representation type',
|
||||
{
|
||||
cartoon: SimpleParamsSchema(Cartoon),
|
||||
backbone: SimpleParamsSchema(Backbone),
|
||||
ball_and_stick: SimpleParamsSchema(BallAndStick),
|
||||
line: SimpleParamsSchema(Line),
|
||||
spacefill: SimpleParamsSchema(Spacefill),
|
||||
carbohydrate: SimpleParamsSchema(Carbohydrate),
|
||||
surface: SimpleParamsSchema(Surface),
|
||||
|
||||
@@ -57,6 +57,8 @@ const TransformParams = SimpleParamsSchema({
|
||||
rotation: OptionalField(Matrix, [1, 0, 0, 0, 1, 0, 0, 0, 1], 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
|
||||
/** Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation). */
|
||||
translation: OptionalField(Vector3, [0, 0, 0], 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
|
||||
/** Point to rotate the object around. Can be either a 3D vector or dynamically computed object centroid. */
|
||||
rotation_center: OptionalField(nullable(union(Vector3, literal('centroid'))), null, 'Point to rotate the object around. Can be either a 3D vector or dynamically computed object centroid.'),
|
||||
/** Transform matrix (4x4 matrix flattened in column major format (j*4+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. Takes precedence over `rotation` and `translation`. */
|
||||
matrix: OptionalField(nullable(Matrix), null, 'Transform matrix (4x4 matrix flattened in column major format (j*4+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. Takes precedence over `rotation` and `translation`.'),
|
||||
});
|
||||
@@ -90,6 +92,12 @@ export const MVSTreeSchema = TreeSchema({
|
||||
format: RequiredField(ParseFormatT, 'Format of the input data resource.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to retrieve molecular coordinates from a parsed data resource. */
|
||||
coordinates: {
|
||||
description: 'This node instructs to retrieve molecular coordinates from a parsed data resource.',
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({}),
|
||||
},
|
||||
/** This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation. */
|
||||
structure: {
|
||||
description: 'This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.',
|
||||
@@ -111,6 +119,8 @@ export const MVSTreeSchema = TreeSchema({
|
||||
ijk_min: OptionalField(tuple([int, int, int]), [-1, -1, -1], 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
/** Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`). */
|
||||
ijk_max: OptionalField(tuple([int, int, int]), [1, 1, 1], 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
/** Reference to a specific set of coordinates. */
|
||||
coordinates_ref: OptionalField(nullable(str), null, 'Reference to a specific set of coordinates.')
|
||||
}),
|
||||
},
|
||||
/** This node instructs to rotate and/or translate structure coordinates. */
|
||||
@@ -322,7 +332,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
parent: ['root'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
background_color: RequiredField(ColorT, 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
background_color: OptionalField(ColorT, 'white', 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). Defaults to white.'),
|
||||
}),
|
||||
},
|
||||
primitives: {
|
||||
|
||||
@@ -11,11 +11,42 @@ import { ValueFor, bool, dict, float, int, list, literal, nullable, object, part
|
||||
|
||||
|
||||
/** `format` parameter values for `parse` node in MVS tree */
|
||||
export const ParseFormatT = literal('mmcif', 'bcif', 'pdb', 'map');
|
||||
export const ParseFormatT = literal(
|
||||
// trajectory
|
||||
'mmcif',
|
||||
'bcif', // +volumes
|
||||
'pdb',
|
||||
'pdbqt',
|
||||
'gro',
|
||||
'xyz',
|
||||
'mol',
|
||||
'sdf',
|
||||
'mol2',
|
||||
'lammpstrj', // + coordinates
|
||||
// coordinates
|
||||
'xtc',
|
||||
// volumes
|
||||
'map',
|
||||
);
|
||||
export type ParseFormatT = ValueFor<typeof ParseFormatT>
|
||||
|
||||
/** `format` parameter values for `parse` node in Molstar tree */
|
||||
export const MolstarParseFormatT = literal('cif', 'pdb', 'map');
|
||||
export const MolstarParseFormatT = literal(
|
||||
// trajectory
|
||||
'cif', // +volumes
|
||||
'pdb',
|
||||
'pdbqt',
|
||||
'gro',
|
||||
'xyz',
|
||||
'mol',
|
||||
'sdf',
|
||||
'mol2',
|
||||
'lammpstrj',
|
||||
// coordinates
|
||||
'xtc',
|
||||
// volumes
|
||||
'map'
|
||||
);
|
||||
export type MolstarParseFormatT = ValueFor<typeof MolstarParseFormatT>
|
||||
|
||||
/** `kind` parameter values for `structure` node in MVS tree */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -11,6 +11,7 @@ import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Scene } from '../mol-gl/scene';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
|
||||
|
||||
export type { ICamera };
|
||||
|
||||
@@ -26,6 +27,7 @@ interface ICamera {
|
||||
readonly near: number,
|
||||
readonly fogFar: number,
|
||||
readonly fogNear: number,
|
||||
readonly headRotation: Mat4,
|
||||
}
|
||||
|
||||
const tmpClip = Vec4();
|
||||
@@ -35,6 +37,7 @@ export class Camera implements ICamera {
|
||||
readonly projection: Mat4 = Mat4.identity();
|
||||
readonly projectionView: Mat4 = Mat4.identity();
|
||||
readonly inverseProjectionView: Mat4 = Mat4.identity();
|
||||
readonly headRotation: Mat4 = Mat4.zero();
|
||||
|
||||
readonly viewport: Viewport;
|
||||
readonly state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
|
||||
@@ -69,7 +72,7 @@ export class Camera implements ICamera {
|
||||
return false;
|
||||
}
|
||||
|
||||
const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target);
|
||||
const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target) * this.state.scale;
|
||||
this.zoom = this.viewport.height / height;
|
||||
|
||||
updateClip(this);
|
||||
@@ -191,6 +194,22 @@ export class Camera implements ICamera {
|
||||
return (2 / w) / (rx * Math.abs(P00));
|
||||
}
|
||||
|
||||
getRay(out: Ray3D, x: number, y: number) {
|
||||
if (this.state.mode === 'orthographic') {
|
||||
Vec3.set(out.origin, x, y, 0);
|
||||
this.unproject(out.origin, out.origin);
|
||||
Vec3.normalize(out.direction, Vec3.sub(out.direction, this.target, this.position));
|
||||
Vec3.scaleAndAdd(out.origin, out.origin, out.direction, -this.near);
|
||||
} else {
|
||||
Vec3.copy(out.origin, this.state.position);
|
||||
Vec3.scale(out.origin, out.origin, this.state.scale);
|
||||
Vec3.set(out.direction, x, y, 0.5);
|
||||
this.unproject(out.direction, out.direction);
|
||||
Vec3.normalize(out.direction, Vec3.sub(out.direction, out.direction, out.origin));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {
|
||||
this.viewport = viewport;
|
||||
Camera.copySnapshot(this.state, state);
|
||||
@@ -270,6 +289,8 @@ export namespace Camera {
|
||||
clipFar: true,
|
||||
minNear: 5,
|
||||
minFar: 0,
|
||||
|
||||
scale: 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,6 +308,8 @@ export namespace Camera {
|
||||
clipFar: boolean
|
||||
minNear: number
|
||||
minFar: number
|
||||
|
||||
scale: number
|
||||
}
|
||||
|
||||
export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
|
||||
@@ -306,6 +329,8 @@ export namespace Camera {
|
||||
if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
|
||||
if (typeof source.minFar !== 'undefined') out.minFar = source.minFar;
|
||||
|
||||
if (typeof source.scale !== 'undefined') out.scale = source.scale;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -318,12 +343,26 @@ export namespace Camera {
|
||||
&& a.clipFar === b.clipFar
|
||||
&& a.minNear === b.minNear
|
||||
&& a.minFar === b.minFar
|
||||
&& a.scale === b.scale
|
||||
&& Vec3.exactEquals(a.position, b.position)
|
||||
&& Vec3.exactEquals(a.up, b.up)
|
||||
&& Vec3.exactEquals(a.target, b.target);
|
||||
}
|
||||
}
|
||||
|
||||
const tmpPosition = Vec3();
|
||||
const tmpTarget = Vec3();
|
||||
|
||||
function updateView(camera: Camera) {
|
||||
if (camera.state.scale === 1) {
|
||||
Mat4.lookAt(camera.view, camera.state.position, camera.state.target, camera.state.up);
|
||||
} else {
|
||||
Vec3.scale(tmpPosition, camera.state.position, camera.state.scale);
|
||||
Vec3.scale(tmpTarget, camera.state.target, camera.state.scale);
|
||||
Mat4.lookAt(camera.view, tmpPosition, tmpTarget, camera.state.up);
|
||||
}
|
||||
}
|
||||
|
||||
function updateOrtho(camera: Camera) {
|
||||
const { viewport, zoom, near, far, viewOffset } = camera;
|
||||
|
||||
@@ -357,7 +396,7 @@ function updateOrtho(camera: Camera) {
|
||||
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
|
||||
|
||||
// build view matrix
|
||||
Mat4.lookAt(camera.view, camera.position, camera.target, camera.up);
|
||||
updateView(camera);
|
||||
}
|
||||
|
||||
function updatePers(camera: Camera) {
|
||||
@@ -381,15 +420,23 @@ function updatePers(camera: Camera) {
|
||||
Mat4.perspective(camera.projection, left, left + width, top, top - height, near, far);
|
||||
|
||||
// build view matrix
|
||||
Mat4.lookAt(camera.view, camera.position, camera.target, camera.up);
|
||||
updateView(camera);
|
||||
}
|
||||
|
||||
function updateClip(camera: Camera) {
|
||||
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar } = camera.state;
|
||||
if (radius < 0.01) radius = 0.01;
|
||||
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar, scale } = camera.state;
|
||||
radiusMax *= scale;
|
||||
minFar *= scale;
|
||||
minNear *= scale;
|
||||
radius *= scale;
|
||||
|
||||
const minRadius = 0.01 * scale;
|
||||
if (radius < minRadius) radius = minRadius;
|
||||
|
||||
const normalizedFar = Math.max(clipFar ? radius : radiusMax, minFar);
|
||||
const cameraDistance = Vec3.distance(camera.position, camera.target);
|
||||
Vec3.scale(tmpTarget, camera.state.target, scale);
|
||||
Vec3.scale(tmpPosition, camera.state.position, scale);
|
||||
const cameraDistance = Vec3.distance(tmpPosition, tmpTarget);
|
||||
let near = cameraDistance - radius;
|
||||
let far = cameraDistance + normalizedFar;
|
||||
|
||||
@@ -405,7 +452,7 @@ function updateClip(camera: Camera) {
|
||||
|
||||
if (near === far) {
|
||||
// make sure near and far are not identical to avoid Infinity in the projection matrix
|
||||
far = near + 0.01;
|
||||
far = near + 0.01 * scale;
|
||||
}
|
||||
|
||||
const fogNearFactor = -(50 - fog) / 50;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -61,6 +61,7 @@ class EyeCamera implements ICamera {
|
||||
projection = Mat4();
|
||||
projectionView = Mat4();
|
||||
inverseProjectionView = Mat4();
|
||||
headRotation = Mat4();
|
||||
state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
|
||||
viewOffset: Readonly<Camera.ViewOffset> = Camera.ViewOffset();
|
||||
far: number = 0;
|
||||
@@ -69,30 +70,29 @@ class EyeCamera implements ICamera {
|
||||
fogNear: number = 0;
|
||||
}
|
||||
|
||||
const eyeLeft = Mat4.identity(), eyeRight = Mat4.identity();
|
||||
const tmpEyeLeft = Mat4.identity();
|
||||
const tmpEyeRight = Mat4.identity();
|
||||
|
||||
function copyStates(parent: Camera, eye: EyeCamera) {
|
||||
Viewport.copy(eye.viewport, parent.viewport);
|
||||
Mat4.copy(eye.view, parent.view);
|
||||
Mat4.copy(eye.projection, parent.projection);
|
||||
Mat4.copy(eye.headRotation, parent.headRotation);
|
||||
Camera.copySnapshot(eye.state, parent.state);
|
||||
Camera.copyViewOffset(eye.viewOffset, parent.viewOffset);
|
||||
eye.far = parent.far;
|
||||
eye.near = parent.near;
|
||||
eye.fogFar = parent.fogFar;
|
||||
eye.fogNear = parent.fogNear;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right: EyeCamera) {
|
||||
// Copy the states
|
||||
|
||||
Viewport.copy(left.viewport, camera.viewport);
|
||||
Mat4.copy(left.view, camera.view);
|
||||
Mat4.copy(left.projection, camera.projection);
|
||||
Camera.copySnapshot(left.state, camera.state);
|
||||
Camera.copyViewOffset(left.viewOffset, camera.viewOffset);
|
||||
left.far = camera.far;
|
||||
left.near = camera.near;
|
||||
left.fogFar = camera.fogFar;
|
||||
left.fogNear = camera.fogNear;
|
||||
|
||||
Viewport.copy(right.viewport, camera.viewport);
|
||||
Mat4.copy(right.view, camera.view);
|
||||
Mat4.copy(right.projection, camera.projection);
|
||||
Camera.copySnapshot(right.state, camera.state);
|
||||
Camera.copyViewOffset(right.viewOffset, camera.viewOffset);
|
||||
right.far = camera.far;
|
||||
right.near = camera.near;
|
||||
right.fogFar = camera.fogFar;
|
||||
right.fogNear = camera.fogNear;
|
||||
copyStates(camera, left);
|
||||
copyStates(camera, right);
|
||||
|
||||
// update the view offsets
|
||||
|
||||
@@ -112,8 +112,8 @@ function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right
|
||||
|
||||
// translate xOffset
|
||||
|
||||
eyeLeft[12] = -eyeSepHalf;
|
||||
eyeRight[12] = eyeSepHalf;
|
||||
tmpEyeLeft[12] = -eyeSepHalf;
|
||||
tmpEyeRight[12] = eyeSepHalf;
|
||||
|
||||
// for left eye
|
||||
|
||||
@@ -123,7 +123,7 @@ function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right
|
||||
left.projection[0] = 2 * camera.near / (xmax - xmin);
|
||||
left.projection[8] = (xmax + xmin) / (xmax - xmin);
|
||||
|
||||
Mat4.mul(left.view, left.view, eyeLeft);
|
||||
Mat4.mul(left.view, left.view, tmpEyeLeft);
|
||||
Mat4.mul(left.projectionView, left.projection, left.view);
|
||||
Mat4.invert(left.inverseProjectionView, left.projectionView);
|
||||
|
||||
@@ -135,7 +135,7 @@ function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right
|
||||
right.projection[0] = 2 * camera.near / (xmax - xmin);
|
||||
right.projection[8] = (xmax + xmin) / (xmax - xmin);
|
||||
|
||||
Mat4.mul(right.view, right.view, eyeRight);
|
||||
Mat4.mul(right.view, right.view, tmpEyeRight);
|
||||
Mat4.mul(right.projectionView, right.projection, right.view);
|
||||
Mat4.invert(right.inverseProjectionView, right.projectionView);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { Vec3, Vec2 } from '../mol-math/linear-algebra';
|
||||
import { InputObserver, ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
|
||||
import { Renderer, RendererStats, RendererParams } from '../mol-gl/renderer';
|
||||
import { GraphicsRenderObject } from '../mol-gl/render-object';
|
||||
import { TrackballControls, TrackballControlsParams } from './controls/trackball';
|
||||
import { DefaultTrackballControlsAttribs, TrackballControls, TrackballControlsParams } from './controls/trackball';
|
||||
import { Viewport } from './camera/util';
|
||||
import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
|
||||
import { Representation } from '../mol-repr/representation';
|
||||
@@ -28,13 +28,12 @@ import { SetUtils } from '../mol-util/set';
|
||||
import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
|
||||
import { PostprocessingParams } from './passes/postprocessing';
|
||||
import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
|
||||
import { PickData } from './passes/pick';
|
||||
import { PickHelper } from './passes/pick';
|
||||
import { AsyncPickData, DefaultPickOptions, PickData } from './passes/pick';
|
||||
import { PickHelper } from './helper/pick-helper';
|
||||
import { ImagePass, ImageProps } from './passes/image';
|
||||
import { Sphere3D } from '../mol-math/geometry';
|
||||
import { addConsoleStatsProvider, isDebugMode, isTimingMode, removeConsoleStatsProvider } from '../mol-util/debug';
|
||||
import { CameraHelperParams } from './helper/camera-helper';
|
||||
import { produce } from 'immer';
|
||||
import { HandleHelperParams } from './helper/handle-helper';
|
||||
import { StereoCamera, StereoCameraParams } from './camera/stereo';
|
||||
import { Helper } from './helper/helper';
|
||||
@@ -47,7 +46,13 @@ import { deepClone } from '../mol-util/object';
|
||||
import { HiZParams, HiZPass } from './passes/hi-z';
|
||||
import { IlluminationParams } from './passes/illumination';
|
||||
import { isMobileBrowser } from '../mol-util/browser';
|
||||
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
|
||||
import { RayHelper } from './helper/ray-helper';
|
||||
import { produce } from '../mol-util/produce';
|
||||
|
||||
export const CameraFogParams = {
|
||||
intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
|
||||
};
|
||||
export const Canvas3DParams = {
|
||||
camera: PD.Group({
|
||||
mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }),
|
||||
@@ -57,12 +62,11 @@ export const Canvas3DParams = {
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
|
||||
fov: PD.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }),
|
||||
scale: PD.Numeric(1, { min: 0.001, max: 1, step: 0.001 }, { label: 'Scene scale' }),
|
||||
manualReset: PD.Boolean(false, { isHidden: true }),
|
||||
}, { pivot: 'mode' }),
|
||||
cameraFog: PD.MappedStatic('on', {
|
||||
on: PD.Group({
|
||||
intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
|
||||
}),
|
||||
on: PD.Group(CameraFogParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Show fog in the distance' }),
|
||||
cameraClipping: PD.Group({
|
||||
@@ -110,6 +114,11 @@ export type PartialCanvas3DProps = {
|
||||
[K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
|
||||
}
|
||||
|
||||
export const DefaultCanvas3DAttribs = {
|
||||
trackball: DefaultTrackballControlsAttribs,
|
||||
};
|
||||
export type Canvas3DAttribs = typeof DefaultCanvas3DAttribs
|
||||
|
||||
export { Canvas3DContext };
|
||||
|
||||
/** Can be used to create multiple Canvas3D objects */
|
||||
@@ -330,7 +339,8 @@ interface Canvas3D {
|
||||
pause(noDraw?: boolean): void
|
||||
/** Sets drawPaused = false without starting the built in animation loop */
|
||||
resume(): void
|
||||
identify(x: number, y: number): PickData | undefined
|
||||
identify(target: Vec2 | Ray3D): PickData | undefined
|
||||
asyncIdentify(target: Vec2 | Ray3D): AsyncPickData | undefined
|
||||
mark(loci: Representation.Loci, action: MarkerAction): void
|
||||
getLoci(pickingId: PickingId | undefined): Representation.Loci
|
||||
|
||||
@@ -355,6 +365,7 @@ interface Canvas3D {
|
||||
|
||||
/** Returns a copy of the current Canvas3D instance props */
|
||||
readonly props: Readonly<Canvas3DProps>
|
||||
readonly attribs: Readonly<Canvas3DAttribs>
|
||||
readonly input: InputObserver
|
||||
readonly stats: RendererStats
|
||||
readonly interaction: Canvas3dInteractionHelper['events']
|
||||
@@ -374,9 +385,10 @@ namespace Canvas3D {
|
||||
export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
|
||||
export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
|
||||
|
||||
export function create(ctx: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
|
||||
export function create(ctx: Canvas3DContext, props: Partial<Canvas3DProps> = {}, attribs: Partial<Canvas3DAttribs> = {}): Canvas3D {
|
||||
const { webgl, input, passes, assetManager, canvas, contextLost } = ctx;
|
||||
const p: Canvas3DProps = { ...deepClone(DefaultCanvas3DParams), ...deepClone(props) };
|
||||
const a = { ...deepClone(DefaultCanvas3DAttribs), ...deepClone(attribs) };
|
||||
|
||||
const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
|
||||
const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
|
||||
@@ -412,18 +424,24 @@ namespace Canvas3D {
|
||||
clipFar: p.cameraClipping.far,
|
||||
minNear: p.cameraClipping.minNear,
|
||||
fov: degToRad(p.camera.fov),
|
||||
scale: p.camera.scale,
|
||||
}, { x, y, width, height });
|
||||
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
|
||||
|
||||
const controls = TrackballControls.create(input, camera, scene, p.trackball);
|
||||
const controls = TrackballControls.create(input, camera, scene, p.trackball, a.trackball);
|
||||
const helper = new Helper(webgl, scene, p);
|
||||
const hiZ = new HiZPass(webgl, passes.draw, canvas, p.hiZ);
|
||||
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
renderer.setOcclusionTest(hiZ.isOccluded);
|
||||
|
||||
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, p.pickPadding);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, controls, p.interaction);
|
||||
const pickOptions = {
|
||||
pickPadding: p.pickPadding,
|
||||
maxAsyncReadLag: DefaultPickOptions.maxAsyncReadLag,
|
||||
};
|
||||
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, pickOptions);
|
||||
const rayHelper = new RayHelper(webgl, renderer, scene, helper, pickOptions);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, asyncIdentify, getLoci, input, camera, controls, p.interaction);
|
||||
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
|
||||
|
||||
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
|
||||
@@ -649,9 +667,26 @@ namespace Canvas3D {
|
||||
animationFrameHandle = 0;
|
||||
}
|
||||
|
||||
function identify(x: number, y: number): PickData | undefined {
|
||||
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
|
||||
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
|
||||
function identify(target: Vec2 | Ray3D): PickData | undefined {
|
||||
if (webgl.isContextLost) return undefined;
|
||||
|
||||
if ('origin' in target) {
|
||||
return rayHelper.identify(target, camera);
|
||||
} else {
|
||||
const cam = (p.camera.stereo.name === 'on') ? stereoCamera : camera;
|
||||
return pickHelper.identify(target[0], target[1], cam);
|
||||
}
|
||||
}
|
||||
|
||||
function asyncIdentify(target: Vec2 | Ray3D): AsyncPickData | undefined {
|
||||
if (webgl.isContextLost) return undefined;
|
||||
|
||||
if ('origin' in target) {
|
||||
return rayHelper.asyncIdentify(target, camera);
|
||||
} else {
|
||||
const cam = (p.camera.stereo.name === 'on') ? stereoCamera : camera;
|
||||
return pickHelper.asyncIdentify(target[0], target[1], cam);
|
||||
}
|
||||
}
|
||||
|
||||
function commit(isSynchronous: boolean = false) {
|
||||
@@ -841,6 +876,7 @@ namespace Canvas3D {
|
||||
helper: { ...helper.camera.props },
|
||||
stereo: { ...p.camera.stereo },
|
||||
fov: Math.round(radToDeg(camera.state.fov)),
|
||||
scale: camera.state.scale,
|
||||
manualReset: !!p.camera.manualReset
|
||||
},
|
||||
cameraFog: camera.state.fog > 0
|
||||
@@ -875,6 +911,10 @@ namespace Canvas3D {
|
||||
});
|
||||
|
||||
const contextRestoredSub = contextRestored.subscribe(() => {
|
||||
pickHelper.reset();
|
||||
rayHelper.reset();
|
||||
hiZ.reset();
|
||||
|
||||
scene.forEach(r => {
|
||||
if (r.values.meta?.ref.value.reset) {
|
||||
r.values.meta.ref.value.reset();
|
||||
@@ -952,7 +992,7 @@ namespace Canvas3D {
|
||||
input.click.subscribe(e => {
|
||||
if (!e.modifiers.control || e.button !== 2) return;
|
||||
|
||||
const p = identify(e.x, e.y);
|
||||
const p = identify(Vec2.create(e.x, e.y));
|
||||
if (!p) {
|
||||
occlusionLoci = undefined;
|
||||
printOcclusion(occlusionLoci);
|
||||
@@ -1020,6 +1060,7 @@ namespace Canvas3D {
|
||||
pause,
|
||||
resume: () => { drawPaused = false; },
|
||||
identify,
|
||||
asyncIdentify,
|
||||
mark,
|
||||
getLoci,
|
||||
|
||||
@@ -1060,6 +1101,9 @@ namespace Canvas3D {
|
||||
if (props.camera && props.camera.fov !== undefined && props.camera.fov !== oldFov) {
|
||||
cameraState.fov = degToRad(props.camera.fov);
|
||||
}
|
||||
if (props.camera && props.camera.scale !== undefined && props.camera.scale !== cameraState.scale) {
|
||||
cameraState.scale = props.camera.scale;
|
||||
}
|
||||
if (props.cameraFog !== undefined && props.cameraFog.params) {
|
||||
const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
|
||||
if (newFog !== camera.state.fog) cameraState.fog = newFog;
|
||||
@@ -1143,6 +1187,9 @@ namespace Canvas3D {
|
||||
get props() {
|
||||
return getProps();
|
||||
},
|
||||
get attribs() {
|
||||
return a;
|
||||
},
|
||||
get input() {
|
||||
return input;
|
||||
},
|
||||
@@ -1170,6 +1217,9 @@ namespace Canvas3D {
|
||||
renderer.dispose();
|
||||
interactionHelper.dispose();
|
||||
hiZ.dispose();
|
||||
pickHelper.dispose();
|
||||
rayHelper.dispose();
|
||||
|
||||
if (fenceSync !== null) {
|
||||
webgl.deleteSync(fenceSync);
|
||||
fenceSync = null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -56,8 +56,6 @@ export const DefaultTrackballBindings = {
|
||||
};
|
||||
|
||||
export const TrackballControlsParams = {
|
||||
noScroll: PD.Boolean(true, { isHidden: true }),
|
||||
|
||||
rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
|
||||
zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
|
||||
panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
|
||||
@@ -68,10 +66,10 @@ export const TrackballControlsParams = {
|
||||
animate: PD.MappedStatic('off', {
|
||||
off: PD.EmptyGroup(),
|
||||
spin: PD.Group({
|
||||
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }, { description: 'Rotation speed in radians per second' }),
|
||||
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }, { description: 'Number of rotations per second' }),
|
||||
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
|
||||
rock: PD.Group({
|
||||
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
|
||||
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }, { description: 'Number of oscilations per second' }),
|
||||
angle: PD.Numeric(10, { min: 0, max: 90, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
|
||||
}, { description: 'Rock the 3D scene around the x-axis in view space' })
|
||||
}),
|
||||
@@ -85,8 +83,6 @@ export const TrackballControlsParams = {
|
||||
gestureScaleFactor: PD.Numeric(1, {}, { isHidden: true }),
|
||||
maxWheelDelta: PD.Numeric(0.02, {}, { isHidden: true }),
|
||||
|
||||
bindings: PD.Value(DefaultTrackballBindings, { isHidden: true }),
|
||||
|
||||
/**
|
||||
* minDistance = minDistanceFactor * boundingSphere.radius + minDistancePadding
|
||||
* maxDistance = max(maxDistanceFactor * boundingSphere.radius, maxDistanceMin)
|
||||
@@ -103,6 +99,11 @@ export const TrackballControlsParams = {
|
||||
};
|
||||
export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
|
||||
|
||||
export const DefaultTrackballControlsAttribs = {
|
||||
bindings: DefaultTrackballBindings,
|
||||
};
|
||||
export type TrackballControlsAttribs = typeof DefaultTrackballControlsAttribs
|
||||
|
||||
export { TrackballControls };
|
||||
interface TrackballControls {
|
||||
readonly viewport: Viewport
|
||||
@@ -112,20 +113,25 @@ interface TrackballControls {
|
||||
readonly props: Readonly<TrackballControlsProps>
|
||||
setProps: (props: Partial<TrackballControlsProps>) => void
|
||||
|
||||
readonly attribs: Readonly<TrackballControlsAttribs>
|
||||
setAttribs: (attribs: Partial<TrackballControlsAttribs>) => void
|
||||
|
||||
start: (t: number) => void
|
||||
update: (t: number) => void
|
||||
reset: () => void
|
||||
dispose: () => void
|
||||
}
|
||||
namespace TrackballControls {
|
||||
export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}): TrackballControls {
|
||||
export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}, attribs: Partial<TrackballControlsAttribs> = {}): TrackballControls {
|
||||
const p: TrackballControlsProps = {
|
||||
...PD.getDefaultValues(TrackballControlsParams),
|
||||
...props,
|
||||
// include default bindings for backwards state compatibility
|
||||
bindings: { ...DefaultTrackballBindings, ...props.bindings }
|
||||
};
|
||||
const b = p.bindings;
|
||||
const a: TrackballControlsAttribs = {
|
||||
...DefaultTrackballControlsAttribs,
|
||||
...attribs
|
||||
};
|
||||
const b = a.bindings;
|
||||
|
||||
const viewport = Viewport.clone(camera.viewport);
|
||||
|
||||
@@ -825,7 +831,7 @@ namespace TrackballControls {
|
||||
function spin(deltaT: number) {
|
||||
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
|
||||
|
||||
const radPerMs = p.animate.params.speed / 1000;
|
||||
const radPerMs = 2 * Math.PI * p.animate.params.speed / 1000;
|
||||
_spinSpeed[0] = deltaT * radPerMs / getRotateFactor();
|
||||
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
||||
}
|
||||
@@ -885,7 +891,12 @@ namespace TrackballControls {
|
||||
}
|
||||
}
|
||||
Object.assign(p, props);
|
||||
Object.assign(b, props.bindings);
|
||||
},
|
||||
|
||||
get attribs() { return a as Readonly<TrackballControlsAttribs>; },
|
||||
setAttribs: (attribs: Partial<TrackballControlsAttribs>) => {
|
||||
Object.assign(a, attribs);
|
||||
Object.assign(b, a.bindings);
|
||||
},
|
||||
|
||||
start,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { produce } from 'immer';
|
||||
import { produce } from '../../mol-util/produce';
|
||||
import { Interval } from '../../mol-data/int/interval';
|
||||
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
|
||||
@@ -16,7 +16,7 @@ import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { produce } from 'immer';
|
||||
import { produce } from '../../mol-util/produce';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { Camera } from '../camera';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -14,14 +14,14 @@ import { Camera } from '../camera';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Bond } from '../../mol-model/structure';
|
||||
import { TrackballControls } from '../controls/trackball';
|
||||
import { Ray3D } from '../../mol-math/geometry/primitives/ray3d';
|
||||
import { AsyncPickData } from '../passes/pick';
|
||||
|
||||
type Canvas3D = import('../canvas3d').Canvas3D
|
||||
type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
|
||||
type DragEvent = import('../canvas3d').Canvas3D.DragEvent
|
||||
type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
|
||||
|
||||
enum InputEvent { Move, Click, Drag }
|
||||
|
||||
const tmpPosA = Vec3();
|
||||
const tmpPos = Vec3();
|
||||
const tmpNorm = Vec3();
|
||||
@@ -29,6 +29,7 @@ const tmpNorm = Vec3();
|
||||
export const Canvas3dInteractionHelperParams = {
|
||||
maxFps: PD.Numeric(30, { min: 10, max: 60, step: 10 }),
|
||||
preferAtomPixelPadding: PD.Numeric(3, { min: 0, max: 20, step: 1 }, { description: 'Number of extra pixels at which to prefer atoms over bonds.' }),
|
||||
convertCoordsToRay: PD.Boolean(false, { description: 'Convert screen coordinates to ray for picking.' }),
|
||||
};
|
||||
export type Canvas3dInteractionHelperParams = typeof Canvas3dInteractionHelperParams
|
||||
export type Canvas3dInteractionHelperProps = PD.Values<Canvas3dInteractionHelperParams>
|
||||
@@ -47,10 +48,11 @@ export class Canvas3dInteractionHelper {
|
||||
private endX = -1;
|
||||
private endY = -1;
|
||||
|
||||
private id: PickingId | undefined = void 0;
|
||||
private ray: Ray3D | undefined = void 0;
|
||||
|
||||
private pickData: AsyncPickData | undefined = void 0;
|
||||
private position: Vec3 | undefined = void 0;
|
||||
|
||||
private currentIdentifyT = 0;
|
||||
private isInteracting = false;
|
||||
|
||||
private prevLoci: Representation.Loci = Representation.Loci.Empty;
|
||||
@@ -68,46 +70,66 @@ export class Canvas3dInteractionHelper {
|
||||
Object.assign(this.props, props);
|
||||
}
|
||||
|
||||
private identify(e: InputEvent, t: number) {
|
||||
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
|
||||
|
||||
if (e === InputEvent.Drag) {
|
||||
if (xyChanged && !this.outsideViewport(this.startX, this.startY)) {
|
||||
this.events.drag.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, pageStart: Vec2.create(this.startX, this.startY), pageEnd: Vec2.create(this.endX, this.endY) });
|
||||
|
||||
this.startX = this.endX;
|
||||
this.startY = this.endY;
|
||||
}
|
||||
return;
|
||||
private getTarget(): Vec2 | Ray3D {
|
||||
if (this.ray) {
|
||||
return this.ray;
|
||||
} else if (this.props.convertCoordsToRay) {
|
||||
return this.camera.getRay(Ray3D(), this.endX, this.input.height - this.endY);
|
||||
} else {
|
||||
return Vec2.create(this.endX, this.endY);
|
||||
}
|
||||
}
|
||||
|
||||
private handleMove() {
|
||||
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
|
||||
if (xyChanged) {
|
||||
const pickData = this.canvasIdentify(this.endX, this.endY);
|
||||
this.id = pickData?.id;
|
||||
this.position = pickData?.position;
|
||||
this.pickData = this.canvasAsyncIdentify(this.getTarget());
|
||||
this.startX = this.endX;
|
||||
this.startY = this.endY;
|
||||
}
|
||||
}
|
||||
|
||||
if (e === InputEvent.Click) {
|
||||
const loci = this.getLoci(this.id, this.position);
|
||||
this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
|
||||
this.prevLoci = loci;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.inside || this.currentIdentifyT !== t || !xyChanged || this.outsideViewport(this.endX, this.endY)) return;
|
||||
|
||||
const loci = this.getLoci(this.id, this.position);
|
||||
this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
|
||||
private handleClick() {
|
||||
const pickData = this.canvasIdentify(this.getTarget());
|
||||
const loci = this.getLoci(pickData?.id, pickData?.position);
|
||||
this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: pickData?.position });
|
||||
this.prevLoci = loci;
|
||||
}
|
||||
|
||||
private handleDrag() {
|
||||
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
|
||||
|
||||
if (xyChanged && !this.outsideViewport(this.startX, this.startY, this.ray)) {
|
||||
this.events.drag.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, pageStart: Vec2.create(this.startX, this.startY), pageEnd: Vec2.create(this.endX, this.endY) });
|
||||
|
||||
this.startX = this.endX;
|
||||
this.startY = this.endY;
|
||||
}
|
||||
}
|
||||
|
||||
tick(t: number) {
|
||||
if (this.inside && t - this.prevT > 1000 / this.props.maxFps) {
|
||||
if (!this.inside) return;
|
||||
|
||||
if (this.pickData) {
|
||||
const pickData = this.pickData.tryGet();
|
||||
if (pickData !== 'pending') {
|
||||
this.position = pickData?.position;
|
||||
if (this.inside) {
|
||||
const loci = this.getLoci(pickData?.id, pickData?.position);
|
||||
this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: pickData?.position });
|
||||
this.prevLoci = loci;
|
||||
}
|
||||
this.pickData = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (t - this.prevT > 1000 / this.props.maxFps) {
|
||||
this.prevT = t;
|
||||
this.currentIdentifyT = t;
|
||||
this.identify(this.isInteracting ? InputEvent.Drag : InputEvent.Move, t);
|
||||
if (this.isInteracting) {
|
||||
this.handleDrag();
|
||||
} else {
|
||||
this.handleMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,22 +141,24 @@ export class Canvas3dInteractionHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private move(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
private move(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, ray?: Ray3D) {
|
||||
this.inside = true;
|
||||
this.buttons = buttons;
|
||||
this.button = button;
|
||||
this.modifiers = modifiers;
|
||||
this.ray = ray;
|
||||
this.endX = x;
|
||||
this.endY = y;
|
||||
}
|
||||
|
||||
private click(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
private click(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, ray?: Ray3D) {
|
||||
this.endX = x;
|
||||
this.endY = y;
|
||||
this.buttons = buttons;
|
||||
this.button = button;
|
||||
this.modifiers = modifiers;
|
||||
this.identify(InputEvent.Click, 0);
|
||||
this.ray = ray;
|
||||
this.handleClick();
|
||||
}
|
||||
|
||||
private drag(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
@@ -143,7 +167,7 @@ export class Canvas3dInteractionHelper {
|
||||
this.buttons = buttons;
|
||||
this.button = button;
|
||||
this.modifiers = modifiers;
|
||||
this.identify(InputEvent.Drag, 0);
|
||||
this.handleDrag();
|
||||
}
|
||||
|
||||
private modify(modifiers: ModifiersKeys) {
|
||||
@@ -152,7 +176,9 @@ export class Canvas3dInteractionHelper {
|
||||
this.events.hover.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
|
||||
}
|
||||
|
||||
private outsideViewport(x: number, y: number) {
|
||||
private outsideViewport(x: number, y: number, ray?: Ray3D) {
|
||||
if (ray) return false;
|
||||
|
||||
const { input, camera: { viewport } } = this;
|
||||
x *= input.pixelRatio;
|
||||
y *= input.pixelRatio;
|
||||
@@ -189,7 +215,7 @@ export class Canvas3dInteractionHelper {
|
||||
this.ev.dispose();
|
||||
}
|
||||
|
||||
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private controls: TrackballControls, props: Partial<Canvas3dInteractionHelperProps> = {}) {
|
||||
constructor(private canvasIdentify: Canvas3D['identify'], private canvasAsyncIdentify: Canvas3D['asyncIdentify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private controls: TrackballControls, props: Partial<Canvas3dInteractionHelperProps> = {}) {
|
||||
this.props = { ...PD.getDefaultValues(Canvas3dInteractionHelperParams), ...props };
|
||||
|
||||
input.drag.subscribe(({ x, y, buttons, button, modifiers }) => {
|
||||
@@ -198,14 +224,14 @@ export class Canvas3dInteractionHelper {
|
||||
this.drag(x, y, buttons, button, modifiers);
|
||||
});
|
||||
|
||||
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement }) => {
|
||||
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement, ray }) => {
|
||||
if (!inside || this.isInteracting) return;
|
||||
if (!onElement) {
|
||||
this.leave();
|
||||
return;
|
||||
}
|
||||
// console.log('move');
|
||||
this.move(x, y, buttons, button, modifiers);
|
||||
this.move(x, y, buttons, button, modifiers, ray);
|
||||
});
|
||||
|
||||
input.leave.subscribe(() => {
|
||||
@@ -213,10 +239,10 @@ export class Canvas3dInteractionHelper {
|
||||
this.leave();
|
||||
});
|
||||
|
||||
input.click.subscribe(({ x, y, buttons, button, modifiers }) => {
|
||||
if (this.outsideViewport(x, y)) return;
|
||||
input.click.subscribe(({ x, y, buttons, button, modifiers, ray }) => {
|
||||
if (this.outsideViewport(x, y, ray)) return;
|
||||
// console.log('click');
|
||||
this.click(x, y, buttons, button, modifiers);
|
||||
this.click(x, y, buttons, button, modifiers, ray);
|
||||
});
|
||||
|
||||
input.interactionEnd.subscribe(() => {
|
||||
|
||||
208
src/mol-canvas3d/helper/pick-helper.ts
Normal file
208
src/mol-canvas3d/helper/pick-helper.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Renderer } from '../../mol-gl/renderer';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { spiral2d } from '../../mol-math/misc';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { Camera } from '../camera';
|
||||
import { StereoCamera } from '../camera/stereo';
|
||||
import { cameraUnproject, Viewport } from '../camera/util';
|
||||
import { Helper } from '../helper/helper';
|
||||
import { AsyncPickData, AsyncPickStatus, checkAsyncPickingSupport, PickBuffers, PickData, PickOptions, PickPass } from '../passes/pick';
|
||||
|
||||
export class PickHelper {
|
||||
dirty = true;
|
||||
|
||||
private pickPadding: number;
|
||||
private buffers = new PickBuffers(this.webgl, this.pickPass);
|
||||
private viewport = Viewport();
|
||||
|
||||
private pickRatio: number;
|
||||
private pickX: number;
|
||||
private pickY: number;
|
||||
private pickWidth: number;
|
||||
private pickHeight: number;
|
||||
private halfPickWidth: number;
|
||||
|
||||
private spiral: [number, number][];
|
||||
|
||||
setViewport(x: number, y: number, width: number, height: number) {
|
||||
Viewport.set(this.viewport, x, y, width, height);
|
||||
this.update();
|
||||
}
|
||||
|
||||
setPickPadding(pickPadding: number) {
|
||||
if (this.pickPadding !== pickPadding) {
|
||||
this.pickPadding = pickPadding;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
const { x, y, width, height } = this.viewport;
|
||||
|
||||
this.pickRatio = this.pickPass.pickRatio;
|
||||
this.pickX = Math.ceil(x * this.pickRatio);
|
||||
this.pickY = Math.ceil(y * this.pickRatio);
|
||||
|
||||
const pickWidth = Math.floor(width * this.pickRatio);
|
||||
const pickHeight = Math.floor(height * this.pickRatio);
|
||||
|
||||
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
|
||||
this.pickWidth = pickWidth;
|
||||
this.pickHeight = pickHeight;
|
||||
this.halfPickWidth = Math.floor(this.pickWidth / 2);
|
||||
|
||||
this.buffers.setViewport(this.pickX, this.pickY, this.pickWidth, this.pickHeight);
|
||||
}
|
||||
|
||||
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private render(camera: Camera | StereoCamera) {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', { captureStats: true });
|
||||
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
|
||||
const { renderer, scene, helper } = this;
|
||||
|
||||
renderer.setTransparentBackground(false);
|
||||
renderer.setDrawingBufferSize(pickWidth, pickHeight);
|
||||
renderer.setPixelRatio(this.pickRatio);
|
||||
|
||||
if (StereoCamera.is(camera)) {
|
||||
renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera.left, scene, helper);
|
||||
|
||||
renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera.right, scene, helper);
|
||||
} else {
|
||||
renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera, scene, helper);
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.render');
|
||||
}
|
||||
|
||||
private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
|
||||
if (this.webgl.isContextLost) return;
|
||||
|
||||
const { webgl, pickRatio } = this;
|
||||
if (webgl.isContextLost) return;
|
||||
|
||||
x *= webgl.pixelRatio;
|
||||
y *= webgl.pixelRatio;
|
||||
y = this.pickPass.drawingBufferHeight - y; // flip y
|
||||
|
||||
const { viewport } = this;
|
||||
|
||||
// check if within viewport
|
||||
if (x < viewport.x ||
|
||||
y < viewport.y ||
|
||||
x > viewport.x + viewport.width ||
|
||||
y > viewport.y + viewport.height
|
||||
) return;
|
||||
|
||||
const xv = x - viewport.x;
|
||||
const yv = y - viewport.y;
|
||||
|
||||
const xp = Math.floor(xv * pickRatio);
|
||||
const yp = Math.floor(yv * pickRatio);
|
||||
|
||||
const pickingId = this.buffers.getPickingId(xp, yp);
|
||||
if (pickingId === undefined) return;
|
||||
|
||||
const z = this.buffers.getDepth(xp, yp);
|
||||
const position = Vec3.create(x, y, z);
|
||||
if (StereoCamera.is(camera)) {
|
||||
const halfWidth = Math.floor(viewport.width / 2);
|
||||
if (x > viewport.x + halfWidth) {
|
||||
position[0] = viewport.x + (xv - halfWidth) * 2;
|
||||
cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
|
||||
} else {
|
||||
position[0] = viewport.x + xv * 2;
|
||||
cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
|
||||
}
|
||||
} else {
|
||||
cameraUnproject(position, position, viewport, camera.inverseProjectionView);
|
||||
}
|
||||
|
||||
return { id: pickingId, position };
|
||||
}
|
||||
|
||||
private prepare() {
|
||||
if (this.pickRatio !== this.pickPass.pickRatio) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
private getPickData(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
|
||||
for (const d of this.spiral) {
|
||||
const pickData = this.identifyInternal(x + d[0], y + d[1], camera);
|
||||
if (pickData) return pickData;
|
||||
}
|
||||
}
|
||||
|
||||
identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
|
||||
this.prepare();
|
||||
|
||||
if (this.dirty) {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.identify');
|
||||
this.render(camera);
|
||||
this.buffers.read();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.identify');
|
||||
}
|
||||
|
||||
return this.getPickData(x, y, camera);
|
||||
}
|
||||
|
||||
asyncIdentify(x: number, y: number, camera: Camera | StereoCamera): AsyncPickData | undefined {
|
||||
this.prepare();
|
||||
|
||||
if (this.dirty) {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.asyncIdentify');
|
||||
this.render(camera);
|
||||
this.buffers.asyncRead();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.asyncIdentify');
|
||||
}
|
||||
|
||||
return {
|
||||
tryGet: () => {
|
||||
const status = this.buffers.check();
|
||||
if (status === AsyncPickStatus.Resolved) {
|
||||
return this.getPickData(x, y, camera);
|
||||
} else if (status === AsyncPickStatus.Pending) {
|
||||
return 'pending';
|
||||
} else if (status === AsyncPickStatus.Failed) {
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.buffers.reset();
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.buffers.dispose();
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, options: PickOptions) {
|
||||
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||
this.pickPadding = options.pickPadding;
|
||||
|
||||
if (!checkAsyncPickingSupport(webgl)) {
|
||||
this.asyncIdentify = (x, y, camera) => ({
|
||||
tryGet: () => this.identify(x, y, camera)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
205
src/mol-canvas3d/helper/ray-helper.ts
Normal file
205
src/mol-canvas3d/helper/ray-helper.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Renderer } from '../../mol-gl/renderer';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Ray3D } from '../../mol-math/geometry/primitives/ray3d';
|
||||
import { Mat4, Quat, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { degToRad, spiral2d } from '../../mol-math/misc';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { Camera } from '../camera';
|
||||
import { cameraUnproject } from '../camera/util';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { Helper } from './helper';
|
||||
import { AsyncPickData, PickBuffers, PickData, PickPass, PickOptions, checkAsyncPickingSupport, AsyncPickStatus } from '../passes/pick';
|
||||
import { Sphere3D } from '../../mol-math/geometry/primitives/sphere3d';
|
||||
|
||||
export class RayHelper {
|
||||
private viewport = Viewport();
|
||||
private size: number;
|
||||
private spiral: [number, number][];
|
||||
|
||||
private pickPadding: number;
|
||||
private camera: Camera;
|
||||
private pickPass: PickPass;
|
||||
private buffers: PickBuffers;
|
||||
|
||||
setPickPadding(pickPadding: number) {
|
||||
if (this.pickPadding !== pickPadding) {
|
||||
this.pickPadding = pickPadding;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
const size = this.pickPadding * 2 + 1;
|
||||
Viewport.set(this.viewport, 0, 0, size, size);
|
||||
this.buffers.setViewport(0, 0, size, size);
|
||||
|
||||
this.spiral = spiral2d(this.pickPadding);
|
||||
this.size = size;
|
||||
|
||||
this.pickPass.setSize(size, size);
|
||||
}
|
||||
|
||||
private render(camera: Camera) {
|
||||
if (isTimingMode) this.webgl.timer.mark('RayHelper.render', { captureStats: true });
|
||||
const { renderer, scene, helper } = this;
|
||||
|
||||
renderer.setTransparentBackground(false);
|
||||
renderer.setDrawingBufferSize(this.size, this.size);
|
||||
renderer.setPixelRatio(1);
|
||||
|
||||
renderer.setViewport(0, 0, this.size, this.size);
|
||||
this.pickPass.render(renderer, camera, scene, helper);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.markEnd('RayHelper.render');
|
||||
}
|
||||
|
||||
private identifyInternal(x: number, y: number): PickData | undefined {
|
||||
if (this.webgl.isContextLost) return;
|
||||
|
||||
const { viewport } = this;
|
||||
|
||||
const pickingId = this.buffers.getPickingId(x, y);
|
||||
if (pickingId === undefined) return;
|
||||
|
||||
const z = this.buffers.getDepth(x, y);
|
||||
const position = Vec3.create(x, y, z);
|
||||
cameraUnproject(position, position, viewport, this.camera.inverseProjectionView);
|
||||
|
||||
return { id: pickingId, position };
|
||||
}
|
||||
|
||||
private prepare(ray: Ray3D, cam: Camera) {
|
||||
this.camera.far = cam.far;
|
||||
this.camera.near = cam.near;
|
||||
this.camera.fogFar = cam.fogFar;
|
||||
this.camera.fogNear = cam.fogNear;
|
||||
Viewport.copy(this.camera.viewport, this.viewport);
|
||||
Camera.copySnapshot(this.camera.state, { ...cam.state, mode: 'orthographic' });
|
||||
|
||||
updateOrthoRayCamera(this.camera, ray);
|
||||
Mat4.mul(this.camera.projectionView, this.camera.projection, this.camera.view);
|
||||
Mat4.tryInvert(this.camera.inverseProjectionView, this.camera.projectionView);
|
||||
}
|
||||
|
||||
private getPickData(): PickData | undefined {
|
||||
const c = this.pickPadding;
|
||||
for (const d of this.spiral) {
|
||||
const pickData = this.identifyInternal(c + d[0], c + d[1]);
|
||||
if (pickData) return pickData;
|
||||
}
|
||||
}
|
||||
|
||||
sphere = Sphere3D();
|
||||
|
||||
private intersectsScene(ray: Ray3D, scale: number): boolean {
|
||||
Sphere3D.scaleNX(this.sphere, this.scene.boundingSphereVisible, scale);
|
||||
return Ray3D.isInsideSphere3D(ray, this.sphere) || Ray3D.isIntersectingSphere3D(ray, this.sphere);
|
||||
}
|
||||
|
||||
identify(ray: Ray3D, cam: Camera): PickData | undefined {
|
||||
if (!this.intersectsScene(ray, cam.state.scale)) return;
|
||||
|
||||
this.prepare(ray, cam);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('RayHelper.identify');
|
||||
this.render(this.camera);
|
||||
this.buffers.read();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('RayHelper.identify');
|
||||
|
||||
return this.getPickData();
|
||||
}
|
||||
|
||||
asyncIdentify(ray: Ray3D, cam: Camera): AsyncPickData | undefined {
|
||||
if (!this.intersectsScene(ray, cam.state.scale)) return;
|
||||
|
||||
this.prepare(ray, cam);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('RayHelper.asyncIdentify');
|
||||
this.render(this.camera);
|
||||
this.buffers.asyncRead();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('RayHelper.asyncIdentify');
|
||||
|
||||
return {
|
||||
tryGet: () => {
|
||||
const status = this.buffers.check();
|
||||
if (status === AsyncPickStatus.Resolved) {
|
||||
return this.getPickData();
|
||||
} else if (status === AsyncPickStatus.Pending) {
|
||||
return 'pending';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.buffers.reset();
|
||||
this.pickPass.reset();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.buffers.dispose();
|
||||
this.pickPass.dispose();
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, options: PickOptions) {
|
||||
const size = options.pickPadding * 2 + 1;
|
||||
|
||||
this.camera = new Camera();
|
||||
this.pickPass = new PickPass(webgl, size, size, 1);
|
||||
this.buffers = new PickBuffers(this.webgl, this.pickPass, options.maxAsyncReadLag);
|
||||
this.pickPadding = options.pickPadding;
|
||||
|
||||
this.update();
|
||||
|
||||
if (!checkAsyncPickingSupport(webgl)) {
|
||||
this.asyncIdentify = (ray, cam) => ({
|
||||
tryGet: () => this.identify(ray, cam)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function updateOrthoRayCamera(camera: Camera, ray: Ray3D) {
|
||||
const { near, far, viewport } = camera;
|
||||
|
||||
const height = 2 * Math.tan(degToRad(0.1) / 2) * Vec3.distance(camera.position, camera.target) * camera.state.scale;
|
||||
const zoom = viewport.height / height;
|
||||
|
||||
const fullLeft = -viewport.width / 2;
|
||||
const fullRight = viewport.width / 2;
|
||||
const fullTop = viewport.height / 2;
|
||||
const fullBottom = -viewport.height / 2;
|
||||
|
||||
const dx = (fullRight - fullLeft) / (2 * zoom);
|
||||
const dy = (fullTop - fullBottom) / (2 * zoom);
|
||||
const cx = (fullRight + fullLeft) / 2;
|
||||
const cy = (fullTop + fullBottom) / 2;
|
||||
|
||||
const left = cx - dx;
|
||||
const right = cx + dx;
|
||||
const top = cy + dy;
|
||||
const bottom = cy - dy;
|
||||
|
||||
// build projection matrix
|
||||
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
|
||||
|
||||
const direction = Vec3.normalize(Vec3(), ray.direction);
|
||||
const r = Quat.fromUnitVec3(Quat(), direction, Vec3.negUnitZ);
|
||||
Quat.invert(r, r);
|
||||
|
||||
const eye = Vec3.clone(ray.origin);
|
||||
const up = Vec3.transformQuat(Vec3(), Vec3.unitY, r);
|
||||
const target = Vec3.add(Vec3(), eye, direction);
|
||||
|
||||
// build view matrix
|
||||
Mat4.lookAt(camera.view, eye, target, up);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -26,6 +26,7 @@ import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
|
||||
import { degToRad, isPowerOfTwo } from '../../mol-math/misc';
|
||||
import { Mat3 } from '../../mol-math/linear-algebra/3d/mat3';
|
||||
import { Euler } from '../../mol-math/linear-algebra/3d/euler';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
const SharedParams = {
|
||||
opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }),
|
||||
@@ -172,10 +173,14 @@ export class BackgroundPass {
|
||||
}
|
||||
|
||||
const m = this.renderable.values.uViewDirectionProjectionInverse.ref.value;
|
||||
Vec3.sub(this.dir, cam.state.position, cam.state.target);
|
||||
Vec3.setMagnitude(this.dir, this.dir, 0.1);
|
||||
Vec3.copy(this.position, this.dir);
|
||||
Mat4.lookAt(m, this.position, this.target, cam.state.up);
|
||||
if (Mat4.isZero(camera.headRotation)) {
|
||||
Vec3.sub(this.dir, cam.state.position, cam.state.target);
|
||||
Vec3.setMagnitude(this.dir, this.dir, 0.1);
|
||||
Vec3.copy(this.position, this.dir);
|
||||
Mat4.lookAt(m, this.position, this.target, cam.state.up);
|
||||
} else {
|
||||
Mat4.invert(m, camera.headRotation);
|
||||
}
|
||||
Mat4.mul(m, cam.projection, m);
|
||||
Mat4.invert(m, m);
|
||||
ValueCell.update(this.renderable.values.uViewDirectionProjectionInverse, m);
|
||||
@@ -292,7 +297,7 @@ export class BackgroundPass {
|
||||
ValueCell.update(this.renderable.values.uViewport, Vec4.set(this.renderable.values.uViewport.ref.value, x, y, width, height));
|
||||
}
|
||||
|
||||
isEnabled(props: BackgroundProps) {
|
||||
private _isEnabled(props: BackgroundProps) {
|
||||
return !!(
|
||||
(this.skybox && this.skybox.loaded) ||
|
||||
(this.image && this.image.loaded) ||
|
||||
@@ -301,6 +306,10 @@ export class BackgroundPass {
|
||||
);
|
||||
}
|
||||
|
||||
isEnabled(props: PostprocessingProps) {
|
||||
return props.enabled && this._isEnabled(props.background);
|
||||
}
|
||||
|
||||
private isReady() {
|
||||
return !!(
|
||||
(this.skybox && this.skybox.loaded) ||
|
||||
@@ -315,7 +324,7 @@ export class BackgroundPass {
|
||||
clear(props: BackgroundProps, transparentBackground: boolean, backgroundColor: Color) {
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
if (this.isEnabled(props)) {
|
||||
if (this._isEnabled(props)) {
|
||||
if (transparentBackground) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
} else {
|
||||
@@ -332,7 +341,7 @@ export class BackgroundPass {
|
||||
}
|
||||
|
||||
render(props: BackgroundProps) {
|
||||
if (!this.isEnabled(props) || !this.isReady()) return;
|
||||
if (!this._isEnabled(props) || !this.isReady()) return;
|
||||
|
||||
if (this.renderable.values.dVariant.ref.value === 'image') {
|
||||
this.updateImageScaling();
|
||||
|
||||
@@ -38,7 +38,7 @@ export type BloomProps = PD.Values<typeof BloomParams>
|
||||
|
||||
export class BloomPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.bloom.name === 'on';
|
||||
return props.enabled && props.bloom.name === 'on';
|
||||
}
|
||||
|
||||
readonly emissiveTarget: RenderTarget;
|
||||
|
||||
@@ -37,7 +37,7 @@ export type DofProps = PD.Values<typeof DofParams>
|
||||
|
||||
export class DofPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.dof.name !== 'off';
|
||||
return props.enabled && props.dof.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
@@ -119,18 +119,18 @@ export class DofPass {
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
const wolrdCenter = (props.center === 'scene-center' ? sphere.center : camera.state.target);
|
||||
const distance = Vec3.distance(camera.state.position, wolrdCenter);
|
||||
const worldCenter = (props.center === 'scene-center' ? sphere.center : camera.state.target);
|
||||
const distance = Vec3.distance(camera.state.position, worldCenter);
|
||||
const inFocus = distance + props.inFocus;
|
||||
ValueCell.updateIfChanged(this.renderable.values.uInFocus, inFocus);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uInFocus, inFocus * camera.state.scale);
|
||||
|
||||
// transform center in view space
|
||||
const center = this.renderable.values.uCenter.ref.value;
|
||||
Vec3.transformMat4(center, wolrdCenter, camera.view);
|
||||
Vec3.transformMat4(center, worldCenter, camera.view);
|
||||
ValueCell.update(this.renderable.values.uCenter, center);
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBlurSpread, props.blurSpread);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uPPM, props.PPM);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uPPM, props.PPM * camera.state.scale);
|
||||
|
||||
if (needsUpdate) {
|
||||
this.renderable.update();
|
||||
|
||||
@@ -167,6 +167,7 @@ export class DpoitPass {
|
||||
if (isTimingMode) this.webgl.timer.mark('DpoitPass.render');
|
||||
const { state, gl } = this.webgl;
|
||||
|
||||
state.blendEquation(gl.FUNC_ADD);
|
||||
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
ValueCell.update(this.renderable.values.tDpoitFrontColor, this.colorFrontTextures[this.writeId]);
|
||||
|
||||
@@ -198,8 +198,10 @@ export class DrawPass {
|
||||
const dpoitTextures = this.dpoit.bindDualDepthPeeling();
|
||||
renderer.renderDpoitTransparent(scene.primitives, camera, this.depthTextureOpaque, dpoitTextures);
|
||||
|
||||
target.bind();
|
||||
this.dpoit.renderBlendBack();
|
||||
if (iterations > 1) {
|
||||
target.bind();
|
||||
this.dpoit.renderBlendBack();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
|
||||
}
|
||||
|
||||
@@ -377,6 +379,7 @@ export class DrawPass {
|
||||
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
|
||||
const markingEnabled = MarkingPass.isEnabled(props.marking);
|
||||
const dofEnabled = DofPass.isEnabled(props.postprocessing);
|
||||
const bloomEnabled = BloomPass.isEnabled(props.postprocessing);
|
||||
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
renderer.setViewport(x, y, width, height);
|
||||
@@ -446,7 +449,7 @@ export class DrawPass {
|
||||
needsTargetCopy = true;
|
||||
}
|
||||
|
||||
if (props.postprocessing.dof.name === 'on') {
|
||||
if (dofEnabled && props.postprocessing.dof.name === 'on') {
|
||||
const input = AntialiasingPass.isEnabled(props.postprocessing)
|
||||
? this.antialiasing.target.texture
|
||||
: PostprocessingPass.isEnabled(props.postprocessing)
|
||||
@@ -469,7 +472,7 @@ export class DrawPass {
|
||||
}
|
||||
}
|
||||
|
||||
if (props.postprocessing.bloom.name === 'on') {
|
||||
if (bloomEnabled && props.postprocessing.bloom.name === 'on') {
|
||||
const emissiveBloom = props.postprocessing.bloom.params.mode === 'emissive';
|
||||
|
||||
if (emissiveBloom && scene.emissiveAverage > 0) {
|
||||
@@ -493,7 +496,7 @@ export class DrawPass {
|
||||
const { renderer, camera, scene, helper } = ctx;
|
||||
|
||||
this.postprocessing.setTransparentBackground(props.transparentBackground);
|
||||
const transparentBackground = props.transparentBackground || this.postprocessing.background.isEnabled(props.postprocessing.background);
|
||||
const transparentBackground = props.transparentBackground || this.postprocessing.background.isEnabled(props.postprocessing);
|
||||
|
||||
renderer.setTransparentBackground(transparentBackground);
|
||||
renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -20,7 +20,7 @@ import { Camera } from '../camera';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { DrawPass } from './draw';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { getBuffer } from '../../mol-gl/webgl/buffer';
|
||||
import { PixelPackBuffer } from '../../mol-gl/webgl/buffer';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3transformMat4 = Vec3.transformMat4;
|
||||
@@ -128,7 +128,7 @@ export class HiZPass {
|
||||
|
||||
private readonly levelData: LevelData = [];
|
||||
private readonly fb: Framebuffer;
|
||||
private readonly buf: WebGLBuffer;
|
||||
private readonly buf: PixelPackBuffer;
|
||||
private readonly tex: Texture;
|
||||
private readonly renderable: HiZRenderable;
|
||||
private readonly supported: boolean;
|
||||
@@ -221,10 +221,7 @@ export class HiZPass {
|
||||
const hw = this.tex.getWidth();
|
||||
const hh = this.tex.getHeight();
|
||||
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.buf);
|
||||
gl.bufferData(gl.PIXEL_PACK_BUFFER, this.buffer.byteLength, gl.STREAM_READ);
|
||||
gl.readPixels(0, 0, hw, hh, gl.RED, gl.FLOAT, 0);
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||
this.buf.read(0, 0, hw, hh);
|
||||
|
||||
this.sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
gl.flush();
|
||||
@@ -249,9 +246,7 @@ export class HiZPass {
|
||||
this.frameLag += 1;
|
||||
// console.log(`waiting for buffer data for ${this.frameLag} frames`);
|
||||
} else {
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.buf);
|
||||
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, this.buffer);
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||
this.buf.getSubData(this.buffer);
|
||||
// console.log(`got buffer data after ${this.frameLag + 1} frames`);
|
||||
gl.deleteSync(this.sync);
|
||||
this.sync = null;
|
||||
@@ -510,6 +505,16 @@ export class HiZPass {
|
||||
|
||||
//
|
||||
|
||||
reset() {
|
||||
this.sync = null;
|
||||
this.ready = false;
|
||||
this.frameLag = 0;
|
||||
this.levelData.length = 0;
|
||||
|
||||
const { x, y, width, height } = this.viewport;
|
||||
this.setViewport(x, y, width, height);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (!this.supported) return;
|
||||
|
||||
@@ -517,7 +522,7 @@ export class HiZPass {
|
||||
|
||||
this.fb.destroy();
|
||||
this.tex.destroy();
|
||||
this.webgl.gl.deleteBuffer(this.buf);
|
||||
this.buf.destroy();
|
||||
this.renderable.dispose();
|
||||
|
||||
for (const td of this.levelData) {
|
||||
@@ -527,6 +532,8 @@ export class HiZPass {
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, private drawPass: DrawPass, canvas: HTMLCanvasElement | undefined, props: Partial<HiZProps>) {
|
||||
this.props = { ...PD.getDefaultValues(HiZParams), ...props };
|
||||
|
||||
const { gl, extensions } = webgl;
|
||||
if (!isWebGL2(gl) || !extensions.colorBufferFloat) {
|
||||
if (isDebugMode) {
|
||||
@@ -552,8 +559,7 @@ export class HiZPass {
|
||||
}
|
||||
|
||||
this.supported = true;
|
||||
this.props = { ...PD.getDefaultValues(HiZParams), ...props };
|
||||
this.buf = getBuffer(gl);
|
||||
this.buf = webgl.resources.pixelPack('alpha', 'float');
|
||||
this.renderable = createHiZRenderable(webgl, this.drawPass.depthTextureOpaque);
|
||||
|
||||
if (isDebugMode && canvas) {
|
||||
|
||||
@@ -33,6 +33,8 @@ import { JitterVectors, MultiSampleProps } from './multi-sample';
|
||||
import { compose_frag as multiSample_compose_frag } from '../../mol-gl/shader/compose.frag';
|
||||
import { clamp, lerp } from '../../mol-math/interpolate';
|
||||
import { SsaoProps } from './ssao';
|
||||
import { OutlinePass } from './outline';
|
||||
import { BloomPass } from './bloom';
|
||||
|
||||
type Props = {
|
||||
transparentBackground: boolean;
|
||||
@@ -169,13 +171,15 @@ export class IlluminationPass {
|
||||
const dpoitTextures = this.drawPass.dpoit.bind();
|
||||
renderer.renderDpoitTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque, dpoitTextures);
|
||||
|
||||
for (let i = 0, il = props.dpoitIterations; i < il; i++) {
|
||||
for (let i = 0, iterations = props.dpoitIterations; i < iterations; i++) {
|
||||
if (isTimingMode) this.webgl.timer.mark('DpoitPass.layer');
|
||||
const dpoitTextures = this.drawPass.dpoit.bindDualDepthPeeling();
|
||||
renderer.renderDpoitTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque, dpoitTextures);
|
||||
|
||||
this.transparentTarget.bind();
|
||||
this.drawPass.dpoit.renderBlendBack();
|
||||
if (iterations > 1) {
|
||||
this.transparentTarget.bind();
|
||||
this.drawPass.dpoit.renderBlendBack();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
|
||||
}
|
||||
|
||||
@@ -313,8 +317,11 @@ export class IlluminationPass {
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
|
||||
const outlinesEnabled = props.postprocessing.outline.name === 'on' && !props.illumination.ignoreOutline;
|
||||
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
|
||||
const outlinesEnabled = OutlinePass.isEnabled(props.postprocessing) && !props.illumination.ignoreOutline;
|
||||
const occlusionEnabled = PostprocessingPass.isTransparentSsaoEnabled(scene, props.postprocessing);
|
||||
const bloomEnabled = BloomPass.isEnabled(props.postprocessing);
|
||||
const dofEnabled = DofPass.isEnabled(props.postprocessing);
|
||||
|
||||
const markingEnabled = MarkingPass.isEnabled(props.marking);
|
||||
const hasTransparent = scene.opacityAverage < 1;
|
||||
@@ -327,7 +334,7 @@ export class IlluminationPass {
|
||||
ValueCell.update(this.composeRenderable.values.dOutlineEnable, outlinesEnabled);
|
||||
}
|
||||
|
||||
if (props.postprocessing.outline.name === 'on') {
|
||||
if (outlinesEnabled && props.postprocessing.outline.name === 'on') {
|
||||
const { transparentOutline, outlineScale } = this.drawPass.postprocessing.outline.update(camera, props.postprocessing.outline.params, this.drawPass.depthTargetTransparent.texture, this.drawPass.depthTextureOpaque);
|
||||
this.drawPass.postprocessing.outline.render();
|
||||
|
||||
@@ -348,7 +355,7 @@ export class IlluminationPass {
|
||||
ValueCell.update(this.composeRenderable.values.dOcclusionEnable, occlusionEnabled);
|
||||
}
|
||||
|
||||
if (props.postprocessing.occlusion.name === 'on') {
|
||||
if (occlusionEnabled && props.postprocessing.occlusion.name === 'on') {
|
||||
ValueCell.update(this.composeRenderable.values.uOcclusionColor, Color.toVec3Normalized(this.composeRenderable.values.uOcclusionColor.ref.value, props.postprocessing.occlusion.params.color));
|
||||
}
|
||||
|
||||
@@ -370,7 +377,7 @@ export class IlluminationPass {
|
||||
|
||||
// background
|
||||
|
||||
const _toDrawingBuffer = toDrawingBuffer && !AntialiasingPass.isEnabled(props.postprocessing) && props.postprocessing.dof.name === 'off';
|
||||
const _toDrawingBuffer = toDrawingBuffer && !antialiasingEnabled && !dofEnabled;
|
||||
if (_toDrawingBuffer) {
|
||||
this.webgl.bindDrawingBuffer();
|
||||
} else {
|
||||
@@ -384,7 +391,7 @@ export class IlluminationPass {
|
||||
|
||||
// compose
|
||||
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uTransparentBackground, props.transparentBackground || this.drawPass.postprocessing.background.isEnabled(props.postprocessing.background));
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uTransparentBackground, props.transparentBackground || this.drawPass.postprocessing.background.isEnabled(props.postprocessing));
|
||||
if (this.composeRenderable.values.dDenoise.ref.value !== props.illumination.denoise) {
|
||||
ValueCell.update(this.composeRenderable.values.dDenoise, props.illumination.denoise);
|
||||
needsUpdateCompose = true;
|
||||
@@ -421,8 +428,8 @@ export class IlluminationPass {
|
||||
let targetIsDrawingbuffer = false;
|
||||
let swapTarget = this.outputTarget;
|
||||
|
||||
if (AntialiasingPass.isEnabled(props.postprocessing)) {
|
||||
const _toDrawingBuffer = toDrawingBuffer && props.postprocessing.dof.name === 'off';
|
||||
if (antialiasingEnabled) {
|
||||
const _toDrawingBuffer = toDrawingBuffer && !dofEnabled;
|
||||
this.drawPass.antialiasing.render(camera, this.tracing.composeTarget.texture, _toDrawingBuffer ? true : this.outputTarget, props.postprocessing);
|
||||
|
||||
if (_toDrawingBuffer) {
|
||||
@@ -433,13 +440,13 @@ export class IlluminationPass {
|
||||
}
|
||||
}
|
||||
|
||||
if (props.postprocessing.bloom.name === 'on') {
|
||||
const _toDrawingBuffer = (toDrawingBuffer && props.postprocessing.dof.name === 'off') || targetIsDrawingbuffer;
|
||||
if (bloomEnabled && props.postprocessing.bloom.name === 'on') {
|
||||
const _toDrawingBuffer = (toDrawingBuffer && !dofEnabled) || targetIsDrawingbuffer;
|
||||
this.drawPass.bloom.update(this.tracing.colorTextureOpaque, this.tracing.normalTextureOpaque, this.drawPass.depthTextureOpaque, props.postprocessing.bloom.params);
|
||||
this.drawPass.bloom.render(camera.viewport, _toDrawingBuffer ? undefined : this._colorTarget);
|
||||
}
|
||||
|
||||
if (props.postprocessing.dof.name === 'on') {
|
||||
if (dofEnabled && props.postprocessing.dof.name === 'on') {
|
||||
const _toDrawingBuffer = toDrawingBuffer || targetIsDrawingbuffer;
|
||||
this.drawPass.dof.update(camera, this._colorTarget.texture, this.drawPass.depthTextureOpaque, this.drawPass.depthTargetTransparent.texture, props.postprocessing.dof.params, scene.boundingSphereVisible);
|
||||
this.drawPass.dof.render(camera.viewport, _toDrawingBuffer ? undefined : swapTarget);
|
||||
|
||||
@@ -36,7 +36,7 @@ export type OutlineProps = PD.Values<typeof OutlineParams>
|
||||
|
||||
export class OutlinePass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.outline.name !== 'off';
|
||||
return props.enabled && props.outline.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Passes {
|
||||
constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, transparency: 'wboit' | 'dpoit' | 'blended' }> = {}) {
|
||||
const drs = this.webgl.getDrawingBufferSize();
|
||||
this.draw = new DrawPass(webgl, assetManager, drs.width, drs.height, attribs.transparency || 'blended');
|
||||
this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
|
||||
this.pick = new PickPass(webgl, drs.width, drs.height, attribs.pickScale || 0.25);
|
||||
this.multiSample = new MultiSamplePass(webgl, this.draw);
|
||||
this.illumination = new IlluminationPass(webgl, this.draw);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export class Passes {
|
||||
const width = Math.max(drs.width, 2);
|
||||
const height = Math.max(drs.height, 2);
|
||||
this.draw.setSize(width, height);
|
||||
this.pick.syncSize();
|
||||
this.pick.setSize(width, height);
|
||||
this.multiSample.syncSize();
|
||||
this.illumination.setSize(width, height);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { PickType, Renderer } from '../../mol-gl/renderer';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { PixelPackBuffer } from '../../mol-gl/webgl/buffer';
|
||||
import { isWebGL2 } from '../../mol-gl/webgl/compat';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
@@ -14,20 +15,29 @@ import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { Renderbuffer } from '../../mol-gl/webgl/renderbuffer';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { spiral2d } from '../../mol-math/misc';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { unpackRGBToInt, unpackRGBAToDepth } from '../../mol-util/number-packing';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { StereoCamera } from '../camera/stereo';
|
||||
import { cameraUnproject } from '../camera/util';
|
||||
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
import { now } from '../../mol-util/now';
|
||||
import { unpackRGBAToDepth, unpackRGBToInt } from '../../mol-util/number-packing';
|
||||
import { ICamera } from '../camera';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { Helper } from '../helper/helper';
|
||||
import { DrawPass } from './draw';
|
||||
|
||||
const NullId = Math.pow(2, 24) - 2;
|
||||
|
||||
export type PickData = { id: PickingId, position: Vec3 }
|
||||
|
||||
export type AsyncPickData = {
|
||||
tryGet: () => 'pending' | PickData | undefined,
|
||||
}
|
||||
|
||||
export const DefaultPickOptions = {
|
||||
pickPadding: 1,
|
||||
maxAsyncReadLag: 5,
|
||||
};
|
||||
export type PickOptions = typeof DefaultPickOptions
|
||||
|
||||
//
|
||||
|
||||
export class PickPass {
|
||||
private readonly objectPickTarget: RenderTarget;
|
||||
private readonly instancePickTarget: RenderTarget;
|
||||
@@ -51,10 +61,10 @@ export class PickPass {
|
||||
private pickWidth: number;
|
||||
private pickHeight: number;
|
||||
|
||||
constructor(private webgl: WebGLContext, private drawPass: DrawPass, private pickScale: number) {
|
||||
constructor(private webgl: WebGLContext, private width: number, private height: number, private pickScale: number) {
|
||||
const pickRatio = pickScale / webgl.pixelRatio;
|
||||
this.pickWidth = Math.ceil(drawPass.colorTarget.getWidth() * pickRatio);
|
||||
this.pickHeight = Math.ceil(drawPass.colorTarget.getHeight() * pickRatio);
|
||||
this.pickWidth = Math.ceil(width * pickRatio);
|
||||
this.pickHeight = Math.ceil(height * pickRatio);
|
||||
|
||||
const { resources, extensions: { drawBuffers }, gl } = webgl;
|
||||
|
||||
@@ -109,13 +119,36 @@ export class PickPass {
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.webgl.extensions.drawBuffers) {
|
||||
this.framebuffer.destroy();
|
||||
|
||||
this.objectPickTexture.destroy();
|
||||
this.instancePickTexture.destroy();
|
||||
this.groupPickTexture.destroy();
|
||||
this.depthPickTexture.destroy();
|
||||
|
||||
this.objectPickFramebuffer.destroy();
|
||||
this.instancePickFramebuffer.destroy();
|
||||
this.groupPickFramebuffer.destroy();
|
||||
this.depthPickFramebuffer.destroy();
|
||||
|
||||
this.depthRenderbuffer.destroy();
|
||||
} else {
|
||||
this.objectPickTarget.destroy();
|
||||
this.instancePickTarget.destroy();
|
||||
this.groupPickTarget.destroy();
|
||||
this.depthPickTarget.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
get pickRatio() {
|
||||
return this.pickScale / this.webgl.pixelRatio;
|
||||
}
|
||||
|
||||
setPickScale(pickScale: number) {
|
||||
this.pickScale = pickScale;
|
||||
this.syncSize();
|
||||
this.setSize(this.width, this.height);
|
||||
}
|
||||
|
||||
bindObject() {
|
||||
@@ -151,13 +184,16 @@ export class PickPass {
|
||||
}
|
||||
|
||||
get drawingBufferHeight() {
|
||||
return this.drawPass.colorTarget.getHeight();
|
||||
return this.height;
|
||||
}
|
||||
|
||||
syncSize() {
|
||||
setSize(width: number, height: number) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
const pickRatio = this.pickScale / this.webgl.pixelRatio;
|
||||
const pickWidth = Math.ceil(this.drawPass.colorTarget.getWidth() * pickRatio);
|
||||
const pickHeight = Math.ceil(this.drawPass.colorTarget.getHeight() * pickRatio);
|
||||
const pickWidth = Math.ceil(this.width * pickRatio);
|
||||
const pickHeight = Math.ceil(this.height * pickRatio);
|
||||
|
||||
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
|
||||
this.pickWidth = pickWidth;
|
||||
@@ -225,6 +261,7 @@ export class PickPass {
|
||||
if (this.webgl.extensions.drawBuffers) {
|
||||
this.framebuffer.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.None);
|
||||
// printTextureImage(readTexture(this.webgl, this.groupPickTexture, new Uint8Array(this.pickWidth * this.pickHeight * 4)), { scale: 16, id: 'group', pixelated: true, useCanvas: true, flipY: true });
|
||||
} else {
|
||||
this.objectPickTarget.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.Object);
|
||||
@@ -234,7 +271,7 @@ export class PickPass {
|
||||
|
||||
this.groupPickTarget.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'pick', PickType.Group);
|
||||
// printTexture(this.webgl, this.groupPickTarget.texture, { id: 'group' })
|
||||
// printTextureImage(readTexture(this.webgl, this.groupPickTarget.texture, new Uint8Array(this.pickWidth * this.pickHeight * 4)), { scale: 16, id: 'group', pixelated: true, useCanvas: true, flipY: true });
|
||||
|
||||
this.depthPickTarget.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'depth', PickType.None);
|
||||
@@ -242,200 +279,220 @@ export class PickPass {
|
||||
}
|
||||
}
|
||||
|
||||
export class PickHelper {
|
||||
dirty = true;
|
||||
let AsyncPickingWarningShown = false;
|
||||
|
||||
private objectBuffer: Uint8Array;
|
||||
private instanceBuffer: Uint8Array;
|
||||
private groupBuffer: Uint8Array;
|
||||
private depthBuffer: Uint8Array;
|
||||
export function checkAsyncPickingSupport(webgl: WebGLContext): boolean {
|
||||
if (webgl.isWebGL2) return true;
|
||||
|
||||
private viewport = Viewport();
|
||||
if (isDebugMode && !AsyncPickingWarningShown) {
|
||||
console.log('WebGL2 required for async picking. Falling back to synchronous picking.');
|
||||
AsyncPickingWarningShown = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private pickRatio: number;
|
||||
private pickX: number;
|
||||
private pickY: number;
|
||||
private pickWidth: number;
|
||||
private pickHeight: number;
|
||||
private halfPickWidth: number;
|
||||
export enum AsyncPickStatus { Pending, Resolved, Failed };
|
||||
|
||||
private spiral: [number, number][];
|
||||
export class PickBuffers {
|
||||
private object: Uint8Array;
|
||||
private instance: Uint8Array;
|
||||
private group: Uint8Array;
|
||||
private depth: Uint8Array;
|
||||
|
||||
private setupBuffers() {
|
||||
const bufferSize = this.pickWidth * this.pickHeight * 4;
|
||||
if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
|
||||
this.objectBuffer = new Uint8Array(bufferSize);
|
||||
this.instanceBuffer = new Uint8Array(bufferSize);
|
||||
this.groupBuffer = new Uint8Array(bufferSize);
|
||||
this.depthBuffer = new Uint8Array(bufferSize);
|
||||
private objectBuffer: PixelPackBuffer;
|
||||
private instanceBuffer: PixelPackBuffer;
|
||||
private groupBuffer: PixelPackBuffer;
|
||||
private depthBuffer: PixelPackBuffer;
|
||||
|
||||
private viewport = Viewport.create(0, 0, 0, 0);
|
||||
|
||||
private setup() {
|
||||
const size = this.viewport.width * this.viewport.height * 4;
|
||||
if (!this.object || this.object.length !== size) {
|
||||
this.object = new Uint8Array(size);
|
||||
this.instance = new Uint8Array(size);
|
||||
this.group = new Uint8Array(size);
|
||||
this.depth = new Uint8Array(size);
|
||||
}
|
||||
}
|
||||
|
||||
setViewport(x: number, y: number, width: number, height: number) {
|
||||
Viewport.set(this.viewport, x, y, width, height);
|
||||
this.update();
|
||||
this.setup();
|
||||
}
|
||||
|
||||
setPickPadding(pickPadding: number) {
|
||||
if (this.pickPadding !== pickPadding) {
|
||||
this.pickPadding = pickPadding;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
read() {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickBuffers.read');
|
||||
const { x, y, width, height } = this.viewport;
|
||||
|
||||
this.pickRatio = this.pickPass.pickRatio;
|
||||
this.pickX = Math.ceil(x * this.pickRatio);
|
||||
this.pickY = Math.ceil(y * this.pickRatio);
|
||||
|
||||
const pickWidth = Math.floor(width * this.pickRatio);
|
||||
const pickHeight = Math.floor(height * this.pickRatio);
|
||||
|
||||
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
|
||||
this.pickWidth = pickWidth;
|
||||
this.pickHeight = pickHeight;
|
||||
this.halfPickWidth = Math.floor(this.pickWidth / 2);
|
||||
|
||||
this.setupBuffers();
|
||||
}
|
||||
|
||||
this.spiral = spiral2d(Math.ceil(this.pickRatio * this.pickPadding));
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private syncBuffers() {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.syncBuffers');
|
||||
const { pickX, pickY, pickWidth, pickHeight } = this;
|
||||
|
||||
this.pickPass.bindObject();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.objectBuffer);
|
||||
this.webgl.readPixels(x, y, width, height, this.object);
|
||||
|
||||
this.pickPass.bindInstance();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.instanceBuffer);
|
||||
this.webgl.readPixels(x, y, width, height, this.instance);
|
||||
|
||||
this.pickPass.bindGroup();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.groupBuffer);
|
||||
this.webgl.readPixels(x, y, width, height, this.group);
|
||||
|
||||
this.pickPass.bindDepth();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.depthBuffer);
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.syncBuffers');
|
||||
this.webgl.readPixels(x, y, width, height, this.depth);
|
||||
|
||||
this.ready = true;
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickBuffers.read');
|
||||
}
|
||||
|
||||
private getBufferIdx(x: number, y: number): number {
|
||||
return (y * this.pickWidth + x) * 4;
|
||||
private fenceSync: WebGLSync | null = null;
|
||||
private fenceTimestamp: number = 0;
|
||||
|
||||
private ready = false;
|
||||
private lag = 0;
|
||||
|
||||
asyncRead() {
|
||||
const { gl } = this.webgl;
|
||||
if (!isWebGL2(gl)) return;
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('PickBuffers.asyncRead');
|
||||
if (this.fenceSync !== null) {
|
||||
gl.deleteSync(this.fenceSync);
|
||||
}
|
||||
const { x, y, width, height } = this.viewport;
|
||||
|
||||
this.pickPass.bindObject();
|
||||
this.objectBuffer.read(x, y, width, height);
|
||||
|
||||
this.pickPass.bindInstance();
|
||||
this.instanceBuffer.read(x, y, width, height);
|
||||
|
||||
this.pickPass.bindGroup();
|
||||
this.groupBuffer.read(x, y, width, height);
|
||||
|
||||
this.pickPass.bindDepth();
|
||||
this.depthBuffer.read(x, y, width, height);
|
||||
|
||||
this.fenceTimestamp = now();
|
||||
this.fenceSync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
// gl.flush();
|
||||
|
||||
this.ready = false;
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickBuffers.asyncRead');
|
||||
}
|
||||
|
||||
private getDepth(x: number, y: number): number {
|
||||
const idx = this.getBufferIdx(x, y);
|
||||
const b = this.depthBuffer;
|
||||
check(): AsyncPickStatus {
|
||||
if (this.ready) return AsyncPickStatus.Resolved;
|
||||
if (this.fenceSync === null) return AsyncPickStatus.Failed;
|
||||
|
||||
const { gl } = this.webgl;
|
||||
if (!isWebGL2(gl)) return AsyncPickStatus.Failed;
|
||||
|
||||
const res = gl.clientWaitSync(this.fenceSync, 0, 0);
|
||||
if (res === gl.WAIT_FAILED || this.lag >= this.maxAsyncReadLag) {
|
||||
// console.log(`failed to get buffer data after ${this.lag + 1} checks`);
|
||||
if (res !== gl.WAIT_FAILED && now() - this.fenceTimestamp < 1000 / 60) {
|
||||
this.lag += 1;
|
||||
return AsyncPickStatus.Pending;
|
||||
}
|
||||
gl.deleteSync(this.fenceSync);
|
||||
this.fenceSync = null;
|
||||
this.lag = 0;
|
||||
this.ready = false;
|
||||
return AsyncPickStatus.Failed;
|
||||
} else if (res === gl.TIMEOUT_EXPIRED) {
|
||||
this.lag += 1;
|
||||
// console.log(`waiting for buffer data for ${this.lag} checks`);
|
||||
return AsyncPickStatus.Pending;
|
||||
} else {
|
||||
this.objectBuffer.getSubData(this.object);
|
||||
this.instanceBuffer.getSubData(this.instance);
|
||||
this.groupBuffer.getSubData(this.group);
|
||||
this.depthBuffer.getSubData(this.depth);
|
||||
|
||||
// console.log(`got buffer data after ${this.lag + 1} checks`);
|
||||
gl.deleteSync(this.fenceSync);
|
||||
this.fenceSync = null;
|
||||
this.lag = 0;
|
||||
this.ready = true;
|
||||
|
||||
return AsyncPickStatus.Resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private getIdx(x: number, y: number): number {
|
||||
return (y * this.viewport.width + x) * 4;
|
||||
}
|
||||
|
||||
getDepth(x: number, y: number): number {
|
||||
if (!this.ready) return -1;
|
||||
|
||||
const idx = this.getIdx(x, y);
|
||||
const b = this.depth;
|
||||
return unpackRGBAToDepth(b[idx], b[idx + 1], b[idx + 2], b[idx + 3]);
|
||||
}
|
||||
|
||||
private getId(x: number, y: number, buffer: Uint8Array) {
|
||||
const idx = this.getBufferIdx(x, y);
|
||||
if (!this.ready) return -1;
|
||||
|
||||
const idx = this.getIdx(x, y);
|
||||
return unpackRGBToInt(buffer[idx], buffer[idx + 1], buffer[idx + 2]);
|
||||
}
|
||||
|
||||
private render(camera: Camera | StereoCamera) {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', { captureStats: true });
|
||||
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
|
||||
const { renderer, scene, helper } = this;
|
||||
|
||||
renderer.setTransparentBackground(false);
|
||||
renderer.setDrawingBufferSize(pickWidth, pickHeight);
|
||||
renderer.setPixelRatio(this.pickRatio);
|
||||
|
||||
if (StereoCamera.is(camera)) {
|
||||
renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera.left, scene, helper);
|
||||
|
||||
renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera.right, scene, helper);
|
||||
} else {
|
||||
renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera, scene, helper);
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.render');
|
||||
getObjectId(x: number, y: number) {
|
||||
return this.getId(x, y, this.object);
|
||||
}
|
||||
|
||||
private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
|
||||
if (this.pickRatio !== this.pickPass.pickRatio) {
|
||||
this.update();
|
||||
}
|
||||
getInstanceId(x: number, y: number) {
|
||||
return this.getId(x, y, this.instance);
|
||||
}
|
||||
|
||||
const { webgl, pickRatio } = this;
|
||||
if (webgl.isContextLost) return;
|
||||
getGroupId(x: number, y: number) {
|
||||
return this.getId(x, y, this.group);
|
||||
}
|
||||
|
||||
x *= webgl.pixelRatio;
|
||||
y *= webgl.pixelRatio;
|
||||
y = this.pickPass.drawingBufferHeight - y; // flip y
|
||||
|
||||
const { viewport } = this;
|
||||
|
||||
// check if within viewport
|
||||
if (x < viewport.x ||
|
||||
y < viewport.y ||
|
||||
x > viewport.x + viewport.width ||
|
||||
y > viewport.y + viewport.height
|
||||
) return;
|
||||
|
||||
if (this.dirty) {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.identify');
|
||||
this.render(camera);
|
||||
this.syncBuffers();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PickHelper.identify');
|
||||
}
|
||||
|
||||
const xv = x - viewport.x;
|
||||
const yv = y - viewport.y;
|
||||
|
||||
const xp = Math.floor(xv * pickRatio);
|
||||
const yp = Math.floor(yv * pickRatio);
|
||||
|
||||
const objectId = this.getId(xp, yp, this.objectBuffer);
|
||||
getPickingId(x: number, y: number): PickingId | undefined {
|
||||
const objectId = this.getObjectId(x, y);
|
||||
// console.log('objectId', objectId);
|
||||
if (objectId === -1 || objectId === NullId) return;
|
||||
|
||||
const instanceId = this.getId(xp, yp, this.instanceBuffer);
|
||||
const instanceId = this.getInstanceId(x, y);
|
||||
// console.log('instanceId', instanceId);
|
||||
if (instanceId === -1 || instanceId === NullId) return;
|
||||
|
||||
const groupId = this.getId(xp, yp, this.groupBuffer);
|
||||
const groupId = this.getGroupId(x, y);
|
||||
// console.log('groupId', groupId);
|
||||
if (groupId === -1 || groupId === NullId) return;
|
||||
|
||||
const z = this.getDepth(xp, yp);
|
||||
// console.log('z', z);
|
||||
const position = Vec3.create(x, y, z);
|
||||
if (StereoCamera.is(camera)) {
|
||||
const halfWidth = Math.floor(viewport.width / 2);
|
||||
if (x > viewport.x + halfWidth) {
|
||||
position[0] = viewport.x + (xv - halfWidth) * 2;
|
||||
cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
|
||||
} else {
|
||||
position[0] = viewport.x + xv * 2;
|
||||
cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
|
||||
}
|
||||
} else {
|
||||
cameraUnproject(position, position, viewport, camera.inverseProjectionView);
|
||||
}
|
||||
|
||||
// console.log({ id: { objectId, instanceId, groupId }, position });
|
||||
return { id: { objectId, instanceId, groupId }, position };
|
||||
return { objectId, instanceId, groupId };
|
||||
}
|
||||
|
||||
identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
|
||||
for (const d of this.spiral) {
|
||||
const pickData = this.identifyInternal(x + d[0], y + d[1], camera);
|
||||
if (pickData) return pickData;
|
||||
reset() {
|
||||
this.fenceSync = null;
|
||||
this.ready = false;
|
||||
this.lag = 0;
|
||||
this.fenceTimestamp = 0;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
const { gl } = this.webgl;
|
||||
if (!isWebGL2(gl)) return;
|
||||
|
||||
this.objectBuffer.destroy();
|
||||
this.instanceBuffer.destroy();
|
||||
this.groupBuffer.destroy();
|
||||
this.depthBuffer.destroy();
|
||||
|
||||
if (this.fenceSync !== null) {
|
||||
gl.deleteSync(this.fenceSync);
|
||||
this.fenceSync = null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, private pickPadding = 1) {
|
||||
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||
constructor(private webgl: WebGLContext, private pickPass: PickPass, public maxAsyncReadLag = 5) {
|
||||
if (webgl.isWebGL2) {
|
||||
this.objectBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
|
||||
this.instanceBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
|
||||
this.groupBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
|
||||
this.depthBuffer = webgl.resources.pixelPack('rgba', 'ubyte');
|
||||
}
|
||||
|
||||
this.setup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,59 +120,60 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, t
|
||||
}
|
||||
|
||||
export const PostprocessingParams = {
|
||||
enabled: PD.Boolean(true),
|
||||
occlusion: PD.MappedStatic('on', {
|
||||
on: PD.Group(SsaoParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
|
||||
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect', hideIf: p => p.enabled === false }),
|
||||
shadow: PD.MappedStatic('off', {
|
||||
on: PD.Group(ShadowParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Simplistic shadows' }),
|
||||
}, { cycle: true, description: 'Simplistic shadows', hideIf: p => p.enabled === false }),
|
||||
outline: PD.MappedStatic('off', {
|
||||
on: PD.Group(OutlineParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Draw outline around 3D objects' }),
|
||||
}, { cycle: true, description: 'Draw outline around 3D objects', hideIf: p => p.enabled === false }),
|
||||
dof: PD.MappedStatic('off', {
|
||||
on: PD.Group(DofParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'DOF' }),
|
||||
}, { cycle: true, description: 'DOF', hideIf: p => p.enabled === false }),
|
||||
antialiasing: PD.MappedStatic('smaa', {
|
||||
fxaa: PD.Group(FxaaParams),
|
||||
smaa: PD.Group(SmaaParams),
|
||||
off: PD.Group({})
|
||||
}, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
|
||||
}, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges', hideIf: p => p.enabled === false }),
|
||||
sharpening: PD.MappedStatic('off', {
|
||||
on: PD.Group(CasParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Contrast Adaptive Sharpening' }),
|
||||
background: PD.Group(BackgroundParams, { isFlat: true }),
|
||||
}, { cycle: true, description: 'Contrast Adaptive Sharpening', hideIf: p => p.enabled === false }),
|
||||
background: PD.Group(BackgroundParams, { isFlat: true, hideIf: p => p.enabled === false }),
|
||||
bloom: PD.MappedStatic('on', {
|
||||
on: PD.Group(BloomParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Bloom' }),
|
||||
}, { cycle: true, description: 'Bloom', hideIf: p => p.enabled === false }),
|
||||
};
|
||||
|
||||
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
|
||||
|
||||
export class PostprocessingPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
|
||||
return props.enabled && (SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off');
|
||||
}
|
||||
|
||||
static isTransparentDepthRequired(scene: Scene, props: PostprocessingProps) {
|
||||
return DofPass.isEnabled(props) || OutlinePass.isEnabled(props) && PostprocessingPass.isTransparentOutlineEnabled(props) || SsaoPass.isEnabled(props) && PostprocessingPass.isTransparentSsaoEnabled(scene, props);
|
||||
return props.enabled && (DofPass.isEnabled(props) || OutlinePass.isEnabled(props) && PostprocessingPass.isTransparentOutlineEnabled(props) || SsaoPass.isEnabled(props) && PostprocessingPass.isTransparentSsaoEnabled(scene, props));
|
||||
}
|
||||
|
||||
static isTransparentOutlineEnabled(props: PostprocessingProps) {
|
||||
return OutlinePass.isEnabled(props) && ((props.outline.params as OutlineProps).includeTransparent ?? true);
|
||||
return props.enabled && OutlinePass.isEnabled(props) && ((props.outline.params as OutlineProps).includeTransparent ?? true);
|
||||
}
|
||||
|
||||
static isTransparentSsaoEnabled(scene: Scene, props: PostprocessingProps) {
|
||||
return SsaoPass.isEnabled(props) && SsaoPass.isTransparentEnabled(scene, props.occlusion.params as SsaoProps);
|
||||
return props.enabled && SsaoPass.isEnabled(props) && SsaoPass.isTransparentEnabled(scene, props.occlusion.params as SsaoProps);
|
||||
}
|
||||
|
||||
static isSsaoEnabled(props: PostprocessingProps) {
|
||||
return SsaoPass.isEnabled(props);
|
||||
return props.enabled && SsaoPass.isEnabled(props);
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
@@ -354,7 +355,7 @@ export class PostprocessingPass {
|
||||
|
||||
export class AntialiasingPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.antialiasing.name !== 'off';
|
||||
return props.enabled && (props.antialiasing.name !== 'off' || props.sharpening.name !== 'off');
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -35,7 +35,7 @@ export type ShadowProps = PD.Values<typeof ShadowParams>
|
||||
|
||||
export class ShadowPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.shadow.name !== 'off';
|
||||
return props.enabled && props.shadow.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
@@ -83,8 +83,8 @@ export class ShadowPass {
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uMaxDistance, props.maxDistance);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uTolerance, props.tolerance);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uMaxDistance, props.maxDistance * camera.state.scale);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uTolerance, props.tolerance * camera.state.scale);
|
||||
if (this.renderable.values.dSteps.ref.value !== props.steps) {
|
||||
ValueCell.update(this.renderable.values.dSteps, props.steps);
|
||||
needsUpdateShadows = true;
|
||||
|
||||
@@ -63,7 +63,7 @@ type Levels = {
|
||||
bias: number[]
|
||||
}
|
||||
|
||||
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
|
||||
function getLevels(props: { radius: number, bias: number }[], scale: number, levels?: Levels): Levels {
|
||||
const count = props.length;
|
||||
const { radius, bias } = levels || {
|
||||
radius: (new Array(count * 3)).fill(0),
|
||||
@@ -72,7 +72,7 @@ function getLevels(props: { radius: number, bias: number }[], levels?: Levels):
|
||||
props = props.slice().sort((a, b) => a.radius - b.radius);
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const p = props[i];
|
||||
radius[i] = Math.pow(2, p.radius);
|
||||
radius[i] = Math.pow(2, p.radius) * scale;
|
||||
bias[i] = p.bias;
|
||||
}
|
||||
return { count, radius, bias };
|
||||
@@ -126,6 +126,7 @@ export class SsaoPass {
|
||||
private nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
private texSize: [number, number];
|
||||
private invProjection = Mat4.identity();
|
||||
|
||||
private ssaoScale: number;
|
||||
private calcSsaoScale(resolutionScale: number) {
|
||||
@@ -275,9 +276,7 @@ export class SsaoPass {
|
||||
let needsUpdateDepthHalf = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
|
||||
const invProjection = Mat4.identity();
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
const invProjection = Mat4.invert(this.invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.texSize;
|
||||
const v = camera.viewport;
|
||||
@@ -306,8 +305,8 @@ export class SsaoPass {
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uInvProjection, invProjection);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias * camera.state.scale);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias * camera.state.scale);
|
||||
|
||||
if (this.blurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
@@ -349,7 +348,7 @@ export class SsaoPass {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
this.levels = mp.levels;
|
||||
const levels = getLevels(mp.levels);
|
||||
const levels = getLevels(mp.levels, camera.state.scale);
|
||||
ValueCell.updateIfChanged(this.renderable.values.dLevels, levels.count);
|
||||
|
||||
ValueCell.update(this.renderable.values.uLevelRadius, levels.radius);
|
||||
@@ -358,7 +357,7 @@ export class SsaoPass {
|
||||
ValueCell.updateIfChanged(this.renderable.values.uNearThreshold, mp.nearThreshold);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFarThreshold, mp.farThreshold);
|
||||
} else {
|
||||
ValueCell.updateIfChanged(this.renderable.values.uRadius, Math.pow(2, props.radius));
|
||||
ValueCell.updateIfChanged(this.renderable.values.uRadius, Math.pow(2, props.radius) * camera.state.scale);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBias, props.bias);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
// from http://burtleburtle.net/bob/hash/integer.html
|
||||
@@ -89,4 +90,456 @@ export function hashFnv32a(array: ArrayLike<number>) {
|
||||
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
|
||||
}
|
||||
return hval >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 256 bit FNV-1a hash, returns 8 32-bit words
|
||||
* Based on the FNV-1a algorithm extended to 256 bits
|
||||
*/
|
||||
export function hashFnv256a(array: ArrayLike<number>, out: Uint32Array) {
|
||||
out.set(Fnv256Base);
|
||||
|
||||
for (let i = 0, il = array.length; i < il; ++i) {
|
||||
// XOR with input byte
|
||||
out[0] ^= array[i] & 0xff;
|
||||
|
||||
// Multiply by FNV prime (256-bit multiplication)
|
||||
multiplyBy256BitPrime(out);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 256-bit object hash function using FNV-1a
|
||||
*/
|
||||
export function hashFnv256o(obj: any): string {
|
||||
return _Hasher256.hash(obj);
|
||||
}
|
||||
|
||||
class ObjectHasher256 {
|
||||
private hashTarget: Uint32Array = new Uint32Array(8);
|
||||
private numberBytes = new Uint8Array(8);
|
||||
private numberView = new DataView(this.numberBytes.buffer);
|
||||
|
||||
hash(obj: any): string {
|
||||
this.hashTarget.set(Fnv256Base);
|
||||
this.hashValue(obj, 0);
|
||||
return hashFnv256aToHex(this.hashTarget);
|
||||
}
|
||||
|
||||
private hashValue(value: any, depth: number): void {
|
||||
if (depth > 50) return;
|
||||
|
||||
const type = typeof value;
|
||||
this.addByte(type.charCodeAt(0));
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
this.addString(value);
|
||||
break;
|
||||
case 'number':
|
||||
this.addNumber(value);
|
||||
break;
|
||||
case 'boolean':
|
||||
this.addByte(value ? 1 : 0);
|
||||
break;
|
||||
case 'object':
|
||||
if (value === null) {
|
||||
this.addByte(0);
|
||||
} else if (Array.isArray(value)) {
|
||||
this.addArray(value, depth);
|
||||
} else {
|
||||
this.addObject(value, depth);
|
||||
}
|
||||
break;
|
||||
case 'undefined':
|
||||
this.addByte(255);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private addByte(byte: number): void {
|
||||
// XOR with input byte
|
||||
this.hashTarget[0] ^= byte & 0xff;
|
||||
// Multiply by FNV prime (256-bit multiplication)
|
||||
multiplyBy256BitPrime(this.hashTarget);
|
||||
}
|
||||
|
||||
private addString(str: string): void {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
if (code < 128) {
|
||||
this.addByte(code);
|
||||
} else if (code < 2048) {
|
||||
this.addByte(0xc0 | (code >> 6));
|
||||
this.addByte(0x80 | (code & 0x3f));
|
||||
} else {
|
||||
this.addByte(0xe0 | (code >> 12));
|
||||
this.addByte(0x80 | ((code >> 6) & 0x3f));
|
||||
this.addByte(0x80 | (code & 0x3f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addNumber(num: number): void {
|
||||
if (Number.isNaN(num)) {
|
||||
this.addByte(0x7f); this.addByte(0xc0); this.addByte(0x00); this.addByte(0x00);
|
||||
this.addByte(0x00); this.addByte(0x00); this.addByte(0x00); this.addByte(0x00);
|
||||
} else if (!Number.isFinite(num)) {
|
||||
if (num > 0) {
|
||||
this.addByte(0x7f); this.addByte(0x80); this.addByte(0x00); this.addByte(0x00);
|
||||
} else {
|
||||
this.addByte(0xff); this.addByte(0x80); this.addByte(0x00); this.addByte(0x00);
|
||||
}
|
||||
this.addByte(0x00); this.addByte(0x00); this.addByte(0x00); this.addByte(0x00);
|
||||
} else {
|
||||
this.numberView.setFloat64(0, num, false);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
this.addByte(this.numberBytes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addArray(arr: any[], depth: number): void {
|
||||
this.addNumber(arr.length);
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
this.addNumber(i);
|
||||
this.hashValue(arr[i], depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private addObject(obj: any, depth: number): void {
|
||||
const keys = Object.keys(obj).sort();
|
||||
this.addNumber(keys.length);
|
||||
|
||||
for (const key of keys) {
|
||||
this.addString(key);
|
||||
this.hashValue(obj[key], depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _Hasher256 = new ObjectHasher256();
|
||||
|
||||
const Fnv256Base = new Uint32Array([
|
||||
0x6c62272e, 0x07bb0142, 0x62b82175, 0x6295c58d,
|
||||
0x16d67530, 0xdd7121e3, 0xb3174000, 0x00000100
|
||||
]);
|
||||
|
||||
const MultTmp1 = new Uint32Array(8);
|
||||
const MultTmp2 = new Uint32Array(8);
|
||||
|
||||
/**
|
||||
* Helper function to multiply 256-bit number by FNV prime
|
||||
*/
|
||||
function multiplyBy256BitPrime(hash: Uint32Array): void {
|
||||
// Since FNV 256-bit prime is 2^88 + 2^8 + 0x3b, we can optimize:
|
||||
// hash * prime = hash * (2^88 + 2^8 + 0x3b) = (hash << 88) + (hash << 8) + hash * 0x3b
|
||||
|
||||
// hash << 88 (shift left by 88 bits = 2 full 32-bit words + 24 bits)
|
||||
MultTmp1[0] = 0;
|
||||
MultTmp1[1] = 0;
|
||||
MultTmp1[2] = hash[0] << 24;
|
||||
MultTmp1[3] = (hash[0] >>> 8) | (hash[1] << 24);
|
||||
MultTmp1[4] = (hash[1] >>> 8) | (hash[2] << 24);
|
||||
MultTmp1[5] = (hash[2] >>> 8) | (hash[3] << 24);
|
||||
MultTmp1[6] = (hash[3] >>> 8) | (hash[4] << 24);
|
||||
MultTmp1[7] = (hash[4] >>> 8) | (hash[5] << 24);
|
||||
|
||||
// hash << 8
|
||||
MultTmp2[0] = hash[0] << 8;
|
||||
MultTmp2[1] = (hash[0] >>> 24) | (hash[1] << 8);
|
||||
MultTmp2[2] = (hash[1] >>> 24) | (hash[2] << 8);
|
||||
MultTmp2[3] = (hash[2] >>> 24) | (hash[3] << 8);
|
||||
MultTmp2[4] = (hash[3] >>> 24) | (hash[4] << 8);
|
||||
MultTmp2[5] = (hash[4] >>> 24) | (hash[5] << 8);
|
||||
MultTmp2[6] = (hash[5] >>> 24) | (hash[6] << 8);
|
||||
MultTmp2[7] = (hash[6] >>> 24) | (hash[7] << 8);
|
||||
|
||||
// hash * 0x3b (simple multiplication by small constant)
|
||||
let carry = 0;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const product = hash[i] * 0x3b + carry;
|
||||
hash[i] = product >>> 0;
|
||||
carry = Math.floor(product / 0x100000000);
|
||||
}
|
||||
|
||||
// Add all three components: (hash << 88) + (hash << 8) + hash * 0x3b
|
||||
carry = 0;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const sum = hash[i] + MultTmp1[i] + MultTmp2[i] + carry;
|
||||
hash[i] = sum >>> 0;
|
||||
carry = sum >= 0x100000000 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
const _8digit_padding = [
|
||||
'00000000',
|
||||
'0000000',
|
||||
'000000',
|
||||
'00000',
|
||||
'0000',
|
||||
'000',
|
||||
'00',
|
||||
'0'
|
||||
];
|
||||
|
||||
|
||||
function padHexNumber(num: number): string {
|
||||
const base = num.toString(16);
|
||||
if (base.length >= 8) return base; // No padding needed
|
||||
return _8digit_padding[base.length] + base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert 256-bit hash to hex string
|
||||
*/
|
||||
function hashFnv256aToHex(hash: Uint32Array): string {
|
||||
let result = '';
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
result += padHexNumber(hash[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 32-bit Murmur hash
|
||||
*/
|
||||
export function hashMurmur32o(obj: any, seed: number = 42): number {
|
||||
const jsonString = JSON.stringify(obj);
|
||||
return murmurHash3_32(jsonString, seed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 128-bit Murmur hash
|
||||
*/
|
||||
export function hashMurmur128o(obj: any, seed: number = 42): string {
|
||||
const jsonString = JSON.stringify(obj);
|
||||
return murmurHash3_128(jsonString, seed);
|
||||
}
|
||||
|
||||
/**
|
||||
* MurmurHash3 32-bit implementation
|
||||
* @param key - The input string to hash
|
||||
* @param seed - The seed value (default: 0)
|
||||
* @returns The 32-bit hash as a number
|
||||
*/
|
||||
export function murmurHash3_32(key: string, seed: number): number {
|
||||
let h = seed >>> 0;
|
||||
const remainder = key.length % 4;
|
||||
const bytes = key.length - remainder;
|
||||
|
||||
for (let i = 0; i < bytes; i += 4) {
|
||||
let k = (key.charCodeAt(i) & 0xff) |
|
||||
((key.charCodeAt(i + 1) & 0xff) << 8) |
|
||||
((key.charCodeAt(i + 2) & 0xff) << 16) |
|
||||
((key.charCodeAt(i + 3) & 0xff) << 24);
|
||||
|
||||
k = Math.imul(k, 0xcc9e2d51);
|
||||
k = (k << 15) | (k >>> 17);
|
||||
k = Math.imul(k, 0x1b873593);
|
||||
|
||||
h ^= k;
|
||||
h = (h << 13) | (h >>> 19);
|
||||
h = Math.imul(h, 5) + 0xe6546b64;
|
||||
}
|
||||
|
||||
let k = 0;
|
||||
switch (remainder) {
|
||||
case 3: k ^= (key.charCodeAt(bytes + 2) & 0xff) << 16;
|
||||
case 2: k ^= (key.charCodeAt(bytes + 1) & 0xff) << 8;
|
||||
case 1: k ^= (key.charCodeAt(bytes) & 0xff);
|
||||
k = Math.imul(k, 0xcc9e2d51);
|
||||
k = (k << 15) | (k >>> 17);
|
||||
k = Math.imul(k, 0x1b873593);
|
||||
h ^= k;
|
||||
}
|
||||
|
||||
h ^= key.length;
|
||||
h ^= h >>> 16;
|
||||
h = Math.imul(h, 0x85ebca6b);
|
||||
h ^= h >>> 13;
|
||||
h = Math.imul(h, 0xc2b2ae35);
|
||||
h ^= h >>> 16;
|
||||
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* MurmurHash3 128-bit implementation
|
||||
* @param key - The input data to hash
|
||||
* @param seed - The seed value (default: 0)
|
||||
* @returns The 128-bit hash as a hexadecimal string
|
||||
*/
|
||||
export function murmurHash3_128_fromBytes(key: Uint8Array, seed: number): string {
|
||||
// This fakeString approach is much faster than `new TextDecoder('ascii').decode(key)`
|
||||
const fakeString = {
|
||||
length: key.length,
|
||||
charCodeAt(i: number) { return key[i]; },
|
||||
};
|
||||
return murmurHash3_128(fakeString as string, seed);
|
||||
}
|
||||
|
||||
/**
|
||||
* MurmurHash3 128-bit implementation
|
||||
* @param key - The input string to hash
|
||||
* @param seed - The seed value (default: 0)
|
||||
* @returns The 128-bit hash as a hexadecimal string
|
||||
*/
|
||||
export function murmurHash3_128(key: string, seed: number): string {
|
||||
let h1 = seed >>> 0;
|
||||
let h2 = seed >>> 0;
|
||||
let h3 = seed >>> 0;
|
||||
let h4 = seed >>> 0;
|
||||
|
||||
const remainder = key.length % 16;
|
||||
const bytes = key.length - remainder;
|
||||
|
||||
for (let i = 0; i < bytes; i += 16) {
|
||||
let k1 = (key.charCodeAt(i) & 0xff) |
|
||||
((key.charCodeAt(i + 1) & 0xff) << 8) |
|
||||
((key.charCodeAt(i + 2) & 0xff) << 16) |
|
||||
((key.charCodeAt(i + 3) & 0xff) << 24);
|
||||
|
||||
let k2 = (key.charCodeAt(i + 4) & 0xff) |
|
||||
((key.charCodeAt(i + 5) & 0xff) << 8) |
|
||||
((key.charCodeAt(i + 6) & 0xff) << 16) |
|
||||
((key.charCodeAt(i + 7) & 0xff) << 24);
|
||||
|
||||
let k3 = (key.charCodeAt(i + 8) & 0xff) |
|
||||
((key.charCodeAt(i + 9) & 0xff) << 8) |
|
||||
((key.charCodeAt(i + 10) & 0xff) << 16) |
|
||||
((key.charCodeAt(i + 11) & 0xff) << 24);
|
||||
|
||||
let k4 = (key.charCodeAt(i + 12) & 0xff) |
|
||||
((key.charCodeAt(i + 13) & 0xff) << 8) |
|
||||
((key.charCodeAt(i + 14) & 0xff) << 16) |
|
||||
((key.charCodeAt(i + 15) & 0xff) << 24);
|
||||
|
||||
k1 = Math.imul(k1, 0x239b961b);
|
||||
k1 = (k1 << 15) | (k1 >>> 17);
|
||||
k1 = Math.imul(k1, 0xab0e9789);
|
||||
h1 ^= k1;
|
||||
|
||||
h1 = (h1 << 19) | (h1 >>> 13);
|
||||
h1 += h2;
|
||||
h1 = Math.imul(h1, 5) + 0x561ccd1b;
|
||||
|
||||
k2 = Math.imul(k2, 0xab0e9789);
|
||||
k2 = (k2 << 16) | (k2 >>> 16);
|
||||
k2 = Math.imul(k2, 0x38b34ae5);
|
||||
h2 ^= k2;
|
||||
|
||||
h2 = (h2 << 17) | (h2 >>> 15);
|
||||
h2 += h3;
|
||||
h2 = Math.imul(h2, 5) + 0x0bcaa747;
|
||||
|
||||
k3 = Math.imul(k3, 0x38b34ae5);
|
||||
k3 = (k3 << 17) | (k3 >>> 15);
|
||||
k3 = Math.imul(k3, 0xa1e38b93);
|
||||
h3 ^= k3;
|
||||
|
||||
h3 = (h3 << 15) | (h3 >>> 17);
|
||||
h3 += h4;
|
||||
h3 = Math.imul(h3, 5) + 0x96cd1c35;
|
||||
|
||||
k4 = Math.imul(k4, 0xa1e38b93);
|
||||
k4 = (k4 << 13) | (k4 >>> 19);
|
||||
k4 = Math.imul(k4, 0x239b961b);
|
||||
h4 ^= k4;
|
||||
|
||||
h4 = (h4 << 13) | (h4 >>> 19);
|
||||
h4 += h1;
|
||||
h4 = Math.imul(h4, 5) + 0x32ac3b17;
|
||||
}
|
||||
|
||||
let k1 = 0, k2 = 0, k3 = 0, k4 = 0;
|
||||
|
||||
switch (remainder) {
|
||||
case 15: k4 ^= key.charCodeAt(bytes + 14) << 16;
|
||||
case 14: k4 ^= key.charCodeAt(bytes + 13) << 8;
|
||||
case 13: k4 ^= key.charCodeAt(bytes + 12);
|
||||
k4 = Math.imul(k4, 0xa1e38b93);
|
||||
k4 = (k4 << 13) | (k4 >>> 19);
|
||||
k4 = Math.imul(k4, 0x239b961b);
|
||||
h4 ^= k4;
|
||||
|
||||
case 12: k3 ^= key.charCodeAt(bytes + 11) << 24;
|
||||
case 11: k3 ^= key.charCodeAt(bytes + 10) << 16;
|
||||
case 10: k3 ^= key.charCodeAt(bytes + 9) << 8;
|
||||
case 9: k3 ^= key.charCodeAt(bytes + 8);
|
||||
k3 = Math.imul(k3, 0x38b34ae5);
|
||||
k3 = (k3 << 17) | (k3 >>> 15);
|
||||
k3 = Math.imul(k3, 0xa1e38b93);
|
||||
h3 ^= k3;
|
||||
|
||||
case 8: k2 ^= key.charCodeAt(bytes + 7) << 24;
|
||||
case 7: k2 ^= key.charCodeAt(bytes + 6) << 16;
|
||||
case 6: k2 ^= key.charCodeAt(bytes + 5) << 8;
|
||||
case 5: k2 ^= key.charCodeAt(bytes + 4);
|
||||
k2 = Math.imul(k2, 0xab0e9789);
|
||||
k2 = (k2 << 16) | (k2 >>> 16);
|
||||
k2 = Math.imul(k2, 0x38b34ae5);
|
||||
h2 ^= k2;
|
||||
|
||||
case 4: k1 ^= key.charCodeAt(bytes + 3) << 24;
|
||||
case 3: k1 ^= key.charCodeAt(bytes + 2) << 16;
|
||||
case 2: k1 ^= key.charCodeAt(bytes + 1) << 8;
|
||||
case 1: k1 ^= key.charCodeAt(bytes);
|
||||
k1 = Math.imul(k1, 0x239b961b);
|
||||
k1 = (k1 << 15) | (k1 >>> 17);
|
||||
k1 = Math.imul(k1, 0xab0e9789);
|
||||
h1 ^= k1;
|
||||
}
|
||||
|
||||
h1 ^= key.length;
|
||||
h2 ^= key.length;
|
||||
h3 ^= key.length;
|
||||
h4 ^= key.length;
|
||||
|
||||
h1 += h2;
|
||||
h1 += h3;
|
||||
h1 += h4;
|
||||
h2 += h1;
|
||||
h3 += h1;
|
||||
h4 += h1;
|
||||
|
||||
h1 ^= h1 >>> 16;
|
||||
h1 = Math.imul(h1, 0x85ebca6b);
|
||||
h1 ^= h1 >>> 13;
|
||||
h1 = Math.imul(h1, 0xc2b2ae35);
|
||||
h1 ^= h1 >>> 16;
|
||||
|
||||
h2 ^= h2 >>> 16;
|
||||
h2 = Math.imul(h2, 0x85ebca6b);
|
||||
h2 ^= h2 >>> 13;
|
||||
h2 = Math.imul(h2, 0xc2b2ae35);
|
||||
h2 ^= h2 >>> 16;
|
||||
|
||||
h3 ^= h3 >>> 16;
|
||||
h3 = Math.imul(h3, 0x85ebca6b);
|
||||
h3 ^= h3 >>> 13;
|
||||
h3 = Math.imul(h3, 0xc2b2ae35);
|
||||
h3 ^= h3 >>> 16;
|
||||
|
||||
h4 ^= h4 >>> 16;
|
||||
h4 = Math.imul(h4, 0x85ebca6b);
|
||||
h4 ^= h4 >>> 13;
|
||||
h4 = Math.imul(h4, 0xc2b2ae35);
|
||||
h4 ^= h4 >>> 16;
|
||||
|
||||
h1 += h2;
|
||||
h1 += h3;
|
||||
h1 += h4;
|
||||
h2 += h1;
|
||||
h3 += h1;
|
||||
h4 += h1;
|
||||
|
||||
return (
|
||||
(h1 >>> 0).toString(16).padStart(8, '0') +
|
||||
(h2 >>> 0).toString(16).padStart(8, '0') +
|
||||
(h3 >>> 0).toString(16).padStart(8, '0') +
|
||||
(h4 >>> 0).toString(16).padStart(8, '0')
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -12,6 +12,7 @@ import { Cylinder, CylinderProps, DefaultCylinderProps } from '../../../primitiv
|
||||
import { Prism } from '../../../primitive/prism';
|
||||
import { polygon } from '../../../primitive/polygon';
|
||||
import { hashFnv32a } from '../../../../mol-data/util';
|
||||
import { Ray3D } from '../../../../mol-math/geometry/primitives/ray3d';
|
||||
|
||||
const cylinderMap = new Map<number, Primitive>();
|
||||
const up = Vec3.create(0, 1, 0);
|
||||
@@ -77,6 +78,11 @@ export function addSimpleCylinder(state: MeshBuilder.State, start: Vec3, end: Ve
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props));
|
||||
}
|
||||
|
||||
export function addCylinderFromRay3D(state: MeshBuilder.State, ray: Ray3D, length: number, props: BasicCylinderProps) {
|
||||
setCylinderMat(tmpCylinderMat, ray.origin, ray.direction, length, false);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props));
|
||||
}
|
||||
|
||||
export function addCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, props: BasicCylinderProps) {
|
||||
const d = Vec3.distance(start, end) * lengthScale;
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -22,10 +22,4 @@ export namespace Object3D {
|
||||
up: Vec3.create(0, 1, 0),
|
||||
};
|
||||
}
|
||||
|
||||
const center = Vec3.zero();
|
||||
export function update(object3d: Object3D) {
|
||||
Vec3.add(center, object3d.position, object3d.direction);
|
||||
Mat4.lookAt(object3d.view, object3d.position, center, object3d.up);
|
||||
}
|
||||
}
|
||||
@@ -130,11 +130,14 @@ export const GlobalUniformSchema = {
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uModelViewProjection: UniformSpec('m4'),
|
||||
uInvModelViewProjection: UniformSpec('m4'),
|
||||
uHasHeadRotation: UniformSpec('b'),
|
||||
uInvHeadRotation: UniformSpec('m4'),
|
||||
|
||||
uIsOrtho: UniformSpec('f'),
|
||||
uPixelRatio: UniformSpec('f'),
|
||||
uViewport: UniformSpec('v4'),
|
||||
uViewOffset: UniformSpec('v2'),
|
||||
uModelScale: UniformSpec('f'),
|
||||
uDrawingBufferSize: UniformSpec('v2'),
|
||||
|
||||
uCameraPosition: UniformSpec('v3'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -50,6 +50,7 @@ const DefaultPrintImageOptions = {
|
||||
id: 'molstar.debug.image',
|
||||
normalize: false,
|
||||
useCanvas: false,
|
||||
flipY: false,
|
||||
};
|
||||
export type PrintImageOptions = typeof DefaultPrintImageOptions
|
||||
|
||||
@@ -101,6 +102,7 @@ export function printImageData(imageData: ImageData, options: Partial<PrintImage
|
||||
tmpContainer.style.right = '0px';
|
||||
tmpContainer.style.border = 'solid orange';
|
||||
tmpContainer.style.pointerEvents = 'none';
|
||||
if (options.flipY) tmpContainer.style.transform = 'scaleY(-1)';
|
||||
document.body.appendChild(tmpContainer);
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,16 @@ function getLight(props: RendererProps['light'], light?: Light): Light {
|
||||
return { count, direction, color };
|
||||
}
|
||||
|
||||
export function getTransformedLightDirection(light: Light, t: Mat4): Light['direction'] {
|
||||
const tld = new Array(light.count * 3);
|
||||
for (let i = 0, il = light.count; i < il; ++i) {
|
||||
Vec3.fromArray(tmpDir, light.direction, i * 3);
|
||||
Vec3.transformDirection(tmpDir, tmpDir, t);
|
||||
Vec3.toArray(tmpDir, tld, i * 3);
|
||||
}
|
||||
return tld;
|
||||
}
|
||||
|
||||
namespace Renderer {
|
||||
const enum Flag {
|
||||
None = 0,
|
||||
@@ -184,6 +194,7 @@ namespace Renderer {
|
||||
['tDepth', emptyDepthTexture]
|
||||
];
|
||||
|
||||
const model = Mat4();
|
||||
const view = Mat4();
|
||||
const invView = Mat4();
|
||||
const modelView = Mat4();
|
||||
@@ -191,6 +202,7 @@ namespace Renderer {
|
||||
const invProjection = Mat4();
|
||||
const modelViewProjection = Mat4();
|
||||
const invModelViewProjection = Mat4();
|
||||
const invHeadRotation = Mat4();
|
||||
|
||||
const cameraDir = Vec3();
|
||||
const cameraPosition = Vec3();
|
||||
@@ -198,6 +210,9 @@ namespace Renderer {
|
||||
const viewOffset = Vec2();
|
||||
const frustum = Frustum3D();
|
||||
|
||||
let modelScale = 1;
|
||||
const boundingSphere = Sphere3D();
|
||||
|
||||
const ambientColor = Vec3();
|
||||
Vec3.scale(ambientColor, Color.toArrayNormalized(p.ambientColor, ambientColor, 0), p.ambientIntensity);
|
||||
|
||||
@@ -213,9 +228,12 @@ namespace Renderer {
|
||||
uProjection: ValueCell.create(Mat4()),
|
||||
uModelViewProjection: ValueCell.create(modelViewProjection),
|
||||
uInvModelViewProjection: ValueCell.create(invModelViewProjection),
|
||||
uHasHeadRotation: ValueCell.create(false),
|
||||
uInvHeadRotation: ValueCell.create(invHeadRotation),
|
||||
|
||||
uIsOrtho: ValueCell.create(1),
|
||||
uViewOffset: ValueCell.create(viewOffset),
|
||||
uModelScale: ValueCell.create(1),
|
||||
|
||||
uPixelRatio: ValueCell.create(ctx.pixelRatio),
|
||||
uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
|
||||
@@ -274,28 +292,33 @@ namespace Renderer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Frustum3D.intersectsSphere3D(frustum, r.values.boundingSphere.ref.value)) {
|
||||
Sphere3D.scaleNX(boundingSphere, r.values.boundingSphere.ref.value, modelScale);
|
||||
|
||||
if (!Frustum3D.intersectsSphere3D(frustum, boundingSphere)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [minDistance, maxDistance] = r.values.uLod.ref.value;
|
||||
if (minDistance !== 0 || maxDistance !== 0) {
|
||||
const { center, radius } = r.values.boundingSphere.ref.value;
|
||||
const { center, radius } = boundingSphere;
|
||||
const d = Plane3D.distanceToPoint(cameraPlane, center);
|
||||
if (d + radius < minDistance) return;
|
||||
if (d - radius > maxDistance) return;
|
||||
if (d + radius < minDistance * modelScale) return;
|
||||
if (d - radius > maxDistance * modelScale) return;
|
||||
}
|
||||
|
||||
if (isOccluded !== null && isOccluded(r.values.boundingSphere.ref.value)) {
|
||||
return;
|
||||
}
|
||||
const unscaled = modelScale === 1;
|
||||
if (unscaled) {
|
||||
if (isOccluded !== null && isOccluded(boundingSphere)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasInstanceGrid = r.values.instanceGrid.ref.value.cellSize > 0;
|
||||
const hasMultipleInstances = r.values.uInstanceCount.ref.value > 1;
|
||||
if (hasInstanceGrid && (hasMultipleInstances || r.values.lodLevels)) {
|
||||
r.cull(cameraPlane, frustum, isOccluded, ctx.stats);
|
||||
} else {
|
||||
r.uncull();
|
||||
const hasInstanceGrid = r.values.instanceGrid.ref.value.cellSize > 0;
|
||||
const hasMultipleInstances = r.values.uInstanceCount.ref.value > 1;
|
||||
if (hasInstanceGrid && (hasMultipleInstances || r.values.lodLevels)) {
|
||||
r.cull(cameraPlane, frustum, isOccluded, ctx.stats);
|
||||
} else {
|
||||
r.uncull();
|
||||
}
|
||||
}
|
||||
|
||||
let needUpdate = false;
|
||||
@@ -382,9 +405,12 @@ namespace Renderer {
|
||||
|
||||
ValueCell.updateIfChanged(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0);
|
||||
ValueCell.update(globalUniforms.uViewOffset, camera.viewOffset.enabled ? Vec2.set(viewOffset, camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2.set(viewOffset, 0, 0));
|
||||
ValueCell.updateIfChanged(globalUniforms.uModelScale, camera.state.scale);
|
||||
|
||||
ValueCell.update(globalUniforms.uCameraPosition, Vec3.copy(cameraPosition, camera.state.position));
|
||||
ValueCell.update(globalUniforms.uCameraDir, Vec3.normalize(cameraDir, Vec3.sub(cameraDir, camera.state.target, camera.state.position)));
|
||||
ValueCell.update(globalUniforms.uCameraPosition, Mat4.getTranslation(cameraPosition, invView));
|
||||
const cameraTarget = Vec3.scale(Vec3(), camera.state.target, camera.state.scale);
|
||||
Vec3.normalize(cameraDir, Vec3.sub(cameraDir, cameraTarget, cameraPosition));
|
||||
ValueCell.update(globalUniforms.uCameraDir, cameraDir);
|
||||
|
||||
ValueCell.updateIfChanged(globalUniforms.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(globalUniforms.uNear, camera.near);
|
||||
@@ -400,13 +426,26 @@ namespace Renderer {
|
||||
ValueCell.update(globalUniforms.uCameraPlane, Plane3D.toArray(cameraPlane, globalUniforms.uCameraPlane.ref.value, 0));
|
||||
|
||||
ValueCell.updateIfChanged(globalUniforms.uMarkerAverage, scene.markerAverage);
|
||||
|
||||
const hasHeadRotation = !Mat4.isZero(camera.headRotation);
|
||||
if (hasHeadRotation) {
|
||||
ValueCell.updateIfChanged(globalUniforms.uHasHeadRotation, hasHeadRotation);
|
||||
ValueCell.update(globalUniforms.uInvHeadRotation, Mat4.invert(invHeadRotation, camera.headRotation));
|
||||
ValueCell.update(globalUniforms.uLightDirection, getTransformedLightDirection(light, invHeadRotation));
|
||||
} else {
|
||||
ValueCell.update(globalUniforms.uHasHeadRotation, false);
|
||||
ValueCell.update(globalUniforms.uInvHeadRotation, Mat4.id);
|
||||
ValueCell.update(globalUniforms.uLightDirection, light.direction);
|
||||
}
|
||||
};
|
||||
|
||||
const updateInternal = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, renderMask: Mask, markingDepthTest: boolean) => {
|
||||
arrayMapUpsert(sharedTexturesList, 'tDepth', depthTexture || emptyDepthTexture);
|
||||
|
||||
ValueCell.update(globalUniforms.uModel, group.view);
|
||||
ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, camera.view, group.view));
|
||||
modelScale = camera.state.scale;
|
||||
|
||||
ValueCell.update(globalUniforms.uModel, Mat4.scaleUniformly(model, group.view, camera.state.scale));
|
||||
ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, camera.view, model));
|
||||
ValueCell.update(globalUniforms.uInvModelView, Mat4.invert(invModelView, modelView));
|
||||
ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection));
|
||||
ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection));
|
||||
|
||||
@@ -312,7 +312,6 @@ namespace Scene {
|
||||
}
|
||||
},
|
||||
update(objects, keepBoundingSphere) {
|
||||
Object3D.update(object3d);
|
||||
if (objects) {
|
||||
for (let i = 0, il = objects.length; i < il; ++i) {
|
||||
renderableMap.get(objects[i])?.update();
|
||||
|
||||
@@ -68,7 +68,6 @@ import { common } from './shader/chunks/common.glsl';
|
||||
import { fade_lod } from './shader/chunks/fade-lod.glsl';
|
||||
import { float_to_rgba } from './shader/chunks/float-to-rgba.glsl';
|
||||
import { light_frag_params } from './shader/chunks/light-frag-params.glsl';
|
||||
import { matrix_scale } from './shader/chunks/matrix-scale.glsl';
|
||||
import { normal_frag_params } from './shader/chunks/normal-frag-params.glsl';
|
||||
import { read_from_texture } from './shader/chunks/read-from-texture.glsl';
|
||||
import { rgba_to_float } from './shader/chunks/rgba-to-float.glsl';
|
||||
@@ -104,7 +103,6 @@ const ShaderChunks: { [k: string]: string } = {
|
||||
fade_lod,
|
||||
float_to_rgba,
|
||||
light_frag_params,
|
||||
matrix_scale,
|
||||
normal_frag_params,
|
||||
read_from_texture,
|
||||
rgba_to_float,
|
||||
|
||||
@@ -36,7 +36,7 @@ export const assign_color_varying = `
|
||||
vec3 cgridPos = (uColorGridTransform.w * (position - uColorGridTransform.xyz)) / uColorGridDim;
|
||||
vColor.rgb = texture3dFrom2dLinear(tColorGrid, cgridPos, uColorGridDim, uColorTexDim).rgb;
|
||||
#elif defined(dColorType_volumeInstance)
|
||||
vec3 cgridPos = (uColorGridTransform.w * (vModelPosition - uColorGridTransform.xyz)) / uColorGridDim;
|
||||
vec3 cgridPos = (uColorGridTransform.w * (vModelPosition / uModelScale - uColorGridTransform.xyz)) / uColorGridDim;
|
||||
vColor.rgb = texture3dFrom2dLinear(tColorGrid, cgridPos, uColorGridDim, uColorTexDim).rgb;
|
||||
#endif
|
||||
|
||||
@@ -52,7 +52,7 @@ export const assign_color_varying = `
|
||||
#elif defined(dOverpaintType_vertexInstance)
|
||||
vOverpaint = readFromTexture(tOverpaint, int(aInstance) * uVertexCount + vertexId, uOverpaintTexDim);
|
||||
#elif defined(dOverpaintType_volumeInstance)
|
||||
vec3 ogridPos = (uOverpaintGridTransform.w * (vModelPosition - uOverpaintGridTransform.xyz)) / uOverpaintGridDim;
|
||||
vec3 ogridPos = (uOverpaintGridTransform.w * (vModelPosition / uModelScale - uOverpaintGridTransform.xyz)) / uOverpaintGridDim;
|
||||
vOverpaint = texture3dFrom2dLinear(tOverpaintGrid, ogridPos, uOverpaintGridDim, uOverpaintTexDim);
|
||||
#endif
|
||||
|
||||
@@ -73,7 +73,7 @@ export const assign_color_varying = `
|
||||
#elif defined(dEmissiveType_vertexInstance)
|
||||
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + vertexId, uEmissiveTexDim).a;
|
||||
#elif defined(dEmissiveType_volumeInstance)
|
||||
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
|
||||
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition / uModelScale - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
|
||||
vEmissive = texture3dFrom2dLinear(tEmissiveGrid, egridPos, uEmissiveGridDim, uEmissiveTexDim).a;
|
||||
#endif
|
||||
vEmissive *= uEmissiveStrength;
|
||||
@@ -87,7 +87,7 @@ export const assign_color_varying = `
|
||||
#elif defined(dSubstanceType_vertexInstance)
|
||||
vSubstance = readFromTexture(tSubstance, int(aInstance) * uVertexCount + vertexId, uSubstanceTexDim);
|
||||
#elif defined(dSubstanceType_volumeInstance)
|
||||
vec3 sgridPos = (uSubstanceGridTransform.w * (vModelPosition - uSubstanceGridTransform.xyz)) / uSubstanceGridDim;
|
||||
vec3 sgridPos = (uSubstanceGridTransform.w * (vModelPosition / uModelScale - uSubstanceGridTransform.xyz)) / uSubstanceGridDim;
|
||||
vSubstance = texture3dFrom2dLinear(tSubstanceGrid, sgridPos, uSubstanceGridDim, uSubstanceTexDim);
|
||||
#endif
|
||||
|
||||
@@ -104,7 +104,7 @@ export const assign_color_varying = `
|
||||
#elif defined(dEmissiveType_vertexInstance)
|
||||
vEmissive = readFromTexture(tEmissive, int(aInstance) * uVertexCount + vertexId, uEmissiveTexDim).a;
|
||||
#elif defined(dEmissiveType_volumeInstance)
|
||||
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
|
||||
vec3 egridPos = (uEmissiveGridTransform.w * (vModelPosition / uModelScale - uEmissiveGridTransform.xyz)) / uEmissiveGridDim;
|
||||
vEmissive = texture3dFrom2dLinear(tEmissiveGrid, egridPos, uEmissiveGridDim, uEmissiveTexDim).a;
|
||||
#endif
|
||||
vEmissive *= uEmissiveStrength;
|
||||
@@ -133,7 +133,7 @@ export const assign_color_varying = `
|
||||
#elif defined(dTransparencyType_vertexInstance)
|
||||
vTransparency = readFromTexture(tTransparency, int(aInstance) * uVertexCount + vertexId, uTransparencyTexDim).a;
|
||||
#elif defined(dTransparencyType_volumeInstance)
|
||||
vec3 tgridPos = (uTransparencyGridTransform.w * (vModelPosition - uTransparencyGridTransform.xyz)) / uTransparencyGridDim;
|
||||
vec3 tgridPos = (uTransparencyGridTransform.w * (vModelPosition / uModelScale - uTransparencyGridTransform.xyz)) / uTransparencyGridDim;
|
||||
vTransparency = texture3dFrom2dLinear(tTransparencyGrid, tgridPos, uTransparencyGridDim, uTransparencyTexDim).a;
|
||||
#endif
|
||||
vTransparency *= uTransparencyStrength;
|
||||
|
||||
@@ -56,6 +56,7 @@ varying vec3 vModelPosition;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
uniform vec2 uViewOffset;
|
||||
uniform float uModelScale;
|
||||
|
||||
uniform float uNear;
|
||||
uniform float uFar;
|
||||
|
||||
@@ -46,6 +46,8 @@ uniform int uPickType;
|
||||
varying vec3 vModelPosition;
|
||||
varying vec3 vViewPosition;
|
||||
|
||||
uniform float uModelScale;
|
||||
|
||||
#if defined(noNonInstancedActiveAttribs)
|
||||
// int() is needed for some Safari versions
|
||||
// see https://bugs.webkit.org/show_bug.cgi?id=244152
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const matrix_scale = `
|
||||
float matrixScale(in mat4 m){
|
||||
vec4 r = m[0];
|
||||
return sqrt(r[0] * r[0] + r[1] * r[1] + r[2] * r[2]);
|
||||
}
|
||||
`;
|
||||
@@ -48,10 +48,10 @@ void main() {
|
||||
|
||||
mat4 modelTransform = uModel * aTransform;
|
||||
|
||||
vTransform = aTransform;
|
||||
vTransform = modelTransform;
|
||||
vStart = (modelTransform * vec4(aStart, 1.0)).xyz;
|
||||
vEnd = (modelTransform * vec4(aEnd, 1.0)).xyz;
|
||||
vSize = size * aScale;
|
||||
vSize = size * aScale * uModelScale;
|
||||
vCap = aCap;
|
||||
|
||||
vModelPosition = (vStart + vEnd) * 0.5;
|
||||
|
||||
@@ -29,11 +29,12 @@ precision highp int;
|
||||
|
||||
uniform mat4 uProjection, uTransform, uModelView, uModel, uView;
|
||||
uniform vec3 uCameraDir;
|
||||
uniform float uModelScale;
|
||||
|
||||
uniform sampler2D tDepth;
|
||||
uniform vec2 uDrawingBufferSize;
|
||||
|
||||
varying vec3 vOrigPos;
|
||||
varying vec3 vModelPosition;
|
||||
varying float vInstance;
|
||||
varying vec4 vBoundingSphere;
|
||||
varying mat4 vTransform;
|
||||
@@ -212,7 +213,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
|
||||
vec3 distVec = startLoc - pos;
|
||||
if (dot(distVec, distVec) > maxDistSq) break;
|
||||
|
||||
unitPos = v3m4(pos, cartnToUnit);
|
||||
unitPos = v3m4(pos / uModelScale, cartnToUnit);
|
||||
|
||||
// continue when outside of grid
|
||||
if (unitPos.x > posMax.x || unitPos.y > posMax.y || unitPos.z > posMax.z ||
|
||||
@@ -228,7 +229,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
|
||||
|
||||
if (uJumpLength > 0.0 && value < 0.01) {
|
||||
nextPos = pos + rayDir * uJumpLength;
|
||||
nextValue = textureVal(v3m4(nextPos, cartnToUnit)).a;
|
||||
nextValue = textureVal(v3m4(nextPos / uModelScale, cartnToUnit)).a;
|
||||
if (nextValue < 0.01) {
|
||||
prevValue = nextValue;
|
||||
pos = nextPos;
|
||||
@@ -361,15 +362,15 @@ void main() {
|
||||
if (gl_FrontFacing)
|
||||
discard;
|
||||
|
||||
vec3 rayDir = mix(normalize(vOrigPos - uCameraPosition), uCameraDir, uIsOrtho);
|
||||
vec3 step = rayDir * uStepScale;
|
||||
vec3 rayDir = mix(normalize(vModelPosition - uCameraPosition), uCameraDir, uIsOrtho);
|
||||
vec3 step = rayDir * uStepScale * uModelScale;
|
||||
|
||||
float boundingSphereNear = distance(vBoundingSphere.xyz, uCameraPosition) - vBoundingSphere.w;
|
||||
float d = max(uNear, boundingSphereNear) - mix(0.0, distance(vOrigPos, uCameraPosition), uIsOrtho);
|
||||
vec3 start = mix(uCameraPosition, vOrigPos, uIsOrtho) + (d * rayDir);
|
||||
float d = max(uNear, boundingSphereNear) - mix(0.0, distance(vModelPosition, uCameraPosition), uIsOrtho);
|
||||
vec3 start = mix(uCameraPosition, vModelPosition, uIsOrtho) + (d * rayDir);
|
||||
gl_FragColor = raymarch(start, step, rayDir);
|
||||
|
||||
float fragmentDepth = calcDepth((uModelView * vec4(start, 1.0)).xyz);
|
||||
float fragmentDepth = calcDepth((uView * vec4(start, 1.0)).xyz);
|
||||
float preFogAlpha = clamp(preFogAlphaBlended, 0.0, 1.0);
|
||||
#include wboit_write
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Michael Krone <michael.krone@uni-tuebingen.de>
|
||||
@@ -12,11 +12,13 @@ attribute vec3 aPosition;
|
||||
attribute mat4 aTransform;
|
||||
attribute float aInstance;
|
||||
|
||||
uniform mat4 uModel;
|
||||
uniform mat4 uModelView;
|
||||
uniform mat4 uProjection;
|
||||
uniform vec4 uInvariantBoundingSphere;
|
||||
uniform float uModelScale;
|
||||
|
||||
varying vec3 vOrigPos;
|
||||
varying vec3 vModelPosition;
|
||||
varying float vInstance;
|
||||
varying vec4 vBoundingSphere;
|
||||
varying mat4 vTransform;
|
||||
@@ -33,11 +35,11 @@ void main() {
|
||||
vec4 unitCoord = vec4(aPosition + vec3(0.5), 1.0);
|
||||
vec4 mvPosition = uModelView * aTransform * uUnitToCartn * unitCoord;
|
||||
|
||||
vOrigPos = (aTransform * uUnitToCartn * unitCoord).xyz;
|
||||
vModelPosition = (uModel * aTransform * uUnitToCartn * unitCoord).xyz;
|
||||
vInstance = aInstance;
|
||||
vBoundingSphere = vec4(
|
||||
(aTransform * vec4(uInvariantBoundingSphere.xyz, 1.0)).xyz,
|
||||
uInvariantBoundingSphere.w
|
||||
(uModel * aTransform * vec4(uInvariantBoundingSphere.xyz, 1.0)).xyz,
|
||||
uModelScale * uInvariantBoundingSphere.w
|
||||
);
|
||||
vTransform = aTransform;
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ void main(void){
|
||||
vec3 vViewPosition = -vPointViewPosition;
|
||||
fragmentDepth = gl_FragCoord.z;
|
||||
#if !defined(dIgnoreLight) || defined(dXrayShaded) || defined(dRenderVariant_tracing)
|
||||
pointDir.z -= cos(length(pointDir));
|
||||
pointDir.z -= cos(length(pointDir)) * vRadius * 0.5;
|
||||
cameraNormal = -normalize(pointDir);
|
||||
#endif
|
||||
interior = false;
|
||||
|
||||
@@ -18,6 +18,8 @@ precision highp int;
|
||||
uniform mat4 uModelView;
|
||||
uniform mat4 uInvProjection;
|
||||
uniform float uIsOrtho;
|
||||
uniform bool uHasHeadRotation;
|
||||
uniform mat4 uInvHeadRotation;
|
||||
|
||||
uniform vec2 uTexDim;
|
||||
uniform sampler2D tPositionGroup;
|
||||
@@ -29,8 +31,6 @@ varying float vRadius;
|
||||
varying vec3 vPoint;
|
||||
varying vec3 vPointViewPosition;
|
||||
|
||||
#include matrix_scale
|
||||
|
||||
/**
|
||||
* Bounding rectangle of a clipped, perspective-projected 3D Sphere.
|
||||
* Michael Mara, Morgan McGuire. 2013
|
||||
@@ -81,7 +81,7 @@ void main(void){
|
||||
#include assign_clipping_varying
|
||||
#include assign_size
|
||||
|
||||
vRadius = size * matrixScale(uModelView);
|
||||
vRadius = size * uModelScale;
|
||||
|
||||
vec4 position4 = vec4(position, 1.0);
|
||||
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
|
||||
@@ -107,6 +107,10 @@ void main(void){
|
||||
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
|
||||
mvCorner.xy += mapping * vRadius;
|
||||
gl_Position = uProjection * mvCorner;
|
||||
} else if (uHasHeadRotation) {
|
||||
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
|
||||
mvCorner.xy += mapping * vRadius * 1.4;
|
||||
gl_Position = uProjection * mvCorner;
|
||||
} else {
|
||||
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
|
||||
sphereProjection(mvPosition.xyz, vRadius, mapping);
|
||||
|
||||
@@ -32,11 +32,11 @@ uniform float uOffsetZ;
|
||||
uniform float uIsOrtho;
|
||||
uniform float uPixelRatio;
|
||||
uniform vec4 uViewport;
|
||||
uniform mat4 uInvHeadRotation;
|
||||
uniform bool uHasHeadRotation;
|
||||
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
#include matrix_scale
|
||||
|
||||
void main(void){
|
||||
int vertexId = VertexID;
|
||||
|
||||
@@ -48,7 +48,7 @@ void main(void){
|
||||
|
||||
vTexCoord = aTexCoord;
|
||||
|
||||
float scale = matrixScale(uModelView);
|
||||
float scale = uModelScale;
|
||||
|
||||
float offsetX = uOffsetX * scale;
|
||||
float offsetY = uOffsetY * scale;
|
||||
@@ -75,9 +75,16 @@ void main(void){
|
||||
offsetZ -= 0.001 * distance(uCameraPosition, (uProjection * mvCorner).xyz);
|
||||
}
|
||||
|
||||
mvCorner.xy += aMapping * size * scale;
|
||||
mvCorner.x += offsetX;
|
||||
mvCorner.y += offsetY;
|
||||
vec3 cornerOffset = vec3(0.0);
|
||||
cornerOffset.xy += aMapping * size * scale;
|
||||
cornerOffset.x += offsetX;
|
||||
cornerOffset.y += offsetY;
|
||||
|
||||
if (uHasHeadRotation) {
|
||||
mvCorner.xyz += (uInvHeadRotation * vec4(cornerOffset, 1.0)).xyz;
|
||||
} else {
|
||||
mvCorner.xyz += cornerOffset;
|
||||
}
|
||||
|
||||
if (uIsOrtho == 1.0) {
|
||||
mvCorner.z += offsetZ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,12 +12,13 @@ import { assertUnreachable, ValueOf } from '../../mol-util/type-helpers';
|
||||
import { GLRenderingContext, isWebGL2 } from './compat';
|
||||
import { WebGLExtensions } from './extensions';
|
||||
import { WebGLState } from './state';
|
||||
import { getBytesPerElement, getFormat, getType, TextureFormat, TextureType } from './texture';
|
||||
|
||||
const getNextBufferId = idFactory();
|
||||
|
||||
export type UsageHint = 'static' | 'dynamic' | 'stream'
|
||||
export type DataType = 'uint8' | 'int8' | 'uint16' | 'int16' | 'uint32' | 'int32' | 'float32'
|
||||
export type BufferType = 'attribute' | 'elements' | 'uniform'
|
||||
export type BufferType = 'attribute' | 'elements' | 'uniform' | 'pixel-pack'
|
||||
|
||||
export type DataTypeArrayType = {
|
||||
'uint8': Uint8Array
|
||||
@@ -36,6 +37,7 @@ export function getUsageHint(gl: GLRenderingContext, usageHint: UsageHint) {
|
||||
case 'static': return gl.STATIC_DRAW;
|
||||
case 'dynamic': return gl.DYNAMIC_DRAW;
|
||||
case 'stream': return gl.STREAM_DRAW;
|
||||
default: assertUnreachable(usageHint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +83,12 @@ export function getBufferType(gl: GLRenderingContext, bufferType: BufferType) {
|
||||
} else {
|
||||
throw new Error('WebGL2 is required for uniform buffers');
|
||||
}
|
||||
case 'pixel-pack':
|
||||
if (isWebGL2(gl)) {
|
||||
return gl.PIXEL_PACK_BUFFER;
|
||||
} else {
|
||||
throw new Error('WebGL2 is required for pixel-pack buffers');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,4 +266,63 @@ export function createElementsBuffer(gl: GLRenderingContext, array: ElementsType
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.getBuffer());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export interface PixelPackBuffer {
|
||||
readonly id: number
|
||||
|
||||
readonly _type: number
|
||||
readonly _format: number
|
||||
readonly _bpe: number
|
||||
|
||||
read: (x: number, y: number, width: number, height: number) => void
|
||||
getSubData: (array: ArrayType) => void
|
||||
|
||||
reset: () => void
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
export function createPixelPackBuffer(gl: WebGL2RenderingContext, extensions: WebGLExtensions, format: TextureFormat, type: TextureType): PixelPackBuffer {
|
||||
let _buffer = getBuffer(gl);
|
||||
|
||||
const _type = getType(gl, extensions, type);
|
||||
const _format = getFormat(gl, format, type);
|
||||
const _bpe = getBytesPerElement(format, type);
|
||||
|
||||
function read(x: number, y: number, width: number, height: number) {
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, _buffer);
|
||||
gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * _bpe, gl.STREAM_READ);
|
||||
gl.readPixels(x, y, width, height, _format, _type, 0);
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||
}
|
||||
|
||||
function getSubData(array: ArrayType) {
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, _buffer);
|
||||
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, array);
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||
}
|
||||
|
||||
let destroyed = false;
|
||||
|
||||
return {
|
||||
id: getNextBufferId(),
|
||||
|
||||
_type,
|
||||
_format,
|
||||
_bpe,
|
||||
|
||||
read,
|
||||
getSubData,
|
||||
|
||||
reset: () => {
|
||||
_buffer = getBuffer(gl);
|
||||
},
|
||||
destroy: () => {
|
||||
if (destroyed) return;
|
||||
gl.deleteBuffer(_buffer);
|
||||
destroyed = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ function createStats() {
|
||||
resourceCounts: {
|
||||
attribute: 0,
|
||||
elements: 0,
|
||||
pixelPack: 0,
|
||||
framebuffer: 0,
|
||||
program: 0,
|
||||
renderbuffer: 0,
|
||||
@@ -253,7 +254,6 @@ export interface WebGLContext {
|
||||
bindDrawingBuffer: () => void
|
||||
getDrawingBufferSize: () => { width: number, height: number }
|
||||
readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array | Int32Array) => void
|
||||
readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
|
||||
waitForGpuCommandsComplete: () => Promise<void>
|
||||
waitForGpuCommandsCompleteSync: () => void
|
||||
getFenceSync: () => WebGLSync | null
|
||||
@@ -304,43 +304,6 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
|
||||
|
||||
let pixelScale = props.pixelScale || 1;
|
||||
|
||||
let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>;
|
||||
if (isWebGL2(gl)) {
|
||||
const pbo = gl.createBuffer();
|
||||
let _buffer: Uint8Array | undefined = void 0;
|
||||
let _resolve: (() => void) | undefined = void 0;
|
||||
let _reading = false;
|
||||
|
||||
const bindPBO = () => {
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
|
||||
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, _buffer!);
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||
_reading = false;
|
||||
_resolve!();
|
||||
_resolve = void 0;
|
||||
_buffer = void 0;
|
||||
};
|
||||
readPixelsAsync = (x: number, y: number, width: number, height: number, buffer: Uint8Array): Promise<void> => new Promise<void>((resolve, reject) => {
|
||||
if (_reading) {
|
||||
reject('Can not call multiple readPixelsAsync at the same time');
|
||||
return;
|
||||
}
|
||||
_reading = true;
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
|
||||
gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STREAM_READ);
|
||||
gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0);
|
||||
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
||||
// need to unbind/bind PBO before/after async awaiting the fence
|
||||
_resolve = resolve;
|
||||
_buffer = buffer;
|
||||
fence(gl, bindPBO);
|
||||
});
|
||||
} else {
|
||||
readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
|
||||
readPixels(gl, x, y, width, height, buffer);
|
||||
};
|
||||
}
|
||||
|
||||
const renderTargets = new Set<RenderTarget>();
|
||||
|
||||
return {
|
||||
@@ -429,7 +392,6 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
|
||||
readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array | Int32Array) => {
|
||||
readPixels(gl, x, y, width, height, buffer);
|
||||
},
|
||||
readPixelsAsync,
|
||||
waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
|
||||
waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
|
||||
getFenceSync: () => {
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
import { ProgramProps, createProgram, Program } from './program';
|
||||
import { ShaderType, createShader, Shader, ShaderProps } from './shader';
|
||||
import { GLRenderingContext } from './compat';
|
||||
import { GLRenderingContext, isWebGL2 } from './compat';
|
||||
import { Framebuffer, createFramebuffer } from './framebuffer';
|
||||
import { WebGLExtensions } from './extensions';
|
||||
import { WebGLState } from './state';
|
||||
import { AttributeBuffer, UsageHint, ArrayType, AttributeItemSize, createAttributeBuffer, ElementsBuffer, createElementsBuffer, ElementsType, AttributeBuffers } from './buffer';
|
||||
import { AttributeBuffer, UsageHint, ArrayType, AttributeItemSize, createAttributeBuffer, ElementsBuffer, createElementsBuffer, ElementsType, AttributeBuffers, PixelPackBuffer, createPixelPackBuffer } from './buffer';
|
||||
import { createReferenceCache, ReferenceItem } from '../../mol-util/reference-cache';
|
||||
import { WebGLStats } from './context';
|
||||
import { hashString, hashFnv32a } from '../../mol-data/util';
|
||||
@@ -54,6 +54,7 @@ type ByteCounts = {
|
||||
export interface WebGLResources {
|
||||
attribute: (array: ArrayType, itemSize: AttributeItemSize, divisor: number, usageHint?: UsageHint) => AttributeBuffer
|
||||
elements: (array: ElementsType, usageHint?: UsageHint) => ElementsBuffer
|
||||
pixelPack: (format: TextureFormat, type: TextureType) => PixelPackBuffer
|
||||
framebuffer: () => Framebuffer
|
||||
program: (defineValues: DefineValues, shaderCode: ShaderCode, schema: RenderableSchema) => Program
|
||||
renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer
|
||||
@@ -72,6 +73,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
|
||||
const sets: { [k in ResourceName]: Set<Resource> } = {
|
||||
attribute: new Set<Resource>(),
|
||||
elements: new Set<Resource>(),
|
||||
pixelPack: new Set<Resource>(),
|
||||
framebuffer: new Set<Resource>(),
|
||||
program: new Set<Resource>(),
|
||||
renderbuffer: new Set<Resource>(),
|
||||
@@ -126,6 +128,12 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
|
||||
elements: (array: ElementsType, usageHint?: UsageHint) => {
|
||||
return wrap('elements', createElementsBuffer(gl, array, usageHint));
|
||||
},
|
||||
pixelPack: (format: TextureFormat, type: TextureType) => {
|
||||
if (!isWebGL2(gl)) {
|
||||
throw new Error('WebGL2 is required for pixel-pack buffers');
|
||||
}
|
||||
return wrap('pixelPack', createPixelPackBuffer(gl, extensions, format, type));
|
||||
},
|
||||
framebuffer: () => {
|
||||
return wrap('framebuffer', createFramebuffer(gl));
|
||||
},
|
||||
@@ -171,6 +179,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
|
||||
reset: () => {
|
||||
sets.attribute.forEach(r => r.reset());
|
||||
sets.elements.forEach(r => r.reset());
|
||||
sets.pixelPack.forEach(r => r.reset());
|
||||
sets.framebuffer.forEach(r => r.reset());
|
||||
sets.renderbuffer.forEach(r => r.reset());
|
||||
sets.shader.forEach(r => r.reset());
|
||||
@@ -182,6 +191,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
|
||||
destroy: () => {
|
||||
sets.attribute.forEach(r => r.destroy());
|
||||
sets.elements.forEach(r => r.destroy());
|
||||
sets.pixelPack.forEach(r => r.destroy());
|
||||
sets.framebuffer.forEach(r => r.destroy());
|
||||
sets.renderbuffer.forEach(r => r.destroy());
|
||||
sets.shader.forEach(r => r.destroy());
|
||||
|
||||
@@ -118,8 +118,11 @@ export function getInternalFormat(gl: GLRenderingContext, format: TextureFormat,
|
||||
}
|
||||
|
||||
function getByteCount(format: TextureFormat, type: TextureType, width: number, height: number, depth: number): number {
|
||||
const bpe = getFormatSize(format) * getTypeSize(type);
|
||||
return bpe * width * height * (depth || 1);
|
||||
return getBytesPerElement(format, type) * width * height * (depth || 1);
|
||||
}
|
||||
|
||||
export function getBytesPerElement(format: TextureFormat, type: TextureType): number {
|
||||
return getFormatSize(format) * getTypeSize(type);
|
||||
}
|
||||
|
||||
function getFormatSize(format: TextureFormat) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -11,11 +11,10 @@ import { ReaderResult } from '../result';
|
||||
import { Tokenizer } from '../common/text/tokenizer';
|
||||
import { StringLike } from '../../common/string-like';
|
||||
|
||||
|
||||
export function parsePDB(data: StringLike, id?: string, isPdbqt = false): Task<ReaderResult<PdbFile>> {
|
||||
return Task.create('Parse PDB', async ctx => ReaderResult.success({
|
||||
lines: await Tokenizer.readAllLinesAsync(data, ctx),
|
||||
id,
|
||||
isPdbqt
|
||||
isPdbqt,
|
||||
}));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -10,5 +10,5 @@ import { Tokens } from '../common/text/tokenizer';
|
||||
export interface PdbFile {
|
||||
lines: Tokens
|
||||
id?: string,
|
||||
isPdbqt?: boolean,
|
||||
isPdbqt?: boolean
|
||||
}
|
||||
44
src/mol-math/geometry/_spec/ray3d.spec.ts
Normal file
44
src/mol-math/geometry/_spec/ray3d.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { Vec3 } from '../../linear-algebra';
|
||||
import { Box3D } from '../primitives/box3d';
|
||||
import { Ray3D } from '../primitives/ray3d';
|
||||
|
||||
describe('ray3d', () => {
|
||||
it('intersectBox3D', () => {
|
||||
const box = Box3D.create(Vec3.create(-1, -1, -1), Vec3.create(1, 1, 1));
|
||||
const out = Vec3();
|
||||
|
||||
// 1. Ray starts outside and hits the box frontally
|
||||
const ray1 = Ray3D.create(Vec3.create(-2, 0, 0), Vec3.create(1, 0, 0));
|
||||
expect(Ray3D.intersectBox3D(out, ray1, box)).toBe(true);
|
||||
expect(out).toEqual(Vec3.create(-1, 0, 0));
|
||||
|
||||
// 2. Ray grazes along the top edge (tangential)
|
||||
const ray2 = Ray3D.create(Vec3.create(-2, 1, 0), Vec3.create(1, 0, 0));
|
||||
expect(Ray3D.intersectBox3D(out, ray2, box)).toBe(true);
|
||||
expect(out).toEqual(Vec3.create(-1, 1, 0));
|
||||
|
||||
// 3. Ray starts exactly on the surface and goes inward
|
||||
const ray3 = Ray3D.create(Vec3.create(-1, 0, 0), Vec3.create(1, 0, 0));
|
||||
expect(Ray3D.intersectBox3D(out, ray3, box)).toBe(true);
|
||||
expect(out).toEqual(Vec3.create(-1, 0, 0));
|
||||
|
||||
// 4. Ray grazes a corner exactly
|
||||
const ray4 = Ray3D.create(Vec3.create(-2, -2, -2), Vec3.create(1, 1, 1));
|
||||
expect(Ray3D.intersectBox3D(out, ray4, box)).toBe(true);
|
||||
expect(out).toEqual(Vec3.create(-1, -1, -1));
|
||||
|
||||
// 5. Ray starts inside the box and exits
|
||||
const ray5 = Ray3D.create(Vec3.create(0, 0, 0), Vec3.create(1, 0, 0));
|
||||
expect(Ray3D.intersectBox3D(out, ray5, box)).toBe(false);
|
||||
|
||||
// 6. Ray starts outside and points away (misses box completely)
|
||||
const ray6 = Ray3D.create(Vec3.create(-2, 2, 0), Vec3.create(1, 0, 0));
|
||||
expect(Ray3D.intersectBox3D(out, ray6, box)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -418,7 +418,7 @@ function queryNearest<T extends number = number>(ctx: QueryContext, result: Resu
|
||||
if (!Box3D.containsVec3(box, tmpRay.origin)) {
|
||||
// intersect ray pointing to box center
|
||||
Ray3D.targetTo(tmpRay, tmpRay, center);
|
||||
Box3D.nearestIntersectionWithRay3D(tmpRay.origin, box, tmpRay);
|
||||
Ray3D.intersectBox3D(tmpRay.origin, tmpRay, box);
|
||||
gX = Math.max(0, Math.min(sX - 1, Math.floor((tmpRay.origin[0] - min[0]) / delta[0])));
|
||||
gY = Math.max(0, Math.min(sY - 1, Math.floor((tmpRay.origin[1] - min[1]) / delta[1])));
|
||||
gZ = Math.max(0, Math.min(sZ - 1, Math.floor((tmpRay.origin[2] - min[2]) / delta[2])));
|
||||
|
||||
@@ -10,7 +10,6 @@ import { OrderedSet } from '../../../mol-data/int';
|
||||
import { Sphere3D } from './sphere3d';
|
||||
import { Vec3 } from '../../linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../linear-algebra/3d/mat4';
|
||||
import { Ray3D } from './ray3d';
|
||||
|
||||
interface Box3D { min: Vec3, max: Vec3 }
|
||||
|
||||
@@ -191,48 +190,6 @@ namespace Box3D {
|
||||
) ? false : true;
|
||||
}
|
||||
|
||||
export function nearestIntersectionWithRay3D(out: Vec3, box: Box3D, ray: Ray3D): Vec3 {
|
||||
const { origin, direction } = ray;
|
||||
const [minX, minY, minZ] = box.min;
|
||||
const [maxX, maxY, maxZ] = box.max;
|
||||
const [x, y, z] = origin;
|
||||
const invDirX = 1.0 / direction[0];
|
||||
const invDirY = 1.0 / direction[1];
|
||||
const invDirZ = 1.0 / direction[2];
|
||||
let tmin, tmax, tymin, tymax, tzmin, tzmax;
|
||||
if (invDirX >= 0) {
|
||||
tmin = (minX - x) * invDirX;
|
||||
tmax = (maxX - x) * invDirX;
|
||||
} else {
|
||||
tmin = (maxX - x) * invDirX;
|
||||
tmax = (minX - x) * invDirX;
|
||||
}
|
||||
if (invDirY >= 0) {
|
||||
tymin = (minY - y) * invDirY;
|
||||
tymax = (maxY - y) * invDirY;
|
||||
} else {
|
||||
tymin = (maxY - y) * invDirY;
|
||||
tymax = (minY - y) * invDirY;
|
||||
}
|
||||
if (invDirZ >= 0) {
|
||||
tzmin = (minZ - z) * invDirZ;
|
||||
tzmax = (maxZ - z) * invDirZ;
|
||||
} else {
|
||||
tzmin = (maxZ - z) * invDirZ;
|
||||
tzmax = (minZ - z) * invDirZ;
|
||||
}
|
||||
if (tymin > tmin)
|
||||
tmin = tymin;
|
||||
if (tymax < tmax)
|
||||
tmax = tymax;
|
||||
if (tzmin > tmin)
|
||||
tmin = tzmin;
|
||||
if (tzmax < tmax)
|
||||
tmax = tzmax;
|
||||
Vec3.scale(out, direction, tmin);
|
||||
return Vec3.set(out, out[0] + x, out[1] + y, out[2] + z);
|
||||
}
|
||||
|
||||
export function center(out: Vec3, box: Box3D): Vec3 {
|
||||
return Vec3.center(out, box.max, box.min);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mat4 } from '../../linear-algebra/3d/mat4';
|
||||
import { Vec3 } from '../../linear-algebra/3d/vec3';
|
||||
import { Box3D } from './box3d';
|
||||
import { Sphere3D } from './sphere3d';
|
||||
|
||||
interface Ray3D { origin: Vec3, direction: Vec3 }
|
||||
|
||||
@@ -38,6 +41,96 @@ namespace Ray3D {
|
||||
Vec3.transformDirection(out.direction, ray.direction, m);
|
||||
return out;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const tmpIR = Vec3();
|
||||
function _intersectSphere3D(ray: Ray3D, sphere: Sphere3D): number {
|
||||
const { center, radius } = sphere;
|
||||
const { origin, direction } = ray;
|
||||
|
||||
const oc = Vec3.sub(tmpIR, origin, center);
|
||||
const a = Vec3.dot(direction, direction);
|
||||
const b = 2.0 * Vec3.dot(oc, direction);
|
||||
const c = Vec3.dot(oc, oc) - radius * radius;
|
||||
const discriminant = b * b - 4 * a * c;
|
||||
|
||||
if (discriminant < 0) return -1; // no intersection
|
||||
|
||||
const t = (-b - Math.sqrt(discriminant)) / (2.0 * a);
|
||||
if (t < 0) return -1; // behind the ray
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
export function intersectSphere3D(out: Vec3, ray: Ray3D, sphere: Sphere3D): boolean {
|
||||
const t = _intersectSphere3D(ray, sphere);
|
||||
if (t < 0) return false;
|
||||
|
||||
Vec3.scaleAndAdd(out, ray.origin, ray.direction, t);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isIntersectingSphere3D(ray: Ray3D, sphere: Sphere3D): boolean {
|
||||
return _intersectSphere3D(ray, sphere) >= 0;
|
||||
}
|
||||
|
||||
export function isInsideSphere3D(ray: Ray3D, sphere: Sphere3D): boolean {
|
||||
return Vec3.distance(ray.origin, sphere.center) < sphere.radius;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function _intersectBox3D(ray: Ray3D, box: Box3D): number {
|
||||
const { origin, direction } = ray;
|
||||
const [minX, minY, minZ] = box.min;
|
||||
const [maxX, maxY, maxZ] = box.max;
|
||||
const [x, y, z] = origin;
|
||||
const invDirX = 1.0 / direction[0];
|
||||
const invDirY = 1.0 / direction[1];
|
||||
const invDirZ = 1.0 / direction[2];
|
||||
let tmin, tmax, tymin, tymax, tzmin, tzmax;
|
||||
if (invDirX >= 0) {
|
||||
tmin = (minX - x) * invDirX;
|
||||
tmax = (maxX - x) * invDirX;
|
||||
} else {
|
||||
tmin = (maxX - x) * invDirX;
|
||||
tmax = (minX - x) * invDirX;
|
||||
}
|
||||
if (invDirY >= 0) {
|
||||
tymin = (minY - y) * invDirY;
|
||||
tymax = (maxY - y) * invDirY;
|
||||
} else {
|
||||
tymin = (maxY - y) * invDirY;
|
||||
tymax = (minY - y) * invDirY;
|
||||
}
|
||||
if ((tmin > tymax) || (tymin > tmax)) return -1;
|
||||
if (tymin > tmin) tmin = tymin;
|
||||
if (tymax < tmax) tmax = tymax;
|
||||
if (invDirZ >= 0) {
|
||||
tzmin = (minZ - z) * invDirZ;
|
||||
tzmax = (maxZ - z) * invDirZ;
|
||||
} else {
|
||||
tzmin = (maxZ - z) * invDirZ;
|
||||
tzmax = (minZ - z) * invDirZ;
|
||||
}
|
||||
if ((tmin > tzmax) || (tzmin > tmax)) return -1;
|
||||
if (tzmin > tmin) tmin = tzmin;
|
||||
if (tzmax < tmax) tmax = tzmax;
|
||||
return tmin >= 0 ? tmin : -1;
|
||||
}
|
||||
|
||||
export function intersectBox3D(out: Vec3, ray: Ray3D, box: Box3D): boolean {
|
||||
const t = _intersectBox3D(ray, box);
|
||||
if (t < 0) return false;
|
||||
|
||||
Vec3.scaleAndAdd(out, ray.origin, ray.direction, t);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isIntersectingBox3D(ray: Ray3D, box: Box3D): boolean {
|
||||
return _intersectBox3D(ray, box) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
export { Ray3D };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -109,6 +109,23 @@ namespace Sphere3D {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Scale sphere by a number */
|
||||
export function scale(out: Sphere3D, sphere: Sphere3D, s: number) {
|
||||
Vec3.scale(out.center, sphere.center, s);
|
||||
out.radius = sphere.radius * s;
|
||||
if (hasExtrema(sphere)) {
|
||||
setExtrema(out, sphere.extrema.map(e => Vec3.scale(Vec3(), e, s)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Scale sphere by a number but without extrema */
|
||||
export function scaleNX(out: Sphere3D, sphere: Sphere3D, s: number) {
|
||||
Vec3.scale(out.center, sphere.center, s);
|
||||
out.radius = sphere.radius * s;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function toArray<T extends NumberArray>(s: Sphere3D, out: T, offset: number) {
|
||||
Vec3.toArray(s.center, out, offset);
|
||||
out[offset + 3] = s.radius;
|
||||
|
||||
@@ -126,6 +126,11 @@ namespace Mat3 {
|
||||
return fromMat4(out, _m4);
|
||||
}
|
||||
|
||||
export function fromRotation(out: Mat3, rad: number, axis: Vec3) {
|
||||
Mat4.fromRotation(_m4, rad, axis);
|
||||
return fromMat4(out, _m4);
|
||||
}
|
||||
|
||||
export function create(a00: number, a01: number, a02: number, a10: number, a11: number, a12: number, a20: number, a21: number, a22: number): Mat3 {
|
||||
const out = zero();
|
||||
out[0] = a00;
|
||||
|
||||
@@ -84,6 +84,11 @@ namespace Mat4 {
|
||||
return mat;
|
||||
}
|
||||
|
||||
export function isZero(mat: Mat4): boolean {
|
||||
for (let i = 0; i < 16; i++) if (mat[i] !== 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setZero(mat: Mat4): Mat4 {
|
||||
for (let i = 0; i < 16; i++) mat[i] = 0;
|
||||
return mat;
|
||||
@@ -1265,6 +1270,14 @@ namespace Mat4 {
|
||||
return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq));
|
||||
}
|
||||
|
||||
export function extractBasis(m: Mat4) {
|
||||
return {
|
||||
x: Vec3.create(m[0], m[1], m[2]),
|
||||
y: Vec3.create(m[4], m[5], m[6]),
|
||||
z: Vec3.create(m[8], m[9], m[10])
|
||||
};
|
||||
}
|
||||
|
||||
const xAxis = [1, 0, 0] as unknown as Vec3;
|
||||
const yAxis = [0, 1, 0] as unknown as Vec3;
|
||||
const zAxis = [0, 0, 1] as unknown as Vec3;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Kim Juho <juho_kim@outlook.com>
|
||||
*/
|
||||
|
||||
import { CifField } from '../../../mol-io/reader/cif';
|
||||
@@ -94,8 +95,10 @@ export function addAnisotropic(sites: AnisotropicTemplate, model: string, data:
|
||||
TokenBuilder.add(sites.pdbx_label_alt_id, s + 16, s + 17);
|
||||
}
|
||||
|
||||
// 18 - 20 Residue name resName Residue name.
|
||||
TokenBuilder.addToken(sites.pdbx_auth_comp_id, Tokenizer.trim(data, s + 17, s + 20));
|
||||
// 18 - 21 Residue name resName Residue name.
|
||||
// PDB spec defines 3-letter
|
||||
// but 4-letter are commonly used
|
||||
TokenBuilder.addToken(sites.pdbx_auth_comp_id, Tokenizer.trim(data, s + 17, s + 21));
|
||||
|
||||
// 22 Character chainID Chain identifier.
|
||||
TokenBuilder.add(sites.pdbx_auth_asym_id, s + 21, s + 22);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Kim Juho <juho_kim@outlook.com>
|
||||
*/
|
||||
|
||||
import { CifField } from '../../../mol-io/reader/cif';
|
||||
@@ -237,8 +238,10 @@ export function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer,
|
||||
TokenBuilder.add(sites.label_alt_id, s + 16, s + 17);
|
||||
}
|
||||
|
||||
// 18 - 20 Residue name Residue name.
|
||||
TokenBuilder.addToken(sites.auth_comp_id, Tokenizer.trim(data, s + 17, s + 20));
|
||||
// 18 - 21 Residue name Residue name.
|
||||
// PDB spec defines 3-letter
|
||||
// but 4-letter are commonly used
|
||||
TokenBuilder.addToken(sites.auth_comp_id, Tokenizer.trim(data, s + 17, s + 21));
|
||||
|
||||
// 22 Character Chain identifier.
|
||||
TokenBuilder.add(sites.auth_asym_id, s + 21, s + 22);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Kim Juho <juho_kim@outlook.com>
|
||||
*/
|
||||
|
||||
import { CifCategory, CifField } from '../../../mol-io/reader/cif';
|
||||
@@ -62,18 +63,22 @@ export function parseHelix(lines: Tokens, lineStart: number, lineEnd: number): C
|
||||
const line = getLine(i);
|
||||
// COLUMNS DATA TYPE FIELD DEFINITION
|
||||
// -----------------------------------------------------------------------------------
|
||||
// 1 - 6 Record name "HELIX "
|
||||
// 8 - 10 Integer serNum Serial number of the helix. This starts
|
||||
// 1 - 6 Record name "HELIX "
|
||||
// 8 - 10 Integer serNum Serial number of the helix. This starts
|
||||
// at 1 and increases incrementally.
|
||||
// 12 - 14 LString(3) helixID Helix identifier. In addition to a serial
|
||||
// number, each helix is given an
|
||||
// alphanumeric character helix identifier.
|
||||
// 16 - 18 Residue name initResName Name of the initial residue.
|
||||
// 16 - 19 Residue name initResName Name of the initial residue.
|
||||
// PDB spec defines 3-letter for residue name,
|
||||
// but 4-letter are commonly used
|
||||
// 20 Character initChainID Chain identifier for the chain containing
|
||||
// this helix.
|
||||
// 22 - 25 Integer initSeqNum Sequence number of the initial residue.
|
||||
// 26 AChar initICode Insertion code of the initial residue.
|
||||
// 28 - 30 Residue name endResName Name of the terminal residue of the helix.
|
||||
// 28 - 31 Residue name endResName Name of the terminal residue of the helix.
|
||||
// PDB spec defines 3-letter for residue name,
|
||||
// but 4-letter are commonly used
|
||||
// 32 Character endChainID Chain identifier for the chain containing
|
||||
// this helix.
|
||||
// 34 - 37 Integer endSeqNum Sequence number of the terminal residue.
|
||||
@@ -82,19 +87,19 @@ export function parseHelix(lines: Tokens, lineStart: number, lineEnd: number): C
|
||||
// 41 - 70 String comment Comment about this helix.
|
||||
// 72 - 76 Integer length Length of this helix.
|
||||
helices.push({
|
||||
serNum: line.substr(7, 3).trim(),
|
||||
helixID: line.substr(11, 3).trim(),
|
||||
initResName: line.substr(15, 3).trim(),
|
||||
initChainID: line.substr(19, 1).trim(),
|
||||
initSeqNum: line.substr(21, 4).trim(),
|
||||
initICode: line.substr(25, 1).trim(),
|
||||
endResName: line.substr(27, 3).trim(),
|
||||
endChainID: line.substr(31, 3).trim(),
|
||||
endSeqNum: line.substr(33, 4).trim(),
|
||||
endICode: line.substr(37, 1).trim(),
|
||||
helixClass: line.substr(38, 2).trim(),
|
||||
comment: line.substr(40, 30).trim(),
|
||||
length: line.substr(71, 5).trim()
|
||||
serNum: line.substring(7, 10).trim(),
|
||||
helixID: line.substring(11, 14).trim(),
|
||||
initResName: line.substring(15, 19).trim(),
|
||||
initChainID: line.substring(19, 20).trim(),
|
||||
initSeqNum: line.substring(21, 25).trim(),
|
||||
initICode: line.substring(25, 26).trim(),
|
||||
endResName: line.substring(27, 31).trim(),
|
||||
endChainID: line.substring(31, 34).trim(),
|
||||
endSeqNum: line.substring(33, 37).trim(),
|
||||
endICode: line.substring(37, 38).trim(),
|
||||
helixClass: line.substring(38, 40).trim(),
|
||||
comment: line.substring(40, 70).trim(),
|
||||
length: line.substring(71, 76).trim()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,19 +172,23 @@ export function parseSheet(lines: Tokens, lineStart: number, lineEnd: number): C
|
||||
const line = getLine(i);
|
||||
// COLUMNS DATA TYPE FIELD DEFINITION
|
||||
// -------------------------------------------------------------------------------------
|
||||
// 1 - 6 Record name "SHEET "
|
||||
// 8 - 10 Integer strand Strand number which starts at 1 for each
|
||||
// 1 - 6 Record name "SHEET "
|
||||
// 8 - 10 Integer strand Strand number which starts at 1 for each
|
||||
// strand within a sheet and increases by one.
|
||||
// 12 - 14 LString(3) sheetID Sheet identifier.
|
||||
// 15 - 16 Integer numStrands Number of strands in sheet.
|
||||
// 18 - 20 Residue name initResName Residue name of initial residue.
|
||||
// 18 - 21 Residue name initResName Residue name of initial residue.
|
||||
// PDB spec defines 3-letter for residue name,
|
||||
// but 4-letter are commonly used
|
||||
// 22 Character initChainID Chain identifier of initial residue
|
||||
// in strand.
|
||||
// 23 - 26 Integer initSeqNum Sequence number of initial residue
|
||||
// in strand.
|
||||
// 27 AChar initICode Insertion code of initial residue
|
||||
// in strand.
|
||||
// 29 - 31 Residue name endResName Residue name of terminal residue.
|
||||
// 29 - 32 Residue name endResName Residue name of terminal residue.
|
||||
// PDB spec defines 3-letter for residue name,
|
||||
// but 4-letter are commonly used
|
||||
// 33 Character endChainID Chain identifier of terminal residue.
|
||||
// 34 - 37 Integer endSeqNum Sequence number of terminal residue.
|
||||
// 38 AChar endICode Insertion code of terminal residue.
|
||||
@@ -187,7 +196,9 @@ export function parseSheet(lines: Tokens, lineStart: number, lineEnd: number): C
|
||||
// strand in the sheet. 0 if first strand,
|
||||
// 1 if parallel,and -1 if anti-parallel.
|
||||
// 42 - 45 Atom curAtom Registration. Atom name in current strand.
|
||||
// 46 - 48 Residue name curResName Registration. Residue name in current strand
|
||||
// 46 - 49 Residue name curResName Registration. Residue name in current strand
|
||||
// PDB spec defines 3-letter for residue name,
|
||||
// but 4-letter are commonly used
|
||||
// 50 Character curChainId Registration. Chain identifier in
|
||||
// current strand.
|
||||
// 51 - 54 Integer curResSeq Registration. Residue sequence number
|
||||
@@ -195,8 +206,10 @@ export function parseSheet(lines: Tokens, lineStart: number, lineEnd: number): C
|
||||
// 55 AChar curICode Registration. Insertion code in
|
||||
// current strand.
|
||||
// 57 - 60 Atom prevAtom Registration. Atom name in previous strand.
|
||||
// 61 - 63 Residue name prevResName Registration. Residue name in
|
||||
// 61 - 64 Residue name prevResName Registration. Residue name in
|
||||
// previous strand.
|
||||
// PDB spec defines 3-letter for residue name,
|
||||
// but 4-letter are commonly used
|
||||
// 65 Character prevChainId Registration. Chain identifier in
|
||||
// previous strand.
|
||||
// 66 - 69 Integer prevResSeq Registration. Residue sequence number
|
||||
@@ -204,28 +217,28 @@ export function parseSheet(lines: Tokens, lineStart: number, lineEnd: number): C
|
||||
// 70 AChar prevICode Registration. Insertion code in
|
||||
// previous strand.
|
||||
sheets.push({
|
||||
strand: line.substr(7, 3).trim(),
|
||||
sheetID: line.substr(11, 3).trim(),
|
||||
numStrands: line.substr(14, 2).trim(),
|
||||
initResName: line.substr(17, 3).trim(),
|
||||
initChainID: line.substr(21, 1).trim(),
|
||||
initSeqNum: line.substr(22, 4).trim(),
|
||||
initICode: line.substr(26, 1).trim(),
|
||||
endResName: line.substr(28, 3).trim(),
|
||||
endChainID: line.substr(32, 1).trim(),
|
||||
endSeqNum: line.substr(33, 4).trim(),
|
||||
endICode: line.substr(37, 1).trim(),
|
||||
sense: line.substr(38, 2).trim(),
|
||||
curAtom: line.substr(41, 4).trim(),
|
||||
curResName: line.substr(45, 3).trim(),
|
||||
curChainId: line.substr(49, 1).trim(),
|
||||
curResSeq: line.substr(50, 4).trim(),
|
||||
curICode: line.substr(54, 1).trim(),
|
||||
prevAtom: line.substr(56, 4).trim(),
|
||||
prevResName: line.substr(60, 3).trim(),
|
||||
prevChainId: line.substr(64, 1).trim(),
|
||||
prevResSeq: line.substr(65, 4).trim(),
|
||||
prevICode: line.substr(69, 1).trim(),
|
||||
strand: line.substring(7, 10).trim(),
|
||||
sheetID: line.substring(11, 14).trim(),
|
||||
numStrands: line.substring(14, 16).trim(),
|
||||
initResName: line.substring(17, 21).trim(),
|
||||
initChainID: line.substring(21, 22).trim(),
|
||||
initSeqNum: line.substring(22, 26).trim(),
|
||||
initICode: line.substring(26, 27).trim(),
|
||||
endResName: line.substring(28, 32).trim(),
|
||||
endChainID: line.substring(32, 33).trim(),
|
||||
endSeqNum: line.substring(33, 37).trim(),
|
||||
endICode: line.substring(37, 38).trim(),
|
||||
sense: line.substring(38, 40).trim(),
|
||||
curAtom: line.substring(41, 45).trim(),
|
||||
curResName: line.substring(45, 49).trim(),
|
||||
curChainId: line.substring(49, 50).trim(),
|
||||
curResSeq: line.substring(50, 54).trim(),
|
||||
curICode: line.substring(54, 55).trim(),
|
||||
prevAtom: line.substring(56, 60).trim(),
|
||||
prevResName: line.substring(60, 64).trim(),
|
||||
prevChainId: line.substring(64, 65).trim(),
|
||||
prevResSeq: line.substring(65, 69).trim(),
|
||||
prevICode: line.substring(69, 70).trim(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user