Compare commits

..

246 Commits

Author SHA1 Message Date
Alexander Rose
93a3eba66d Merge pull request #1834 from molstar/fix-aromatic-ring-hybridization
Fix aromatic ring detection not accounting for hybridization
2026-05-30 21:43:10 -07:00
Alexander Rose
41b8584fb7 Merge branch 'master' of https://github.com/molstar/molstar into fix-aromatic-ring-hybridization 2026-05-30 21:40:31 -07:00
Alexander Rose
523b17dfde Merge pull request #1824 from sbittrich/master
Non-covalent interactions: detect and visualize water bridges
2026-05-30 21:38:35 -07:00
Alexander Rose
c47b4d6078 Merge pull request #1833 from molstar/cam-anim-params
Add axis param to camera spin/rock animation
2026-05-30 21:32:01 -07:00
Alexander Rose
b94073b96f Merge branch 'master' of https://github.com/molstar/molstar into cam-anim-params 2026-05-30 21:27:45 -07:00
Alexander Rose
905eb3ec2f add default for backwards compatibility 2026-05-30 21:26:28 -07:00
Sebastian
3ae72e5c60 generic bridge visuals 2026-05-29 10:55:48 +02:00
Alexander Rose
055dfd4946 Merge pull request #1840 from giagitom/fix-premul-rgb
Fix exported image artifacts on transparent background
2026-05-26 21:58:08 -07:00
Sebastian
2601d2ba63 decouple water bridges from hbond detection 2026-05-26 16:35:38 +02:00
Sebastian
340806d774 generalized support of interaction bridges 2026-05-26 16:06:14 +02:00
Sebastian
18ad848de2 Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	CHANGELOG.md
2026-05-26 13:57:53 +02:00
giagitom
9de8334af5 Fix exported image artifacts on transparent background 2026-05-26 13:05:24 +02:00
Alexander Rose
57580a5e6b Merge pull request #1836 from giagitom/fix-cel-shading-ambient-color
Fix cel-shaded ambient color being stripped to luminance
2026-05-23 21:50:07 -07:00
giagitom
7da4a85459 Fix cel-shaded ambient color being stripped to luminance 2026-05-19 16:43:00 +02:00
Alexander Rose
b7c380fd90 Merge branch 'master' of https://github.com/molstar/molstar into fix-aromatic-ring-hybridization 2026-05-17 22:21:57 -07:00
Alexander Rose
bcd304d058 header 2026-05-17 22:19:04 -07:00
Alexander Rose
fd50a8f8e0 Fix aromatic ring detection not accounting for hybridization 2026-05-17 22:17:55 -07:00
Alexander Rose
27f251e8e4 Merge pull request #1832 from molstar/ssao-multi-fix
Fix SSAO half/quarter resolution textures for multi-scale
2026-05-17 20:15:09 -07:00
Alexander Rose
8d2a44983e remove superfluous enableAnimation param 2026-05-16 22:29:55 -07:00
Alexander Rose
f806ac1444 Add axis param to camera spin/rock animation 2026-05-16 22:25:55 -07:00
Alexander Rose
63a585d88a Merge pull request #1830 from josemduarte/ms-fix-omitwater
Fix ModelServer bugs for omitWater param in surroundingLigands endpoint
2026-05-16 22:24:58 -07:00
Alexander Rose
a4b5a16fcd Merge branch 'master' into ms-fix-omitwater 2026-05-16 22:22:42 -07:00
Alexander Rose
86bf859a63 Fix SSAO half/quarter resolution textures for multi-scale 2026-05-16 22:15:12 -07:00
Alexander Rose
1b8117d3f1 Fix Volume and Isosurface getBoundingSphere ignoring instances 2026-05-10 17:18:18 -07:00
Alexander Rose
400e2bbc45 Merge pull request #1822 from corredD/codex/dot-morton-spheres 2026-05-10 08:42:22 -07:00
Jose Duarte
e2e26c7e9c Updating changelog 2026-05-09 22:28:38 -07:00
Jose Duarte
5ca9020cbf mol-model: fix water leak in surroundingLigands query
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:21:49 -07:00
Jose Duarte
ea4c411d5c model-server: fix omit_water boolean parsing for REST GET requests
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:21:49 -07:00
Alexander Rose
ba7e3fe827 Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1822 2026-05-09 16:03:21 -07:00
Alexander Rose
8f20571a17 Merge pull request #1827 from molstar/camera-changed-event
Camera helpers
2026-05-09 16:02:30 -07:00
Alexander Rose
c25a4247e6 Merge branch 'master' of https://github.com/molstar/molstar into camera-changed-event 2026-05-09 15:57:47 -07:00
Alexander Rose
1071d3d8ba Merge pull request #1828 from molstar/instance-granularity-improvements
Instance granularity improvements
2026-05-09 15:56:36 -07:00
Alexander Rose
e8dc046570 Merge branch 'master' of https://github.com/molstar/molstar into instance-granularity-improvements 2026-05-09 15:54:00 -07:00
Alexander Rose
27f9c2aa67 Merge pull request #1829 from molstar/mesoscale-preset
Mesoscale preset
2026-05-09 15:53:29 -07:00
Alexander Rose
a4962231c8 revert 2026-05-09 15:51:05 -07:00
Alexander Rose
8833f29ce5 Merge branch 'master' of https://github.com/molstar/molstar into mesoscale-preset 2026-05-09 15:44:01 -07:00
Alexander Rose
40b6038380 type tweak 2026-05-09 15:43:04 -07:00
Armando Pellegrini
59e16e0187 Fix State.dispose() not invoking transformer dispose for live cells (#1826)
`Transformer.Definition.dispose` is documented as "automatically called
on deleting an object," but `State.dispose()` only disposed its own event
subjects and action manager — it never iterated still-live cells to call
their per-transformer dispose. Cells holding GL buffers, mesh data, etc.
only had their dispose fired on explicit deletion (e.g. `clear()`), so
any consumer that called `plugin.dispose()` without first awaiting
`plugin.clear()` retained the callback chain, the GL buffers it points
at, and any closures captured by it.

In a long-running single-page app where the user navigates between
routes that mount/unmount a Mol* viewer, this leaked roughly 25–50 MB
of process RSS per cycle even with `plugin.dispose()` correctly called.
A 20-cycle E2E mount/unmount harness on a 1AKE structure measured a
+541 MB RSS / +266 MB JS-heap delta in the unconditional-`dispose()`
case; calling `await plugin.clear()` before `plugin.dispose()` halved
the residual leak, confirming the per-cell dispose path was missing on
the unconditional `dispose()` route.

This change walks the cell tree once (post-order via the existing
`StateTree.doPostOrder` helper) and invokes the per-transformer dispose
for every still-live cell, swallowing+warning on errors so a single
faulty transformer can't prevent siblings from cleaning up. The
existing per-cell `dispose` helper is reused for consistency with
`updateNode`/`findDeletes` semantics.

Tests cover: chained transformers, sibling subtrees, throwing-dispose
isolation, and transformers without a dispose definition.

Also adds `useDefineForClassFields: false` to the jest esbuild
transform so tests can construct `State` (the `TransientTree` parameter
property + class field pattern relies on legacy class-field semantics,
which `tsc` honors via `target: es2018` but esbuild's default `esnext`
target does not).

Fixes #1825

Co-authored-by: Armando Pellegrini <tech.tools@boltz.bio>
2026-05-09 22:17:38 +02:00
Alexander Rose
ca5a50bd53 changelog 2026-05-09 12:36:38 -07:00
Alexander Rose
bccf54fabe avoid extra allocations 2026-05-09 12:36:32 -07:00
Alexander Rose
57a790544c Add mesoscale representation preset 2026-05-09 12:31:26 -07:00
Alexander Rose
df0669598c Add presets option to ObjectList param definition 2026-05-09 12:31:11 -07:00
Alexander Rose
fb912036af Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1822 2026-05-09 08:25:26 -07:00
Alexander Rose
9efb5cd126 Add Camera.changed event and rotation/translation setter/getter 2026-05-09 08:24:26 -07:00
Alexander Rose
08a56ad6ab Instance granularity improvements
- Add `instanceGranularity: 'auto'` as a memory guard
- Honor `instanceGranularity` in `Visual.getLoci`
2026-05-09 08:10:58 -07:00
Sebastian
2c2bd6adda tweak wb labels 2026-05-06 16:06:33 +02:00
Sebastian
b010298acb fix merge 2026-05-06 15:27:45 +02:00
Sebastian
7033a1e0b2 Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	CHANGELOG.md
2026-05-06 15:23:35 +02:00
Sebastian
8ad617acdf fix refinement 2026-05-06 15:20:34 +02:00
Sebastian
31ab6aa93e iterator improv 2026-05-06 11:32:50 +02:00
Sebastian
0a2dbe14d7 refine wb impl/vis 2026-05-06 11:11:43 +02:00
Sebastian
89d305aaa1 cl 2026-05-06 10:41:42 +02:00
Sebastian
dbb6b90fbc nci: improve wb visuals on shared legs 2026-05-06 10:37:08 +02:00
Sebastian
c57150f09f nci: filter hbonds if explained by water bridge 2026-05-06 10:22:33 +02:00
Sebastian
0b30c7344b nci: water bridge support 2026-05-06 09:56:06 +02:00
Alexander Rose
d7ad5a6e9f Fix empty transforms default in ShapeFromPly 2026-05-04 23:14:02 -07:00
Alexander Rose
86a74d1cc2 5.9.0 2026-05-03 10:45:51 -07:00
Alexander Rose
3f0f24cb99 changelog 2026-05-03 10:44:40 -07:00
Alexander Rose
b8ddc142ea schema updates 2026-05-03 00:15:31 -07:00
Alexander Rose
cccaa48589 ts6 tweaks 2026-05-03 00:12:36 -07:00
Alexander Rose
3ad355ad40 package updates 2026-05-03 00:12:23 -07:00
Alexander Rose
918186eb24 Merge pull request #1805 from molstar/proc-anim
Procedural animation
2026-05-02 16:18:22 -07:00
Alexander Rose
db4742cebf tweaks 2026-05-02 16:16:39 -07:00
Ludovic Autin
19fec3bbc1 Order DOT spheres by Morton index
Add DOT sphere impostors in Morton order so sphere LOD stride sampling remains spatially distributed.
2026-05-02 10:48:31 -07:00
Alexander Rose
7d6c77b3bd Merge branch 'master' of https://github.com/molstar/molstar into proc-anim 2026-05-02 09:34:00 -07:00
Alexander Rose
dfcc4e400d add animation support to texture-mesh geometry 2026-04-27 22:30:58 -07:00
Alexander Rose
c9734d83a2 Merge pull request #1818 from corredD/codex/fix-assembly-symmetry
Fix GraphQL POST request handling for Assembly Symmetry
2026-04-25 07:57:40 -07:00
Alexander Rose
93943cc27b changelog 2026-04-25 07:55:08 -07:00
Alexander Rose
25836b2de0 changelog 2026-04-25 07:50:28 -07:00
Alexander Rose
c6874c922d use record for headers
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 07:50:21 -07:00
Ludovic Autin
0937c84f47 Fix GraphQL POST request handling 2026-04-18 22:20:57 -07:00
Alexander Rose
6a7f892d60 cleanup & changelog 2026-04-18 10:57:14 -07:00
Alexander Rose
b4cd2d0a11 Merge branch 'master' of https://github.com/molstar/molstar into proc-anim 2026-04-18 10:56:12 -07:00
Alexander Rose
2067f02830 changelog 2026-04-18 10:55:09 -07:00
Alexander Rose
6d86ada6b4 getNucleicOneLetterCode 2026-04-18 10:54:48 -07:00
Alexander Rose
f656cf09b7 Merge pull request #1813 from corredD/codex/slice-marking-optimization
Slice marking optimization
2026-04-18 10:54:14 -07:00
Alexander Rose
a891b4c551 tweaks and changelog 2026-04-18 10:50:23 -07:00
Alexander Rose
ded844c936 Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1813 2026-04-18 10:46:58 -07:00
Alexander Rose
44b36637fd Merge pull request #1802 from molstar/ccd-bonds-deuterium
Handle CCD bonds with Deuterium atoms
2026-04-18 10:45:26 -07:00
Alexander Rose
f590bd0f0a Merge branch 'master' into ccd-bonds-deuterium 2026-04-18 10:29:12 -07:00
Alexander Rose
9474c80673 Merge pull request #1812 from molstar/8k-uhd-image-option
Add 8K UHD option to `ViewportScreenshotHelper`
2026-04-18 10:28:05 -07:00
Alexander Rose
7b48d691c8 Merge branch 'master' into 8k-uhd-image-option 2026-04-18 10:27:54 -07:00
Alexander Rose
b03146852f Merge pull request #1811 from molstar/mrc-empty-length 2026-04-18 09:34:42 -07:00
Ludovic Autin
9345f3584a Update slice marking file headers 2026-04-12 22:46:06 -07:00
Ludovic Autin
4d058aa1a8 Merge commit '94f6b864b0ede5c88b98725648178ceda5b7340b' into codex/slice-marking-optimization 2026-04-12 22:03:35 -07:00
Ludovic Autin
e7da6092aa Optimize slice marking for hover 2026-04-12 21:53:08 -07:00
David Sehnal
94f6b864b0 Fix empty PluginSpec.animations edgecase (#1810) 2026-04-12 19:37:21 +02:00
Alexander Rose
6e90447511 Add 8K UHD option to ViewportScreenshotHelper 2026-04-11 09:11:16 -07:00
Alexander Rose
b91030c4bd Handle MRC files with empty length header fields 2026-04-11 09:08:57 -07:00
dsehnal
31819dbf16 5.8.0 2026-04-03 19:28:58 +02:00
dsehnal
1665dd7d00 changelog + npm audit 2026-04-03 19:27:46 +02:00
Alexander Rose
9716fecdb9 add time only animation for exporting 2026-04-02 11:39:11 -07:00
Alexander Rose
684fd2d237 Merge branch 'master' of https://github.com/molstar/molstar into proc-anim 2026-04-01 16:34:01 -07:00
Alexander Rose
9432b9a7a7 ME: tweak size scale handling 2026-04-01 16:33:44 -07:00
Alexander Rose
3a37c95c17 scale tumble with bounding-sphere 2026-04-01 16:32:36 -07:00
Alexander Rose
6040b99c19 Merge branch 'master' of https://github.com/molstar/molstar into proc-anim 2026-04-01 14:22:50 -07:00
Alexander Rose
83bef0f0e7 proc anim panel & per-group wiggle 2026-04-01 14:22:38 -07:00
Alexander Rose
95bb3a1f81 Merge pull request #1801 from molstar/traj-static-prop-fix
Fix static model properties for trajectories
2026-04-01 10:31:29 -07:00
Alexander Rose
be677f47cb basic procedural animation 2026-03-27 12:55:00 -07:00
Alexander Rose
43bf69d09c handle ComponentBond.Entry.map changes 2026-03-27 09:35:25 -07:00
Alexander Rose
b6cc626431 cleanup 2026-03-27 09:14:46 -07:00
Alexander Rose
931fdfca9b move ccd logic to ComponentBond 2026-03-27 09:11:55 -07:00
midlik
1c10db5656 VolumeStreaming - avoid re-download on node update (#1804) 2026-03-27 11:36:14 +01:00
Paul Pillot
c4ccd8758f Replace node-fetch/@types with native fetch (#1803) 2026-03-27 08:44:29 +01:00
Alexander Rose
6c99c575bc Handle CCD bonds with Deuterium atoms 2026-03-26 16:23:32 -07:00
Alexander Rose
ae2493b6e3 Merge pull request #1798 from papillot/remove-promisify
remove utils.promisify dependency
2026-03-26 14:46:44 -07:00
Alexander Rose
bcd50c294f Merge pull request #1787 from molstar/vol-instance-slice
fix volume slice visual to handle instances
2026-03-26 14:44:31 -07:00
Alexander Rose
9c0024dbab Merge branch 'master' into vol-instance-slice 2026-03-26 14:41:42 -07:00
Alexander Rose
c15b3603c0 Merge pull request #1786 from molstar/vol-instance-refactor
volume refactoring for improved instance handling
2026-03-26 14:40:41 -07:00
Alexander Rose
70647ba972 tighter frame mode image size 2026-03-26 14:37:23 -07:00
Alexander Rose
8d19357845 reuse static props in Model._trajectoryFromModelAndCoordinates 2026-03-26 09:34:41 -07:00
Alexander Rose
8e9817c4d1 make Model.getAtomicRadii a static property 2026-03-26 09:33:32 -07:00
Alexander Rose
b16147b88c don't calculate atomic radii for fast boundary 2026-03-26 09:32:37 -07:00
Alexander Rose
9840d8f816 changelog 2026-03-25 15:35:11 -07:00
Alexander Rose
d892ccab4c fix mapping for grid image 2026-03-25 15:31:09 -07:00
Alexander Rose
65f88b3293 ensure image plane covers volume 2026-03-25 15:10:05 -07:00
Alexander Rose
9e6e5eb795 ensure image plane normal is normalized 2026-03-25 15:09:34 -07:00
Alexander Rose
2f755efeec handle voxels mapping to multiple pixels 2026-03-25 15:09:15 -07:00
Alexander Rose
012e616ec4 fix Mat4.fromPlane 2026-03-25 15:06:34 -07:00
midlik
007d0e7608 Fix areHierarchiesEqual, MVS empty selection focus, flickering tooltip (#1799)
* Fix StructureComponent.update when substructure empty

* Avoid tooltip box flickering when hovering something under it

* Fix MVS focus on empty selections

* Update CHANGELOG
2026-03-24 15:56:19 +01:00
Paul Pillot
bf313073b9 remove utils.promisify dependency
This dependency pulls a 23MB dependency graph to support nodejs v<8.0
Native alternatives exist: fs.promises, and native utils/promisify

Consistency: renaming readFile --> readFileAsync where changes were made.
2026-03-24 09:54:10 -04:00
Alexander Rose
293928f3de Merge branch 'vol-instance-refactor' of https://github.com/molstar/molstar into vol-instance-slice 2026-03-23 09:55:44 -07:00
Alexander Rose
2404f398b6 Merge branch 'master' of https://github.com/molstar/molstar into vol-instance-refactor 2026-03-23 09:55:11 -07:00
Alexander Rose
43ff6e24c8 Merge pull request #1789 from molstar/more-debug-helpers
More debug helpers
2026-03-23 09:54:24 -07:00
Alexander Rose
9e62112366 Merge branch 'master' into more-debug-helpers 2026-03-23 09:54:14 -07:00
midlik
026d6fc618 MVS VolumeStreamingExtension (#1793)
* InitVolumeStreaming refactor

* InitVolumeStreaming refactor 2

* CreateVolumeStreamingInfo autoEntries param

* MVS VolumeStreamingExtension

* Update CHANGELOG

* MVS: avoid structure focus persisting through states

* MVS VolumeStreamingExtension - collapse VolumeServer node
2026-03-21 10:32:17 +01:00
Alexander Rose
95fcd942dc refactor interior handling 2026-03-19 14:25:51 -07:00
Alexander Rose
805481db14 changelog 2026-03-19 14:06:28 -07:00
Alexander Rose
39175df025 tweaks 2026-03-19 13:55:05 -07:00
Alexander Rose
cd0f451f6b Merge branch 'master' of https://github.com/molstar/molstar into more-debug-helpers 2026-03-18 15:44:28 -07:00
Alexander Rose
fe1aa1a9bf move debug helpers to extension 2026-03-18 15:44:10 -07:00
Alexander Rose
fcfb6e6d5a Merge pull request #1788 from rjdirisio/seqres-to-sequence-toolbar
Use SEQRES to show unresolved residues in Sequence toolbar
2026-03-18 13:41:44 -07:00
Alexander Rose
c548c94575 Merge branch 'master' of https://github.com/molstar/molstar into more-debug-helpers 2026-03-18 12:50:59 -07:00
Alexander Rose
2d45f4a77c move code into getPdbxUnobsOrZeroOccResidues
- explicitely refer to mmcif enum types
2026-03-18 12:19:37 -07:00
Alexander Rose
a5ae887842 schema updates
- fix mmcif int enums
2026-03-18 12:17:28 -07:00
Ryan DiRisio
e4b53cdc6a simplify alignCompIdsToSeqres as a result of merge 2026-03-18 12:06:19 -04:00
Ryan DiRisio
c53940e67e Merge remote-tracking branch 'upstream/master' into seqres-to-sequence-toolbar 2026-03-18 12:01:10 -04:00
Alexander Rose
6d61745f0f Merge pull request #1794 from rjdirisio/fix-return-alignment-return-type
Fix return `alignment.trace()` return to match return type
2026-03-18 08:59:26 -07:00
Alexander Rose
46d86d93b0 tweak unknown residue handling in blosum scoring 2026-03-18 08:55:48 -07:00
Ryan DiRisio
11772b64fb update header 2026-03-18 11:18:07 -04:00
Ryan DiRisio
dbc8ab00c6 Merge branch 'master' into fix-return-alignment-return-type 2026-03-18 11:14:45 -04:00
Ryan DiRisio
015fad4371 trace() returns string[] rather than string 2026-03-18 11:13:07 -04:00
Ryan DiRisio
71a484586f Throw error if getEntityId not called before getEntityIdForChain 2026-03-18 11:02:15 -04:00
Ryan DiRisio
f0b06ee746 Merge remote-tracking branch 'upstream/master' into seqres-to-sequence-toolbar 2026-03-17 15:52:29 -04:00
Ryan DiRisio
b0694b886b add entity_poly_seq check in unobs test 2026-03-17 15:51:31 -04:00
Ryan DiRisio
eaf47b3169 rm excess comments 2026-03-17 15:41:48 -04:00
Ryan DiRisio
ad9046fcf2 regression tests for 1NSA 2026-03-17 15:25:07 -04:00
Ryan DiRisio
eabe4d46bc tests for SEQRES 2026-03-17 15:20:44 -04:00
Ryan DiRisio
003c5f8fb7 update file headers 2026-03-17 15:13:24 -04:00
Ryan DiRisio
68748a4a94 use -1 for unaligned residue 2026-03-17 15:11:44 -04:00
Ryan DiRisio
9bd6b8195d factor out initialLabelSeqId w/ comment 2026-03-17 15:06:53 -04:00
Ryan DiRisio
05848b651c factor out computeSeqresAlignments helper function 2026-03-17 15:02:57 -04:00
Ryan DiRisio
0a8f87dd9f factor out getEntityPolySeq
add to helperCategories
2026-03-17 15:00:09 -04:00
Ryan DiRisio
925aaa701d remove hasSeqRes, add debug log for seqres block 2026-03-17 14:53:49 -04:00
Ryan DiRisio
5be599bad4 Update CHANGELOG.md 2026-03-17 14:48:29 -04:00
Tianzhen Lin (Tangent)
e22ce53e65 Fix circular dependency crash in bundlers (esbuild, Rolldown) (#1792)
* Fix circular dependency crash in bundlers (esbuild, Rolldown)

StateTransforms uses `import * as X` namespace imports that are
assigned as object properties at module construction time. When a
bundler concatenates modules into a single scope and reorders their
initialization (as esbuild and Rolldown do), the namespace variable
can still be undefined when the object literal is evaluated, causing
a runtime crash:

  Cannot read properties of undefined (reading 'ModelUnitcell3D')

Replace direct property assignments with lazy getters so that
namespace imports are resolved at access time rather than at
construction time. This preserves the existing public API — callers
still use `StateTransforms.Representation.CreateStructureRepresentation3D`
— while making the code safe regardless of module evaluation order.

Fixes #1791

* Add changelog entry and contributor for circular dependency fix

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:38:48 +01:00
Alexander Rose
4c49431027 fix imports 2026-03-08 22:33:41 -07:00
Alexander Rose
4192d82ef3 file reorg 2026-03-08 22:25:27 -07:00
Alexander Rose
ce220737f2 description 2026-03-08 22:21:57 -07:00
Alexander Rose
eeb7cd2c52 add more debug helpers
- clip-object
- direct-volume
- image
- mesh
2026-03-08 22:18:30 -07:00
Alexander Rose
748111beb2 rename bounding-sphere helper and add separate debug helper 2026-03-08 22:16:59 -07:00
Alexander Rose
1f7d41c653 improve helpers drawing order 2026-03-08 22:14:46 -07:00
Alexander Rose
b9430ff387 add lines builders (box, plane, sphere) 2026-03-08 22:13:47 -07:00
Alexander Rose
6591bab035 Fix clip-object transform due to missing axis normalization 2026-03-07 10:21:21 -08:00
Ryan DiRisio
4da446aec2 skip non-polymer atoms 2026-03-02 13:01:02 -05:00
Ryan DiRisio
25c170e36d use global alignment that exists in the repo 2026-03-02 11:05:31 -05:00
Ryan DiRisio
eba18d1dce first pass at getting SEQRES data into sequence toolbar 2026-03-02 10:22:03 -05:00
Alexander Rose
2c87d01a5e fix volume slice visual to handle instances 2026-03-01 15:01:42 -08:00
Alexander Rose
e41a2baa32 volume refactoring for improved instance handling
- add Volume._localPropertyData
- move Volume.periodicity to Grid
- add optional Volume.parent
- fix Volume.getBoundingSphere to account for instances
- add periodicRange mode to VolumeInstances PluginStateTransform
- include currentVolume in VolumeViosual.setUpdateState
2026-03-01 14:56:53 -08:00
Alexander Rose
c297017749 Fix detecting sidechain-only structures as coarse-grained 2026-03-01 11:01:42 -08:00
Alexander Rose
9a0fc1faa6 changelog 2026-03-01 11:00:00 -08:00
Alexander Rose
424513f23c Merge branch 'master' of https://github.com/molstar/molstar 2026-03-01 10:58:57 -08:00
zachcp
895d672589 feat: add putty mvs to molstar (#1783) 2026-03-01 16:55:19 +01:00
Alexander Rose
0c6253ed16 formating 2026-02-28 18:23:41 -08:00
Alexander Rose
da97cd20aa 5.7.0 2026-02-28 13:43:11 -08:00
Alexander Rose
ca6d73e048 changelog 2026-02-28 13:41:30 -08:00
Alexander Rose
88b79deefa package updates
- set min node to v22
- added globals for eslint v10
2026-02-28 13:40:38 -08:00
Alexander Rose
d756e2e195 schema updates 2026-02-28 13:22:39 -08:00
Alexander Rose
2ce126a8f5 update cif-core schema 2026-02-28 13:19:05 -08:00
Alexander Rose
01e95dada0 ignore some lint commits in git blame 2026-02-28 13:16:59 -08:00
Alexander Rose
1c024f0943 Merge pull request #1778 from molstar/polyhedron
Polyhedron
2026-02-22 12:32:22 -08:00
Alexander Rose
5901e3d6a1 Merge branch 'master' into polyhedron 2026-02-22 12:29:02 -08:00
Alexander Rose
0cfe1cec66 Merge pull request #1784 from molstar/guard-xr-policy
Guard against `xr-spatial-tracking` blocked in `Permissions-Policy`
2026-02-22 12:28:44 -08:00
Alexander Rose
c1930e4142 Merge pull request #1771 from molstar/basic-streamlines
Basic streamlines
2026-02-22 12:13:39 -08:00
Alexander Rose
71375d908f Guard against xr-spatial-tracking blocked in Permissions-Policy 2026-02-22 11:31:08 -08:00
Alexander Rose
728b87d4e4 Merge branch 'master' of https://github.com/molstar/molstar into polyhedron 2026-02-21 20:58:32 -08:00
Alexander Rose
9c17698a8a rename visual to include "coordination" 2026-02-21 20:57:43 -08:00
Alexander Rose
625381c446 exclude hydrogens 2026-02-21 20:57:20 -08:00
Alexander Rose
da949a245e Merge pull request #1776 from molstar/element-parsing-fixes
Element parsing fixes
2026-02-19 20:40:44 -08:00
Alexander Rose
7000bdd15d Merge branch 'master' into element-parsing-fixes 2026-02-19 20:40:28 -08:00
Alexander Rose
adcf6a6fa8 Merge pull request #1777 from molstar/metal-coordination-style
Add `metalCoordination` style param
2026-02-19 20:40:07 -08:00
Alexander Rose
b70af9f178 Merge branch 'master' into metal-coordination-style 2026-02-19 20:39:40 -08:00
Alexander Rose
e5bdcfd781 Merge pull request #1781 from molstar/fix-unit-symmetry-groups
Fix `unitSymmetryGroups` for representations with `includeParent`
2026-02-18 20:46:15 -08:00
Alexander Rose
6049705224 Fix unitSymmetryGroups for representations with includeParent enabled 2026-02-16 19:57:56 -08:00
Alexander Rose
273d50d403 cleanup coordination data model 2026-02-15 20:59:52 -08:00
Alexander Rose
333ea724d6 Merge pull request #1779 from giagitom/text-improvements
Additional text improvements
2026-02-15 13:32:20 -08:00
giagitom
e96dca91ef Add author to text.ts 2026-02-15 13:36:34 +01:00
giagitom
41a0048f64 Simplify text frag shader, remove near-clip discard, fix bounding sphere padding 2026-02-15 13:33:44 +01:00
Alexander Rose
5e97b05bd2 Add Polyhedron representation showing coordination sites 2026-02-15 00:10:00 -08:00
Alexander Rose
ebc6b2acce Add Structure.coordination sites 2026-02-15 00:09:46 -08:00
Alexander Rose
8372408d9c Add convexHull helper 2026-02-15 00:09:17 -08:00
Alexander Rose
2c6822f5ab Add metalCoordination style param (dashed, solid) for bonds 2026-02-15 00:06:00 -08:00
Alexander Rose
7efbf46e7a Merge pull request #1774 from giagitom/text-improvements
Text label improvements
2026-02-14 23:34:01 -08:00
Alexander Rose
b6d6a518d3 Add more element-pair thresholds for bonding (Ag-S, CoSb, Ga-F) 2026-02-14 23:21:17 -08:00
Alexander Rose
2d690268f9 Handle additional elements in guessElementSymbol* (As, Li, Ga) 2026-02-14 23:20:54 -08:00
Alexander Rose
e0c794b557 Detect metal-coordination when parsing pdb 2026-02-14 23:20:25 -08:00
Alexander Rose
f91f445631 Fix parsing of single charge type_symbols (e.g., N+) in cif-core 2026-02-14 23:20:05 -08:00
giagitom
1cc367c8d8 Fix head rotation for clip-space billboard offset 2026-02-15 01:09:42 +01:00
giagitom
8c6969206d Update copyright years to 2026 2026-02-15 00:59:48 +01:00
Alexander Rose
c0479e3d46 Merge branch 'master' of https://github.com/molstar/molstar into basic-streamlines 2026-02-14 13:41:03 -08:00
Alexander Rose
22e92b38c6 Merge pull request #1775 from molstar/vertex-size
Add `vertex` and `vertexInstance` granularity for size
2026-02-14 13:40:21 -08:00
giagitom
5741709023 Hard discard near clip, revert attachment, cleanup nearFade 2026-02-14 21:11:56 +01:00
Alexander Rose
2265fc02cc per-point tube size 2026-02-14 08:56:04 -08:00
Alexander Rose
64180bef36 add min separation 2026-02-14 08:54:12 -08:00
Alexander Rose
be3caef6e9 Add vertex and vertexInstance granularity for size
- Geometry export: Support vertex-based sizing
- Add `transform` and `domain` parameters to volume-value size theme
2026-02-14 08:26:48 -08:00
giagitom
71a2f71866 Text label improvements
- Fix label attachment inversion (top/bottom and left/right)
- Improve label background vertical centering
- Add label near-clip fade out
- Handle label depth variant for correct transparent background and near fade
- Draw border under text using fragment depth to prevent overlap on adjacent characters
- Clamp border width to avoid exceeding SDF range
- Increase font atlas quality (2x font size multiplier)
- Use clip-space billboard rendering to avoid perspective distortion
2026-02-14 10:53:49 +01:00
David Sehnal
3c6152054e fix TextCtrl (#1773)
* fix TextCtrl

* header
2026-02-12 17:53:02 +01:00
Alexander Rose
080d649bf9 Streamlines support
- Add basic calculation method
 - Add custom-volume-property
 - Add representation with lines and tube-mesh visuals
2026-02-08 19:28:19 -08:00
Alexander Rose
2852b09c77 Merge pull request #1770 from molstar/line-strips 2026-02-08 15:09:12 -08:00
Paul Pillot
5e53467541 Tmalign perf tweaks (#1756)
* add computeCenter util fn

This is more efficient than using the CentroidHelper patterns.

Time improvement: 135ms --> 113ms on 4JV6/3T5I benchmark

* kabsch received array of indices

instead of array of coordinates which requires to build intermediary arrays in each caller.

* Reuse of Vec3 objects

- Avoid Vec3.create in loops
- use Vec3.squaredDistance instead of repeated double index access
- For simplifying TS manipulations, type xa, ya as Vec3 arrays.

* Reuse Positions

MinimeRmsdPositions.empty was called twice in kabsch fn. A unique call is now done with the full length and the same object is reused.
The length property is passed to ensure that only the portion that contains the required coordinates is used.

* simpler hypothenuse implementation

* missing break statements

- when score has not improved, we don't need to run again with the same parameters
- when the number of residues passing the cutoff is lower than 3, we don't need to run again with stricter cutoffs

* hint at float array

* update headers

* cache transformed coordinates, align indices

* replace ndpwScore with ndpwStructure

ndpwScore was creating an intermediate 2D array which is already cached in ndpwStructure

* trimmedKabschWithTransformedCoordinates

avoid recomputing transformed coordinates (as in trimmedKabsch) when they are already available

* cache rmsdResult and rmsdState

bestTransform must not keep a reference to result.bTransform because its value would change each time kabsch function is run.

* swap shortest structure to B

All transforms are made against the coordinates for structureB when yt is computed.

* reafactor: generalize usage of incremental sequence

Previous implementation was relying on tmpAlignA to hold the indices. It was conflictin in `tryGaplessThreading` with the call to `extendFromSeed`.
A distinct buffer has been created.

* refactor: preallocate dp arrays in constructor, uniform usage of typed arrays

* wrap tm-align in a task + useOverlay

* Skip fragment based strategy if similarity is good

Fragment based alignment takes up most of the computation time due to the O(n3) complexity of aligning fragments of different length. When the alignment is already good, this procedure does not yield improved results (observed also by the original authors).

The threshold for "similar structure" is set to a TM-score of 0.5 according to litterature.
Because it's possible to align 2 structures of very different length, and because the TM-score is normalized using the sequence length, it's important to change the reference for computation from a random choice (sequence A), to the minimal sequence length. This is consistent with the equations from the original TM-align publication (Lmin is used in scoring).

Note that the code already ensures that the shortest sequence is B (less iterations), but this commit makes the min sequence length explicit.
2026-02-08 14:49:14 +01:00
Alexander Rose
42dc579ddb changelog 2026-02-07 15:21:06 -08:00
Alexander Rose
890c758585 Geometry export
- Fix vertex-based coloring for non-mesh geometries
- Support line-strips
2026-02-07 15:20:58 -08:00
Alexander Rose
e6c77069df add frenet-frames helper 2026-02-07 15:20:14 -08:00
Alexander Rose
e7ecf98f13 add line-strips to lines geo 2026-02-07 15:19:25 -08:00
Alexander Rose
70ad32f62d Merge pull request #1769 from molstar/split-vol-visual
split volume visual code
2026-02-07 13:52:28 -08:00
Alexander Rose
69fe452055 split volume visual code 2026-02-07 10:43:12 -08:00
Alexander Rose
9edeb84f4e default to linear 2026-02-01 22:54:14 -08:00
Alexander Rose
e1db3114c8 Fix missing usePalette support in MeshExporter 2026-02-01 22:50:50 -08:00
Alexander Rose
8724badcb6 fix volume valueRef getData to select roots 2026-02-01 22:03:28 -08:00
Alexander Rose
d413f74526 Merge pull request #1764 from molstar/pqr-support
PQR support
2026-02-01 21:59:45 -08:00
Alexander Rose
6752108c5f changelog 2026-02-01 21:55:21 -08:00
Alexander Rose
9302fdadb9 undo adding pqr to mvs 2026-02-01 21:22:44 -08:00
Alexander Rose
f7048c7535 Merge branch 'master' of https://github.com/molstar/molstar into pqr-support 2026-02-01 21:20:57 -08:00
Alexander Rose
3252a3f0f3 Merge pull request #1765 from molstar/custom-volume-property 2026-02-01 12:48:28 -08:00
Alexander Rose
6805194d48 file headers 2026-01-31 17:33:52 -08:00
Alexander Rose
acf0dceb47 add CustomVolumeProperty 2026-01-31 17:30:43 -08:00
Alexander Rose
c53f500da6 add pqr support 2026-01-31 17:15:17 -08:00
midlik
defc04278e MVSData.toMVSX (#1763) 2026-01-29 16:51:54 +01:00
Alexander Rose
aa4d5e78a7 Merge pull request #1757 from giagitom/improve-outlines
disable transparent outline when near to solid mesh
2026-01-26 20:05:29 -08:00
Alexander Rose
df3a432afd Merge branch 'master' into improve-outlines 2026-01-25 22:38:19 -08:00
Alexander Rose
1b339d18cc Merge pull request #1762 from molstar/trackball-animation-axis
add axis param to trackball spin & rock animation
2026-01-25 22:37:24 -08:00
Alexander Rose
c4650c91a8 Merge branch 'master' into trackball-animation-axis 2026-01-25 22:37:15 -08:00
Alexander Rose
e3c4f19563 Merge pull request #1761 from molstar/fix-color-smoothing 2026-01-25 22:26:35 -08:00
Alexander Rose
85780a5d6a add axis param to trackball spin & rock animation 2026-01-24 12:11:50 -08:00
Alexander Rose
aab70e2ff0 Color smoothing fixes (#1747)
- use correct instance for non instance-type
- never transform for non instance-type
- add extra radius to gaussian surface boundingsphere
2026-01-24 12:09:04 -08:00
giagitom
3d95ed729c disable transparent outline when near to solid mesh 2026-01-19 19:59:07 +01:00
271 changed files with 21357 additions and 14450 deletions

14
.git-blame-ignore-revs Normal file
View File

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

View File

@@ -4,6 +4,107 @@ All notable changes to this project will be documented in this file, following t
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
## [Unreleased]
- Fix exported image artifacts on transparent background with emissive, bloom, or antialiasing
- Fix cel-shaded ambient color being stripped to luminance (now uses full RGB, matching the classic lighting path)
- Fix empty transforms default in `ShapeFromPly`
- Use morton order for spheres in dot visual with lod-levels
- Add `Camera.changed` event and rotation/translation setter/getter
- Add `instanceGranularity: 'auto'` as a memory guard
- Honor `instanceGranularity` in `Visual.getLoci`
- Add mesoscale representation preset
- Add presets option to `ObjectList` param definition
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
- Fix bugs in ModelServer surroundingLigands endpoint, resulting in omitWater not honored
- Fix `Volume` and `Isosurface` getBoundingSphere ignoring instances
- Fix aromatic ring detection not accounting for hybridization
- Add axis param to camera spin/rock animation
- Fix SSAO half/quarter resolution textures for multi-scale
- Non-covalent interactions: water bridge support
## [v5.9.0] - 2026-05-03
- Fix edge case when `PluginSpec.animations` is empty
- Add 8K UHD option to `ViewportScreenshotHelper`
- Handle MRC files with empty length header fields
- Handle CCD bonds with Deuterium atoms
- [Breaking] ComponentBond.Entry.map now returns ComponentBond.Pairs
- Fix volume slice marking performance regression
- Add GPU procedural animation (wiggle & tumble)
- Per-vertex wiggle via fbm noise (position & group mode)
- Per-instance tumble via fbm noise (rotation + translation)
- `Wiggle` theme layer for data-driven per-group wiggle
- `enableAnimation` Canvas3D param for global toggle
- Add `AnimateTime` built-in for, e.g., exporting procedural animation
- Add Procedural Animation panels
- Viewer: structure dynamics & uncertainty
- Mesoscale Explorer: entity dynamics
- Fix `GraphQLClient` missing required headers
- [Breaking] Use Record instead of Array for headers (assets & data-source utils)
## [v5.8.0] - 2026-04-03
- Dependencies: remove `utils.promisify`, `node-fetch` (#1797)
- Fix circular dependency which causes crash in bundlers (#1791)
- Add `putty` as a mol-view-spec representation.
- Fix detecting sidechain-only structures as coarse-grained (#1420)
- Fix clip-object transform due to missing axis normalization
- Sequence alignment: Fix return type & improve scoring for unknown residues
- Use PDB SEQRES block to show unresolved residues in Sequence toolbar
- Canvas3D debug-helpers
- [Breaking] Move helpers to an extension as a PluginBehavior (params are no longer part of Canvas3D)
- Add helpers for clip-object, direct-volume, image, mesh
- Fix StructureComponent node update throwing error when substructure empty
- CSS: Avoid tooltip box flickering when hovering something under it
- Volume slice visual
- Fix support for volume instances
- Fix plane mode: ensure normalized & correctly oriented
- MolViewSpec
- Add `VolumeStreamingExtension` (`molstar_volume_streaming` custom property)
- Fix focusing empty selections
- Avoid re-calculating static model properties for trajectories
## [v5.7.0] - 2026-02-28
- Text label improvements
- Improve label background vertical centering
- Handle label depth variant for correct transparent background
- Draw border under text using fragment depth to prevent overlap on adjacent characters
- Clamp border width to avoid exceeding SDF range
- Increase font atlas quality (2x font size multiplier)
- TM-align performance improvements (#1745)
- Disable transparent outline close to opaque elements
- Add axis param to trackball spin & rock animation
- Color smoothing fixes (#1747)
- Use correct instance for non instance-type
- Never transform for non instance-type
- Add extra radius to gaussian surface boundingsphere
- MolViewSpec
- Add `MVSData.toMVSX` function and `mvs-mvsj-to-mvsx.js` CLI utility
- [Breaking] Add PQR file format support (#157)
- Replace `isPdbqt` with `variant` param in `TrajectoryFromPDB`
- Add `CustomVolumeProperty` (like for models and structures)
- Geometry export
- Fix missing `usePalette` support
- Fix vertex-based coloring for non-mesh geometries
- Support line-strips
- Support vertex-based sizing
- Support memory efficient line-strips in Lines geometry,
- Add `StripLinesBuilder`
- Add `computeFrenetFrames` helper
- Streamlines support
- Add basic calculation method
- Add custom-volume-property
- Add representation with lines and tube-mesh visuals
- Fix `TextCtrl` always moving cursor to end position
- Add `vertex` and `vertexInstance` granularity support for size themes
- Add `transform` and `domain` parameters to volume-value size theme
- Fix parsing of single charge type_symbols (e.g., N+) in cif-core
- Detect metal-coordination when parsing pdb
- Handle additional elements in `guessElementSymbol*` (As, Li, Ga)
- Add more element-pair thresholds for bonding (Ag-S, CoSb, Ga-F)
- Add `metalCoordination` style param (dashed, solid) for bonds
- Fix `unitSymmetryGroups` for representations with `includeParent` enabled
- Add `convexHull` helper
- Add `Structure.coordination` sites
- Add `Polyhedron` representation showing coordination sites
- Guard against `xr-spatial-tracking` blocked in `Permissions-Policy`
## [v5.6.1] - 2026-01-23
- Disable occlusion culling in `ImagePass` (#1758)

View File

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

View File

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

View File

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

23308
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.6.1",
"version": "5.9.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -11,7 +11,7 @@
"url": "https://github.com/molstar/molstar/issues"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.0.0"
},
"scripts": {
"lint": "eslint .",
@@ -74,7 +74,7 @@
"js"
],
"transform": {
"\\.ts$": "esbuild-jest-transform"
"\\.ts$": ["esbuild-jest-transform", { "tsconfigRaw": "{\"compilerOptions\":{\"useDefineForClassFields\":false}}" }]
},
"moduleDirectories": [
"node_modules",
@@ -124,7 +124,8 @@
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
"Kim Juho <juho_kim@outlook.com>",
"Victoria Doshchenko <doshchenko.victoria@gmail.com>",
"Diego del Alamo <diego.delalamo@gmail.com>"
"Diego del Alamo <diego.delalamo@gmail.com>",
"Tianzhen Lin (Tangent) <tangent@usa.net>"
],
"license": "MIT",
"devDependencies": {
@@ -132,53 +133,51 @@
"@types/gl": "^6.0.5",
"@types/jest": "^30.0.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.27",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"@types/webxr": "^0.5.24",
"@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.53.0",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"benchmark": "^2.1.4",
"concurrently": "^9.2.1",
"cpx2": "^8.0.0",
"css-loader": "^7.1.2",
"esbuild": "^0.27.2",
"cpx2": "^8.0.2",
"css-loader": "^7.1.4",
"esbuild": "^0.28.0",
"esbuild-jest-transform": "^2.0.1",
"esbuild-sass-plugin": "^3.6.0",
"eslint": "^9.39.2",
"fs-extra": "^11.3.3",
"esbuild-sass-plugin": "^3.7.0",
"eslint": "^10.3.0",
"fs-extra": "^11.3.4",
"globals": "^17.6.0",
"http-server": "^14.1.1",
"jest": "^30.2.0",
"jest": "^30.3.0",
"jpeg-js": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.97.2",
"simple-git": "^3.30.0",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
"sass": "^1.99.0",
"simple-git": "^3.36.0",
"tsc-alias": "^1.8.17",
"typescript": "^6.0.3"
},
"dependencies": {
"@types/argparse": "^2.0.17",
"@types/benchmark": "^2.1.5",
"@types/compression": "1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^20.19.30",
"@types/node-fetch": "^2.6.13",
"@types/node": "^22.19.17",
"@types/swagger-ui-dist": "3.30.6",
"argparse": "^2.0.1",
"compression": "^1.8.1",
"cors": "^2.8.5",
"cors": "^2.8.6",
"express": "^5.2.1",
"h264-mp4-encoder": "^1.0.12",
"immutable": "^5.1.4",
"immutable": "^5.1.5",
"io-ts": "^2.2.22",
"mutative": "^1.3.0",
"node-fetch": "^2.7.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"rxjs": "^7.8.2",
"swagger-ui-dist": "^5.31.0",
"tslib": "^2.8.1",
"util.promisify": "^1.1.3"
"swagger-ui-dist": "^5.32.5",
"tslib": "^2.8.1"
},
"peerDependencies": {
"@google-cloud/storage": "^7.14.0",

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ludovic Autin <ludovic.autin@gmail.com>
@@ -55,7 +55,7 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
sizeTheme: {
name: 'physical',
params: {
value: 1,
scale: 1,
}
},
};

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -13,7 +13,7 @@ import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/
import { MesoscaleExplorerState } from '../app';
import { MesoscaleState } from '../data/state';
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, MesoProceduralAnimationControls, ExplorerInfo } from './states';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
import { RendererParams } from '../../../mol-gl/renderer';
@@ -145,6 +145,7 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
<StructureMeasurementsControls initiallyCollapsed={true}/>
</>
<MesoQuickStylesControls />
<MesoProceduralAnimationControls />
<Spacer />
<SectionHeader title='Entities' />
<EntityControls />

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -9,6 +9,7 @@
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { AssemblySymmetry } from '../../extensions/assembly-symmetry';
import { Backgrounds } from '../../extensions/backgrounds';
import { DebugHelpers } from '../../extensions/debug-helpers';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { G3DFormat } from '../../extensions/g3d/format';
import { GeometryExport } from '../../extensions/geo-export';
@@ -32,6 +33,7 @@ export const ExtensionMap = {
// Mol* built-in extensions
'mvs': PluginSpec.Behavior(MolViewSpec),
'backgrounds': PluginSpec.Behavior(Backgrounds),
'debug-helpers': PluginSpec.Behavior(DebugHelpers),
'model-export': PluginSpec.Behavior(ModelExport),
'mp4-export': PluginSpec.Behavior(Mp4Export),
'geo-export': PluginSpec.Behavior(GeometryExport),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,12 @@
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as argparse from 'argparse';
import * as fs from 'fs';
import * as path from 'path';
import fetch from 'node-fetch';
import { UniqueArray } from '../../mol-data/generic';
const LIPIDS_DIR = path.resolve(__dirname, '../../../../build/lipids/');

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import * as fs from 'fs';
import * as argparse from 'argparse';
import * as util from 'util';
import { Volume } from '../../mol-model/volume';
import { downloadCif } from './helpers';
@@ -20,8 +20,7 @@ import { createVolumeIsosurfaceMesh } from '../../mol-repr/volume/isosurface';
import { Theme } from '../../mol-theme/theme';
import { volumeFromDensityServerData, DscifFormat } from '../../mol-model-formats/volume/density-server';
require('util.promisify').shim();
const writeFileAsync = util.promisify(fs.writeFile);
const writeFileAsync = fs.promises.writeFile;
async function getVolume(url: string): Promise<Volume> {
const cif = await downloadCif(url, true);

View File

@@ -24,6 +24,7 @@ import './index.html';
import './tm-align.html';
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData, tmAlignStructures, loadStructuresNoAlignment, sequenceAlignStructures } from './superposition';
import '../../mol-plugin-ui/skin/light.scss';
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
@@ -95,7 +96,7 @@ class BasicWrapper {
...trackball,
animate: trackball.animate.name === 'spin'
? { name: 'off', params: {} }
: { name: 'spin', params: { speed: 1 } }
: { name: 'spin', params: { speed: 0.1, axis: Vec3.create(0, -1, 0) } }
}
}
});

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -118,6 +118,7 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
sourceData: CubeGridFormat(data),
customProperties: new CustomProperties(),
_propertyData: Object.create(null),
_localPropertyData: Object.create(null)
};
if (params.clampValues?.name === 'on') {
@@ -151,6 +152,7 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
sourceData: CubeGridFormat(data),
customProperties: new CustomProperties(),
_propertyData: Object.create(null),
_localPropertyData: Object.create(null)
};
if (params.clampValues?.name === 'on') {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -18,32 +18,33 @@ import { TransformData } from '../../mol-geo/geometry/transform-data';
import { sphereVertexCount } from '../../mol-geo/primitive/sphere';
import { ValueCell } from '../../mol-util';
import { Geometry } from '../../mol-geo/geometry/geometry';
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
export const DebugHelperParams = {
export const BoundingSphereHelperParams = {
sceneBoundingSpheres: PD.Boolean(false, { description: 'Show full scene bounding spheres.' }),
visibleSceneBoundingSpheres: PD.Boolean(false, { description: 'Show visible scene bounding spheres.' }),
objectBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible render objects.' }),
instanceBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible instances.' }),
};
export type DebugHelperParams = typeof DebugHelperParams
export type DebugHelperProps = PD.Values<DebugHelperParams>
export type BoundingSphereHelperParams = typeof BoundingSphereHelperParams;
export type BoundingSphereHelperProps = PD.Values<BoundingSphereHelperParams>;
type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: GraphicsRenderObject, mesh: Mesh }
export class BoundingSphereHelper {
export class BoundingSphereHelper implements DebugHelper<BoundingSphereHelperProps> {
readonly scene: Scene;
private readonly parent: Scene;
private _props: DebugHelperProps;
private _props: BoundingSphereHelperProps;
private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>();
private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>();
private sceneData: BoundingSphereData | undefined;
private visibleSceneData: BoundingSphereData | undefined;
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) {
constructor(ctx: WebGLContext, parent: Scene, props: Partial<BoundingSphereHelperProps>) {
this.scene = Scene.create(ctx, 'blended');
this.parent = parent;
this._props = { ...PD.getDefaultValues(DebugHelperParams), ...props };
this._props = { ...PD.getDefaultValues(BoundingSphereHelperParams), ...props };
}
update() {
@@ -120,9 +121,9 @@ export class BoundingSphereHelper {
this._props.objectBoundingSpheres || this._props.instanceBoundingSpheres
);
}
get props() { return this._props as Readonly<DebugHelperProps>; }
get props() { return this._props as Readonly<BoundingSphereHelperProps>; }
setProps(props: Partial<DebugHelperProps>) {
setProps(props: Partial<BoundingSphereHelperProps>) {
Object.assign(this._props, props);
if (this.isEnabled) this.update();
}
@@ -162,4 +163,4 @@ const instanceMaterialId = getNextMaterialId();
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1, transform);
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -241,7 +241,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
// create a glTF mesh if needed
if (instanceIndex === 0 || !sameGeometryBuffers || !sameColorBuffer) {
const { vertices, normals, indices, groups, vertexCount, drawCount } = GlbExporter.getInstance(input, instanceIndex);
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = GlbExporter.getInstance(input, instanceIndex);
// create geometry buffers if needed
if (instanceIndex === 0 || !sameGeometryBuffers) {
@@ -253,7 +253,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
// create a color buffer if needed
if (instanceIndex === 0 || !sameColorBuffer) {
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
}
// glTF mesh

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ export type InteractionElementSchema =
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
| { kind: 'water-bridge' } & InteractionElementSchemaBase
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
export type InteractionKind = InteractionElementSchema['kind']
@@ -39,6 +40,7 @@ export const InteractionKinds: InteractionKind[] = [
'weak-hydrogen-bond',
'hydrophobic',
'metal-coordination',
'water-bridge',
'covalent',
];
@@ -52,6 +54,7 @@ export type InteractionInfo =
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
| { kind: 'hydrophobic' }
| { kind: 'metal-coordination' }
| { kind: 'water-bridge' }
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
export interface StructureInteractionElement {
@@ -80,4 +83,5 @@ export const InteractionTypeToKind = {
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
[InteractionType.WaterBridge]: 'water-bridge' as InteractionKind,
};

View File

@@ -47,6 +47,7 @@ export const InteractionVisualParams = {
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
'hydrophobic': visualParams({ color: Color(0x555555) }),
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
'water-bridge': visualParams({ color: Color(0x00CCEE), style: 'dashed' }),
'covalent': PD.Group({
color: PD.Color(Color(0x999999)),
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { VolumeStreaming } from '../../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
import { CreateVolumeStreamingBehavior, CreateVolumeStreamingInfo, VolumeStreamingVisual } from '../../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
import { mapObjectMap } from '../../../mol-util/object';
import { decodeColor } from '../helpers/utils';
import { MolstarLoadingExtension } from '../load';
import { UpdateTarget } from '../load-generic';
import { ColorT } from '../tree/mvs/param-types';
/** Type of `molstar_volume_streaming` custom property, used by `VolumeStreamingExtension` MVS loading extension. */
export type MolstarVolumeStreamingCustomProp = {
/** URL of the volume streaming server, e.g. 'https://www.ebi.ac.uk/pdbe/densities'. */
server_url?: string,
/** Volume streaming view type ('off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'). Default value depends on structure type (X-ray/EM). */
view?: VolumeStreaming.ViewTypes,
/** Customization of channel parameters. */
channel_params?: { [name in VolumeStreaming.ChannelType]?: Partial<ChannelParams_> },
/** List of volume streaming entries (if not specified, will be retrieved automatically based on PDB ID) */
entries?: ReturnType<typeof CreateVolumeStreamingInfo['createDefaultParams']>['entries'],
} | boolean | undefined;
/** This MVS loading extension allows turning on volume streaming for a structure by providing custom property `molstar_volume_streaming`.
*
* Examples:
*
* ```
* builder
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1cbs_updated.cif' })
* .parse({ format: 'mmcif' })
* .modelStructure({
* custom: {
* molstar_volume_streaming: true,
* },
* })
* .component()
* .representation();
*
* builder
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1tqn_updated.cif' })
* .parse({ format: 'mmcif' })
* .modelStructure({
* custom: {
* molstar_volume_streaming: {
* channel_params: {
* '2fo-fc': { color: 'skyblue', opacity: 0.3 },
* 'fo-fc(+ve)': { color: 'greenyellow', wireframe: true, isoValue: { kind: 'relative', relativeValue: +2.5 } },
* 'fo-fc(-ve)': { color: 'orange', wireframe: true, isoValue: { kind: 'relative', relativeValue: -2.5 } },
* },
* } satisfies MolstarVolumeStreamingCustomProp,
* },
* })
* .component()
* .representation();
*
* builder
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/8hra_updated.cif' })
* .parse({ format: 'mmcif' })
* .modelStructure({
* custom: {
* molstar_volume_streaming: {
* server_url: 'https://www.ebi.ac.uk/pdbe/densities', // = default
* entries: [{ dataId: 'EMD-34965', source: { name: 'em', params: { isoValue: { kind: 'absolute', absoluteValue: 0.015 } } } }],
* view: 'auto', // default is 'auto' for EM, 'selection-box' for X-ray structures
* channel_params: {
* em: { color: '#ff0000', opacity: 0.4, isoValue: { kind: 'absolute', absoluteValue: 0.025 } },
* },
* } satisfies MolstarVolumeStreamingCustomProp,
* },
* })
* .component()
* .representation();
* ```
*/
export const VolumeStreamingExtension: MolstarLoadingExtension<{}> = {
id: 'wwpdb/volume-streaming',
description: 'Allow turning on volume streaming for a structure',
createExtensionContext: () => ({}),
action: (updateTarget, node, context, extContext) => {
if (node.kind !== 'structure') return;
let params: MolstarVolumeStreamingCustomProp = node.custom?.molstar_volume_streaming;
if (!params) return;
if (params === true) params = {};
const streamingInfo = UpdateTarget.apply(updateTarget, CreateVolumeStreamingInfo, {
serverUrl: params.server_url,
autoEntries: !params.entries,
entries: params.entries,
defaultView: params.view,
defaultChannelParams: params.channel_params && mapObjectMap(params.channel_params, normalizeChannelParams),
}, { state: { isCollapsed: true } });
const streamingBehavior = UpdateTarget.apply(streamingInfo, CreateVolumeStreamingBehavior);
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true }, tags: '2fo-fc' });
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true }, tags: 'fo-fc(+ve)' });
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true }, tags: 'fo-fc(-ve)' });
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true }, tags: 'em' });
},
};
interface ChannelParams_ extends Omit<VolumeStreaming.ChannelParams, 'color'> {
color: ColorT | number,
}
function normalizeChannelParams(p: Partial<ChannelParams_> | undefined): Partial<VolumeStreaming.ChannelParams> | undefined {
if (!p) return undefined;
return {
...p,
color: decodeColor(p.color),
};
}

View File

@@ -405,6 +405,15 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
};
}
case 'putty': {
const sizeTheme = params.size_theme ?? 'uniform';
return {
type: { name: 'putty', params: { alpha, sizeFactor: params.size_factor } },
sizeTheme: sizeTheme === 'uncertainty'
? { name: 'uncertainty', params: {} }
: { name: 'uniform', params: { value: params.size_factor } },
};
}
default:
throw new Error('NotImplementedError');
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -31,6 +31,7 @@ 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 { VolumeStreamingExtension } from './load-extensions/volume-streaming';
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
import { AnnotationFromSourceKind, AnnotationFromUriKind, clippingForNode, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
@@ -194,6 +195,7 @@ function molstarTreeToEntry(
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs });
}
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
snapshot.structureFocus = {}; // avoid structure focus persisting through states (causes weird behaviors, e.g. when turning on Volume Streaming)
if (tree.custom?.molstar_on_load_markdown_commands) {
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
@@ -305,7 +307,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
});
case 'pdb':
case 'pdbqt':
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { isPdbqt: format === 'pdbqt' });
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { variant: format });
case 'gro':
return UpdateTarget.apply(updateParent, TrajectoryFromGRO);
case 'xyz':
@@ -502,4 +504,5 @@ export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<Molsta
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
NonCovalentInteractionsExtension,
IsHiddenCustomStateExtension,
VolumeStreamingExtension,
];

View File

@@ -1,13 +1,16 @@
/**
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2026 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-validation';
import { treeToString } from './tree/generic/tree-utils';
import { ajaxGet } from '../../mol-util/data-source';
import { deepClone } from '../../mol-util/object';
import { createMVSX } from './export';
import { MVSAnimationSchema, MVSAnimationTree } from './tree/animation/animation-tree';
import { findUris, replaceUris, resolveUri, treeToString, windowUrl } from './tree/generic/tree-utils';
import { treeValidationIssues } from './tree/generic/tree-validation';
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
@@ -102,6 +105,55 @@ export const MVSData = {
return JSON.stringify(mvsData, undefined, space);
},
/** Encode `MVSData` to MVSX (MolViewSpec JSON zipped together with referenced assets). Automatically fetches all referenced assets unless specified otherwise in `options`. */
async toMVSX(mvsData: MVSData, options: {
/** Explicitely define assets to be included in the MVSX (binary data or string with asset content).
* If not specified, assets will be fetched automatically. */
assets?: { [uri: string]: Uint8Array<ArrayBuffer> | string },
/** Base URI for resolving relative URIs (only applies if `assets` not specified). */
baseUri?: string,
/** Do not include external resources (i.e. absolute URIs) in the MVSX (default is to include both relative and absolute URIs) (only applies if `assets` not specified). */
skipExternal?: boolean,
/** Optional cache for sharing fetched assets across multiple `toMVSX` calls (only applies if `assets` not specified). */
cache?: { [absoluteUri: string]: Uint8Array<ArrayBuffer> | string },
} = {}): Promise<Uint8Array<ArrayBuffer>> {
let { assets, baseUri, skipExternal, cache } = options;
mvsData = deepClone(mvsData);
const uriParamNames = ['uri', 'url'];
const trees = mvsData.kind === 'multiple' ? mvsData.snapshots.map(s => s.root) : [mvsData.root];
// Fetch assets:
if (!assets) {
assets = {};
cache ??= {};
const theWindowUrl = windowUrl();
const uris = new Set<string>();
for (const tree of trees) {
findUris(tree, uriParamNames, uris);
}
for (const uri of uris) {
if (skipExternal && isAbsoluteUri(uri)) continue;
const resolvedUri = resolveUri(uri, baseUri, theWindowUrl)!;
const content = cache[resolvedUri] ??= await ajaxGet({ url: resolvedUri, type: 'binary' }).run();
assets[uri] = content;
}
}
// Replace URIs by asset names:
const uriMapping: Record<string, string> = {};
const namedAssets: { name: string, content: string | Uint8Array<ArrayBuffer> }[] = [];
let counter = 0;
for (const uri in assets) {
const nameHint = uri.split('/').pop()!.replace(/[^\w\.+-]/g, '_').slice(0, 64);
const assetName = `./assets/${counter++}-${nameHint}`;
uriMapping[uri] = assetName;
namedAssets.push({ name: assetName, content: assets[uri] });
}
for (const tree of trees) {
replaceUris(tree, uriMapping, uriParamNames);
}
// Zip:
return await createMVSX(mvsData, namedAssets);
},
/** Validate `MVSData`. Return `true` if OK; `false` if not OK.
* If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */
isValid(mvsData: MVSData, options: { noExtra?: boolean } = {}): boolean {
@@ -207,3 +259,12 @@ function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: {
function utcNowISO(): string {
return new Date().toISOString();
}
function isAbsoluteUri(uri: string): boolean {
try {
const url = new URL(uri);
return !!url.protocol;
} catch {
return false;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -186,7 +186,7 @@ export function resolveUris<T extends Tree>(tree: T, baseUri: string, uriParamNa
* (i.e. the last one is the base URI). Skip any `undefined`.
* E.g. `resolveUri('./unexpected.png', '/spanish/inquisition/expectations.html', 'https://example.org/spam/spam/spam')`
* returns `'https://example.org/spanish/inquisition/unexpected.png'`. */
function resolveUri(...refs: (string | undefined)[]): string | undefined {
export function resolveUri(...refs: (string | undefined)[]): string | undefined {
let result: string | undefined = undefined;
for (const ref of refs.reverse()) {
if (ref !== undefined) {
@@ -197,7 +197,43 @@ function resolveUri(...refs: (string | undefined)[]): string | undefined {
return result;
}
/** Return URL of the current page when running in a browser; `undefined` when running in Node. */
function windowUrl(): string | undefined {
return (typeof window !== 'undefined') ? window.location.href : undefined;
/** Gather any URI params in a tree. URI params are those listed in `uriParamNames`. */
export function findUris<T extends Tree>(tree: T, uriParamNames: string[], out = new Set<string>()): Set<string> {
dfs(tree, node => {
const params = node.params as Record<string, any> | undefined;
if (!params) return;
for (const name of uriParamNames) {
const uri = params[name];
if (typeof uri === 'string') {
out.add(uri);
}
}
});
return out;
}
/** Replace any URI params in a tree using the given `uriMapping`, in place. URI params are those listed in `uriParamNames`. */
export function replaceUris<T extends Tree>(tree: T, uriMapping: { [oldUri: string]: string }, uriParamNames: string[]): void {
dfs(tree, node => {
const params = node.params as Record<string, any> | undefined;
if (!params) return;
for (const name of uriParamNames) {
const oldUri = params[name];
if (typeof oldUri === 'string' && typeof uriMapping[oldUri] === 'string') {
params[name] = uriMapping[oldUri];
}
}
});
}
/** Return URL of the current page when running in a browser; or file:// URL of the current working directory when running in Node. */
export function windowUrl(): string | undefined {
if (typeof window !== 'undefined') {
return window.location.href;
}
if (typeof process !== 'undefined') {
const cwd = process.cwd().replace(/\/?$/, '/');
return `file://${cwd}`;
}
return undefined;
}

View File

@@ -3,6 +3,7 @@
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Zachary Charlop-Powers <zach.charlop.powers@gmail.com>
*/
import { MVSData } from '../../../mvs-data';
@@ -17,6 +18,42 @@ describe('mvs-builder', () => {
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
});
it('putty representation works', () => {
const builder = createMVSBuilder();
builder
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
.parse({ format: 'mmcif' })
.modelStructure()
.component({ selector: 'polymer' })
.representation({ type: 'putty' });
const state = builder.getState();
expect(MVSData.validationIssues(state)).toEqual(undefined);
});
it('putty representation works with uniform size_theme', () => {
const builder = createMVSBuilder();
builder
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
.parse({ format: 'mmcif' })
.modelStructure()
.component({ selector: 'polymer' })
.representation({ type: 'putty', size_theme: 'uniform', size_factor: 0.5 });
const state = builder.getState();
expect(MVSData.validationIssues(state)).toEqual(undefined);
});
it('putty representation works with uncertainty size_theme', () => {
const builder = createMVSBuilder();
builder
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
.parse({ format: 'mmcif' })
.modelStructure()
.component({ selector: 'polymer' })
.representation({ type: 'putty', size_theme: 'uncertainty' });
const state = builder.getState();
expect(MVSData.validationIssues(state)).toEqual(undefined);
});
it('volume builder works', () => {
const builder = createMVSBuilder();
builder

View File

@@ -46,6 +46,13 @@ const Carbohydrate = {
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
};
const Putty = {
/** Scales the corresponding visuals */
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
/** Controls how the tube radius is determined. */
size_theme: OptionalField(literal('uniform', 'uncertainty'), 'uniform', "Controls how the tube radius is determined. 'uniform' uses a constant radius scaled by size_factor. 'uncertainty' drives the radius from per-residue B-factor/RMSF values."),
};
const Surface = {
/** Type of surface representation. (Default is 'molecular') */
surface_type: OptionalField(literal('molecular', 'gaussian'), 'molecular', `Type of surface representation. (Default is 'molecular')`),
@@ -66,6 +73,7 @@ export const MVSRepresentationParams = UnionParamsSchema(
spacefill: SimpleParamsSchema(Spacefill),
carbohydrate: SimpleParamsSchema(Carbohydrate),
surface: SimpleParamsSchema(Surface),
putty: SimpleParamsSchema(Putty),
},
);

View File

@@ -7,7 +7,7 @@
import { Viewport, cameraProject, cameraUnproject } from './camera/util';
import { CameraTransitionManager } from './camera/transition';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
import { Scene } from '../mol-gl/scene';
import { assertUnreachable } from '../mol-util/type-helpers';
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
@@ -15,6 +15,7 @@ import { Mat4 } from '../mol-math/linear-algebra/3d/mat4';
import { Vec4 } from '../mol-math/linear-algebra/3d/vec4';
import { Vec3 } from '../mol-math/linear-algebra/3d/vec3';
import { EPSILON } from '../mol-math/linear-algebra/3d/common';
import { Euler } from '../mol-math/linear-algebra/3d/euler';
export type { ICamera };
@@ -42,6 +43,12 @@ interface ICamera {
}
const tmpClip = Vec4();
const tmpForward = Vec3();
const tmpRight = Vec3();
const tmpUp = Vec3();
const tmpBack = Vec3();
const tmpDelta = Vec3();
const tmpRotMat = Mat4.identity();
export class Camera implements ICamera {
readonly view: Mat4 = Mat4.identity();
@@ -70,6 +77,8 @@ export class Camera implements ICamera {
readonly transition: CameraTransitionManager = new CameraTransitionManager(this);
readonly stateChanged = new BehaviorSubject<Partial<Camera.Snapshot>>(this.state);
/** Fires whenever update() produces a changed view/projection (covers all mutations, including direct ones from controls). */
readonly changed = new Subject<void>();
get position() { return this.state.position; }
set position(v: Vec3) { Vec3.copy(this.state.position, v); }
@@ -123,6 +132,7 @@ export class Camera implements ICamera {
Mat4.copy(this.prevView, this.view);
Mat4.copy(this.prevProjection, this.projection);
this.changed.next();
}
return changed;
@@ -237,6 +247,57 @@ export class Camera implements ICamera {
return out;
}
/** How much the camera is rotated around its target. Uses 'ZYX' order. */
getRotation(out: Euler) {
const { position, target, up } = this.state;
Vec3.normalize(tmpForward, Vec3.sub(tmpForward, target, position));
Vec3.normalize(tmpRight, Vec3.cross(tmpRight, tmpForward, up));
Vec3.cross(tmpUp, tmpRight, tmpForward);
Mat4.setIdentity(tmpRotMat);
tmpRotMat[0] = tmpRight[0]; tmpRotMat[1] = tmpRight[1]; tmpRotMat[2] = tmpRight[2];
tmpRotMat[4] = tmpUp[0]; tmpRotMat[5] = tmpUp[1]; tmpRotMat[6] = tmpUp[2];
tmpRotMat[8] = -tmpForward[0]; tmpRotMat[9] = -tmpForward[1]; tmpRotMat[10] = -tmpForward[2];
return Euler.fromMat4(out, tmpRotMat, 'ZYX');
}
/** Set the camera rotation around its target. Expects 'ZYX' order. */
setRotation(rotation: Euler, durationMs?: number) {
const snapshot = this.state as Camera.Snapshot;
const distance = Vec3.distance(snapshot.position, snapshot.target);
Mat4.fromEuler(tmpRotMat, rotation, 'ZYX');
// back = R * (0,0,1) → column 2 of R
Vec3.set(tmpBack, tmpRotMat[8], tmpRotMat[9], tmpRotMat[10]);
// up = R * (0,1,0) → column 1 of R
Vec3.set(tmpUp, tmpRotMat[4], tmpRotMat[5], tmpRotMat[6]);
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
Vec3.scaleAndAdd(state.position, snapshot.target, tmpBack, distance);
Vec3.copy(state.up, tmpUp);
this.setState(state, durationMs);
}
/** Translation of the camera target relative to world origin (0, 0, 0) */
getTranslation(out: Vec3) {
return Vec3.copy(out, this.state.target);
}
/** Set the camera target to the given translation, moving position by the same delta so orientation/distance are preserved */
setTranslation(translation: Vec3, durationMs?: number) {
const snapshot = this.state as Camera.Snapshot;
Vec3.sub(tmpDelta, translation, snapshot.target);
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
Vec3.add(state.position, snapshot.position, tmpDelta);
Vec3.copy(state.target, translation);
this.setState(state, durationMs);
}
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {
this.viewport = viewport;
Camera.copySnapshot(this.state, state);

View File

@@ -23,7 +23,7 @@ import { MarkerAction } from '../mol-util/marker-action';
import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
import { Camera } from './camera';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { DebugHelperParams } from './helper/bounding-sphere-helper';
import { DebugRegistry } from './helper/debug-registry';
import { SetUtils } from '../mol-util/set';
import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
import { PostprocessingParams } from './passes/postprocessing';
@@ -109,7 +109,6 @@ export const Canvas3DParams = {
renderer: PD.Group(RendererParams),
trackball: PD.Group(TrackballControlsParams),
interaction: PD.Group(Canvas3dInteractionHelperParams),
debug: PD.Group(DebugHelperParams),
handle: PD.Group(HandleHelperParams),
pointer: PD.Group(PointerHelperParams),
xr: PD.Group(XRManagerParams, { label: 'XR' }),
@@ -388,6 +387,8 @@ interface Canvas3D {
readonly stats: RendererStats
readonly interaction: Canvas3dInteractionHelper['events']
readonly debugRegistry: DebugRegistry
readonly xr: {
request(): Promise<void>
end(): Promise<void>
@@ -674,7 +675,8 @@ namespace Canvas3D {
const xrChanged = xrManager.update(xrFrame);
if (!xrChanged && xrFrame) return false;
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged;
const activeAnimation = renderer.props.enableAnimation && scene.hasAnimation;
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged || activeAnimation;
forceNextRender = false;
if (passes.illumination.supported && p.illumination.enabled && !xrFrame) {
@@ -753,6 +755,7 @@ namespace Canvas3D {
if (webgl.xr.session && !options?.xrFrame) return;
currentTime = t;
renderer.setTime((currentTime - startTime) / 1000);
commit(options?.isSynchronous);
// update the controler before the camera transition
@@ -1078,7 +1081,6 @@ namespace Canvas3D {
renderer: { ...renderer.props },
trackball: { ...controls.props },
interaction: { ...interactionHelper.props },
debug: { ...helper.debug.props },
handle: { ...helper.handle.props },
pointer: { ...helper.pointer.props },
xr: { ...xrManager.props },
@@ -1348,7 +1350,6 @@ namespace Canvas3D {
}
if (props.trackball) controls.setProps(props.trackball);
if (props.interaction) interactionHelper.setProps(props.interaction);
if (props.debug) helper.debug.setProps(props.debug);
if (props.handle) helper.handle.setProps(props.handle);
if (props.pointer) helper.pointer.setProps(props.pointer);
if (props.xr) xrManager.setProps(props.xr);
@@ -1399,6 +1400,7 @@ namespace Canvas3D {
get interaction() {
return interactionHelper.events;
},
debugRegistry: helper.debug,
xr,
dispose: () => {
contextLostSub?.unsubscribe();

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 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>
@@ -76,11 +76,13 @@ export const TrackballControlsParams = {
off: PD.EmptyGroup(),
spin: PD.Group({
speed: PD.Numeric(0.1, { min: -2, max: 2, step: 0.01 }, { description: 'Number of rotations per second' }),
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
}, { description: 'Spin the 3D scene around an axis in camera space' }),
rock: PD.Group({
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' })
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
}, { description: 'Rock the 3D scene around an axis in camera space' })
}),
staticMoving: PD.Boolean(true, { isHidden: true }),
@@ -855,27 +857,67 @@ namespace TrackballControls {
leaveSub.unsubscribe();
}
const _spinSpeed = Vec2.create(0.005, 0);
const _animateQuat = Quat();
const _animateAxis = Vec3();
const _animateUp = Vec3();
const _animateSide = Vec3();
const _animateDir = Vec3();
function spin(deltaT: number) {
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
const radPerMs = 2 * Math.PI * p.animate.params.speed / 1000;
_spinSpeed[0] = deltaT * radPerMs / getRotateFactor();
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
const angle = deltaT * radPerMs;
// Transform axis from camera space to world space
Vec3.sub(_eye, camera.position, camera.target);
Vec3.normalize(_animateDir, _eye); // Z-axis (view direction)
Vec3.normalize(_animateUp, camera.up); // Y-axis (up)
Vec3.cross(_animateSide, _animateUp, _animateDir); // X-axis (right)
Vec3.normalize(_animateSide, _animateSide);
const axis = p.animate.params.axis;
Vec3.set(_animateAxis,
axis[0] * _animateSide[0] + axis[1] * _animateUp[0] + axis[2] * _animateDir[0],
axis[0] * _animateSide[1] + axis[1] * _animateUp[1] + axis[2] * _animateDir[1],
axis[0] * _animateSide[2] + axis[1] * _animateUp[2] + axis[2] * _animateDir[2]
);
Vec3.normalize(_animateAxis, _animateAxis);
Quat.setAxisAngle(_animateQuat, _animateAxis, angle);
Vec3.transformQuat(_eye, _eye, _animateQuat);
Vec3.transformQuat(camera.up, camera.up, _animateQuat);
Vec3.add(camera.position, camera.target, _eye);
}
let _rockPhase = 0;
const _rockSpeed = Vec2.create(0.005, 0);
function rock(deltaT: number) {
if (p.animate.name !== 'rock' || p.animate.params.speed === 0 || _isInteracting) return;
const dt = deltaT / 1000 * p.animate.params.speed;
const maxAngle = degToRad(p.animate.params.angle) / getRotateFactor();
const maxAngle = degToRad(p.animate.params.angle);
const angleA = Math.sin(_rockPhase * Math.PI * 2) * maxAngle;
const angleB = Math.sin((_rockPhase + dt) * Math.PI * 2) * maxAngle;
const angle = angleB - angleA;
_rockSpeed[0] = angleB - angleA;
Vec2.add(_rotCurr, _rotPrev, _rockSpeed);
// Transform axis from camera space to world space
Vec3.sub(_eye, camera.position, camera.target);
Vec3.normalize(_animateDir, _eye); // Z-axis (view direction)
Vec3.normalize(_animateUp, camera.up); // Y-axis (up)
Vec3.cross(_animateSide, _animateUp, _animateDir); // X-axis (right)
Vec3.normalize(_animateSide, _animateSide);
const axis = p.animate.params.axis;
Vec3.set(_animateAxis,
axis[0] * _animateSide[0] + axis[1] * _animateUp[0] + axis[2] * _animateDir[0],
axis[0] * _animateSide[1] + axis[1] * _animateUp[1] + axis[2] * _animateDir[1],
axis[0] * _animateSide[2] + axis[1] * _animateUp[2] + axis[2] * _animateDir[2]
);
Vec3.normalize(_animateAxis, _animateAxis);
Quat.setAxisAngle(_animateQuat, _animateAxis, angle);
Vec3.transformQuat(_eye, _eye, _animateQuat);
Vec3.transformQuat(camera.up, camera.up, _animateQuat);
Vec3.add(camera.position, camera.target, _eye);
_rockPhase += dt;
if (_rockPhase >= 1) {

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { isDebugMode } from '../../mol-util/debug';
export interface DebugHelper<T extends {} = {}> {
readonly scene: Scene;
update(): void;
syncVisibility(): void;
clear(): void;
readonly isEnabled: boolean;
readonly props: T;
setProps(props: Partial<T>): void;
}
export class DebugRegistry {
readonly ctx: WebGLContext;
readonly parent: Scene;
private readonly entries = new Map<string, DebugHelper>();
constructor(ctx: WebGLContext, parent: Scene) {
this.ctx = ctx;
this.parent = parent;
}
register<T extends {}>(name: string, entry: DebugHelper<T>) {
if (this.entries.has(name)) {
if (isDebugMode) {
console.warn(`Debug helper with name '${name}' already exists, replacing.`);
}
this.entries.get(name)!.clear();
}
this.entries.set(name, entry);
}
unregister(name: string) {
const entry = this.entries.get(name);
if (entry) {
entry.clear();
this.entries.delete(name);
}
}
get scenes(): Scene[] {
return Array.from(this.entries.values()).map(e => e.scene);
}
update() {
this.entries.forEach(entry => {
if (entry.isEnabled) entry.update();
});
}
syncVisibility() {
this.entries.forEach(entry => {
entry.syncVisibility();
});
}
clear() {
this.entries.forEach(entry => {
entry.clear();
});
}
get isEnabled() {
let enabled = false;
this.entries.forEach(entry => {
if (entry.isEnabled) enabled = true;
});
return enabled;
}
setProps<T extends {}>(props: Partial<T>) {
this.entries.forEach(entry => {
entry.setProps(props);
});
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,13 +7,12 @@
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { BoundingSphereHelper, DebugHelperParams } from './bounding-sphere-helper';
import { DebugRegistry } from './debug-registry';
import { CameraHelper, CameraHelperParams } from './camera-helper';
import { HandleHelper, HandleHelperParams } from './handle-helper';
import { PointerHelper, PointerHelperParams } from './pointer-helper';
export const HelperParams = {
debug: PD.Group(DebugHelperParams),
camera: PD.Group({
helper: PD.Group(CameraHelperParams)
}),
@@ -25,7 +24,7 @@ export type HelperProps = PD.Values<typeof HelperParams>
export class Helper {
readonly debug: BoundingSphereHelper;
readonly debug: DebugRegistry;
readonly camera: CameraHelper;
readonly handle: HandleHelper;
readonly pointer: PointerHelper;
@@ -33,7 +32,7 @@ export class Helper {
constructor(webgl: WebGLContext, scene: Scene, props: Partial<HelperProps> = {}) {
const p = { ...DefaultHelperProps, ...props };
this.debug = new BoundingSphereHelper(webgl, scene, p.debug);
this.debug = new DebugRegistry(webgl, scene);
this.camera = new CameraHelper(webgl, p.camera.helper);
this.handle = new HandleHelper(webgl, p.handle);
this.pointer = new PointerHelper(webgl, p.pointer);

View File

@@ -121,7 +121,7 @@ export class PointerHelper {
this.camera = new Camera();
this.shape = getPointerMeshShape(this.getData(), this.props, this.shape);
this.shape = getPointerMeshShape(this.getData(), this.props);
this.renderObject = createMeshRenderObject(this.shape, this.props);
this.scene.add(this.renderObject);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -23,6 +23,7 @@ import { Canvas3dInteractionHelper } from './interaction-events';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { cameraProject } from '../camera/util';
import { Binding } from '../../mol-util/binding';
import { isDebugMode } from '../../mol-util/debug';
const B = ButtonsType;
const Trigger = Binding.Trigger;
@@ -281,15 +282,27 @@ export class XRManager {
}
private checkSupported = async () => {
if (!navigator.xr) return false;
if (!navigator.xr) {
this.isSupported.next(false);
return;
}
const [arSupported, vrSupported] = await Promise.all([
navigator.xr.isSessionSupported('immersive-ar'),
navigator.xr.isSessionSupported('immersive-vr'),
]);
this.isSupported.next(arSupported || vrSupported);
try {
const [arSupported, vrSupported] = await Promise.all([
navigator.xr.isSessionSupported('immersive-ar'),
navigator.xr.isSessionSupported('immersive-vr'),
]);
this.isSupported.next(arSupported || vrSupported);
} catch (e) {
if (isDebugMode) console.warn(e);
this.isSupported.next(false);
}
};
/**
* This may fail due to permissions policy, device capabilities, etc.
* Always wrap calls to it in a try/catch block to handle errors.
*/
async request() {
if (!navigator.xr) return;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 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>
@@ -446,18 +446,28 @@ export class DrawPass {
target.bind();
}
if (helper.debug.isEnabled) {
helper.debug.syncVisibility();
renderer.renderBlended(helper.debug.scene, camera);
if (helper.debug.isEnabled || helper.pointer.isEnabled) {
if (!this.packedDepth) {
this.depthTextureOpaque.attachFramebuffer(target.framebuffer, 'depth');
}
if (helper.debug.isEnabled) {
helper.debug.syncVisibility();
for (const scene of helper.debug.scenes) {
renderer.renderBlended(scene, camera);
}
}
if (helper.pointer.isEnabled) {
helper.pointer.setCamera(camera);
renderer.update(helper.pointer.camera, helper.pointer.scene);
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
}
if (!this.packedDepth) {
this.depthTextureOpaque.detachFramebuffer(target.framebuffer, 'depth');
}
}
if (helper.handle.isEnabled) {
renderer.renderBlended(helper.handle.scene, camera);
}
if (helper.pointer.isEnabled) {
helper.pointer.setCamera(camera);
renderer.update(helper.pointer.camera, helper.pointer.scene);
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
}
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera, helper.camera.scene);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2024-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -329,6 +329,31 @@ export class IlluminationPass {
renderer.setViewport(x, y, width, height);
renderer.update(camera, scene);
this.renderInput(renderer, camera, scene, props);
this.transparentTarget.bind();
if (helper.debug.isEnabled || helper.pointer.isEnabled) {
this.drawPass.depthTextureOpaque.attachFramebuffer(this.transparentTarget.framebuffer, 'depth');
if (helper.debug.isEnabled) {
helper.debug.syncVisibility();
for (const scene of helper.debug.scenes) {
renderer.renderBlended(scene, camera);
}
}
if (helper.pointer.isEnabled) {
helper.pointer.setCamera(camera);
renderer.update(helper.pointer.camera, helper.pointer.scene);
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
}
this.drawPass.depthTextureOpaque.detachFramebuffer(this.transparentTarget.framebuffer, 'depth');
}
if (helper.handle.isEnabled) {
renderer.renderBlended(helper.handle.scene, camera);
}
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
}
}
state.disable(gl.BLEND);
@@ -433,19 +458,6 @@ export class IlluminationPass {
renderer.setViewport(x, y, width, height);
renderer.update(camera, scene);
if (helper.debug.isEnabled) {
helper.debug.syncVisibility();
renderer.renderBlended(helper.debug.scene, camera);
}
if (helper.handle.isEnabled) {
renderer.renderBlended(helper.handle.scene, camera);
}
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
}
//
let targetIsDrawingbuffer = false;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 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>
@@ -484,27 +484,45 @@ export class SsaoPass {
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
}
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
if (multiScale) {
// half-resolution viewport (matches dimensions of depthHalfTarget*)
const hsx = Math.floor(sx * 0.5);
const hsy = Math.floor(sy * 0.5);
const hsw = Math.ceil(sw * 0.5);
const hsh = Math.ceil(sh * 0.5);
state.viewport(hsx, hsy, hsw, hsh);
state.scissor(hsx, hsy, hsw, hsh);
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
this.depthHalfTargetOpaque.bind();
this.depthHalfRenderableOpaque.render();
}
if (multiScale && includeTransparent) {
this.depthHalfTargetTransparent.bind();
this.depthHalfRenderableTransparent.render();
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
if (includeTransparent) {
this.depthHalfTargetTransparent.bind();
this.depthHalfRenderableTransparent.render();
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
if (multiScale) {
// quarter-resolution viewport (matches dimensions of depthQuarterTarget*)
const qsx = Math.floor(sx * 0.25);
const qsy = Math.floor(sy * 0.25);
const qsw = Math.ceil(sw * 0.25);
const qsh = Math.ceil(sh * 0.25);
state.viewport(qsx, qsy, qsw, qsh);
state.scissor(qsx, qsy, qsw, qsh);
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
this.depthQuarterTargetOpaque.bind();
this.depthQuarterRenderableOpaque.render();
if (includeTransparent) {
this.depthQuarterTargetTransparent.bind();
this.depthQuarterRenderableTransparent.render();
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
// restore full-scale viewport for SSAO + blur passes
state.viewport(sx, sy, sw, sh);
state.scissor(sx, sy, sw, sh);
}
if (multiScale && includeTransparent) {
this.depthQuarterTargetTransparent.bind();
this.depthQuarterRenderableTransparent.render();
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
if (isTimingMode) this.webgl.timer.mark('SSAO.opaque');
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');

View File

@@ -577,3 +577,25 @@ export class PCG {
return this.int() / 0x100000000;
}
}
export function mortonOrder3d(x: number, y: number, z: number): number {
let out = 0;
for (let i = 0; i < 21; ++i) {
out |= ((x >> i) & 1) << (3 * i + 2);
out |= ((y >> i) & 1) << (3 * i + 1);
out |= ((z >> i) & 1) << (3 * i);
}
return out;
}
export function invertMortonOrder3d(code: number): [number, number, number] {
let x = 0;
let y = 0;
let z = 0;
for (let i = 0; i < 21; ++i) {
x |= ((code >> (3 * i + 2)) & 1) << i;
y |= ((code >> (3 * i + 1)) & 1) << i;
z |= ((code >> (3 * i)) & 1) << i;
}
return [x, y, z];
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ValueCell } from '../../mol-util';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
export type AnimationData = {
uWiggleSpeed: ValueCell<number>,
uWiggleAmplitude: ValueCell<number>,
uWiggleFrequency: ValueCell<number>,
uWiggleMode: ValueCell<number>,
uTumbleSpeed: ValueCell<number>,
uTumbleAmplitude: ValueCell<number>,
uTumbleFrequency: ValueCell<number>,
}
export function getAnimationParam() {
return PD.Group({
wiggleMode: PD.Select('position', [['position', 'Position'], ['group', 'Group']] as const, { description: 'Noise seeding mode. Position: spatially correlated (nearby atoms move together). Group: per-group independent noise.' }),
wiggleSpeed: PD.Numeric(7, { min: 0, max: 10, step: 0.1 }, { description: 'Speed of vertex wiggle animation.' }),
wiggleAmplitude: PD.Numeric(0, { min: 0, max: 5, step: 0.01 }, { description: 'Amplitude of vertex wiggle animation.' }),
wiggleFrequency: PD.Numeric(0.2, { min: 0.01, max: 2, step: 0.01 }, { description: 'Spatial frequency of vertex wiggle noise (position mode). Lower values correlate nearby atoms more.' }),
tumbleSpeed: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, { description: 'Speed of instance tumble animation.' }),
tumbleAmplitude: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, { description: 'Amplitude of instance tumble animation. In Ångströms of implied surface displacement.' }),
tumbleFrequency: PD.Numeric(0.2, { min: 0, max: 2, step: 0.01 }, { description: 'Spatial frequency multiplier for tumble noise.' }),
});
}
export type AnimationParam = ReturnType<typeof getAnimationParam>
export type AnimationProps = AnimationParam['defaultValue'];
export function areAnimationPropsEqual(a: AnimationProps, b: AnimationProps): boolean {
return a.wiggleMode === b.wiggleMode
&& a.wiggleSpeed === b.wiggleSpeed
&& a.wiggleAmplitude === b.wiggleAmplitude
&& a.wiggleFrequency === b.wiggleFrequency
&& a.tumbleSpeed === b.tumbleSpeed
&& a.tumbleAmplitude === b.tumbleAmplitude
&& a.tumbleFrequency === b.tumbleFrequency;
}
export function createAnimationValues(props: AnimationProps) {
return {
uWiggleSpeed: ValueCell.create(props.wiggleSpeed),
uWiggleAmplitude: ValueCell.create(props.wiggleAmplitude),
uWiggleFrequency: ValueCell.create(props.wiggleFrequency),
uWiggleMode: ValueCell.create(props.wiggleMode === 'position' ? 0 : 1),
uTumbleSpeed: ValueCell.create(props.tumbleSpeed),
uTumbleAmplitude: ValueCell.create(props.tumbleAmplitude),
uTumbleFrequency: ValueCell.create(props.tumbleFrequency),
};
}
export function updateAnimationValues(values: AnimationData, props: AnimationProps) {
ValueCell.updateIfChanged(values.uWiggleSpeed, props.wiggleSpeed);
ValueCell.updateIfChanged(values.uWiggleAmplitude, props.wiggleAmplitude);
ValueCell.updateIfChanged(values.uWiggleFrequency, props.wiggleFrequency);
ValueCell.updateIfChanged(values.uWiggleMode, props.wiggleMode === 'position' ? 0 : 1);
ValueCell.updateIfChanged(values.uTumbleSpeed, props.tumbleSpeed);
ValueCell.updateIfChanged(values.uTumbleAmplitude, props.tumbleAmplitude);
ValueCell.updateIfChanged(values.uTumbleFrequency, props.tumbleFrequency);
}

View File

@@ -72,6 +72,25 @@ export function getColorSmoothingProps(smoothColors: PD.Values<ColorSmoothingPar
//
export type InstanceGranularityValue = true | false | 'auto'
export const InstanceGranularityOptions: [InstanceGranularityValue, string][] = [[true, 'On'], [false, 'Off'], ['auto', 'Auto']];
/**
* Threshold (in `groupCount * instanceCount`, e.g. number of marker-texture
* slots) above which `instanceGranularity: 'auto'` resolves to `true`.
*/
export const AutoInstanceGranularityThreshold = 50_000_000;
/**
* Resolves the `instanceGranularity` param value to a boolean.
*/
export function resolveInstanceGranularity(value: InstanceGranularityValue, groupCount: number, instanceCount: number): boolean {
if (value === 'auto') return groupCount * instanceCount > AutoInstanceGranularityThreshold;
return value;
}
//
export namespace BaseGeometry {
export const MaterialCategory: PD.Info = { category: 'Material' };
export const ShadingCategory: PD.Info = { category: 'Shading' };
@@ -88,7 +107,7 @@ export namespace BaseGeometry {
clip: PD.Group(Clip.Params),
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
density: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }, { description: 'Density value to estimate object thickness.' }),
instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
instanceGranularity: PD.Select<InstanceGranularityValue>('auto', InstanceGranularityOptions, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory. When set to `auto`, granularity is enabled if `groupCount * instanceCount` exceeds `AutoInstanceGranularityThreshold`.' }),
lod: PD.Vec3(Vec3(), undefined, { ...CullingLodCategory, description: 'Level of detail.', fieldLabels: { x: 'Min Distance', y: 'Max Distance', z: 'Overlap (Shader)' } }),
cellSize: PD.Numeric(200, { min: 0, max: 5000, step: 100 }, { ...CullingLodCategory, description: 'Instance grid cell size.' }),
batchSize: PD.Numeric(2000, { min: 0, max: 50000, step: 500 }, { ...CullingLodCategory, description: 'Instance grid batch size.' }),
@@ -130,7 +149,7 @@ export namespace BaseGeometry {
uClipObjectScale: ValueCell.create(clip.objects.scale),
uClipObjectTransform: ValueCell.create(clip.objects.transform),
instanceGranularity: ValueCell.create(props.instanceGranularity),
instanceGranularity: ValueCell.create(resolveInstanceGranularity(props.instanceGranularity, counts.groupCount, counts.instanceCount)),
uLod: ValueCell.create(Vec4.create(props.lod[0], props.lod[1], props.lod[2], 0)),
};
}
@@ -153,7 +172,7 @@ export namespace BaseGeometry {
ValueCell.update(values.uClipObjectScale, clip.objects.scale);
ValueCell.update(values.uClipObjectTransform, clip.objects.transform);
ValueCell.updateIfChanged(values.instanceGranularity, props.instanceGranularity);
ValueCell.updateIfChanged(values.instanceGranularity, resolveInstanceGranularity(props.instanceGranularity, values.uGroupCount.ref.value, values.instanceCount.ref.value));
ValueCell.update(values.uLod, Vec4.set(values.uLod.ref.value, props.lod[0], props.lod[1], props.lod[2], 0));
}

View File

@@ -19,7 +19,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
import { Sphere3D } from '../../../mol-math/geometry';
import { Theme } from '../../../mol-theme/theme';
import { Color } from '../../../mol-util/color';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createEmptyOverpaint } from '../overpaint-data';
import { createEmptyTransparency } from '../transparency-data';
import { hashFnv32a } from '../../../mol-data/util';
@@ -28,7 +28,9 @@ import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
import { RenderableState } from '../../../mol-gl/renderable';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
import { createEmptyWiggle } from '../wiggle-data';
import { getInteriorParam, updateInteriorValues, createInteriorValues } from '../interior';
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
export interface Cylinders {
readonly kind: 'cylinders',
@@ -180,6 +182,7 @@ export namespace Cylinders {
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
interior: getInteriorParam(),
animation: getAnimationParam(),
colorMode: PD.Select('default', PD.arrayToOptions(['default', 'interpolate'] as const), BaseGeometry.ShadingCategory)
};
export type Params = typeof Params
@@ -221,8 +224,8 @@ export namespace Cylinders {
const positionIt = createPositionIterator(cylinders, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const marker = props.instanceGranularity
const size = createSizes(locationIt, positionIt, theme.size);
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -230,6 +233,7 @@ export namespace Cylinders {
const emissive = createEmptyEmissive();
const material = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
@@ -258,6 +262,7 @@ export namespace Cylinders {
...emissive,
...material,
...clipping,
...wiggle,
...transform,
padding: ValueCell.create(padding),
@@ -272,9 +277,10 @@ export namespace Cylinders {
dSolidInterior: ValueCell.create(props.solidInterior),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
dDualColor: ValueCell.create(props.colorMode === 'interpolate'),
...createInteriorValues(props.interior),
...createAnimationValues(props.animation),
};
}
@@ -295,9 +301,9 @@ export namespace Cylinders {
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
ValueCell.updateIfChanged(values.dDualColor, props.colorMode === 'interpolate');
updateInteriorValues(values, props.interior);
updateAnimationValues(values, props.animation);
}
function updateBoundingSphere(values: CylindersValues, cylinders: Cylinders) {

View File

@@ -17,7 +17,7 @@ import { ValueCell } from '../../../mol-util';
import { Color } from '../../../mol-util/color';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Box } from '../../primitive/box';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createColors } from '../color-data';
import { GeometryUtils } from '../geometry';
import { createMarkers } from '../marker-data';
@@ -29,6 +29,7 @@ import { createEmptyClipping } from '../clipping-data';
import { Grid } from '../../../mol-model/volume';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { createEmptyWiggle } from '../wiggle-data';
const VolumeBox = Box();
@@ -227,7 +228,7 @@ export namespace DirectVolume {
const positionIt = createPositionIterator(directVolume, transform);
const color = createColors(locationIt, positionIt, theme.color);
const marker = props.instanceGranularity
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -235,6 +236,7 @@ export namespace DirectVolume {
const emissive = createEmptyEmissive();
const material = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const [x, y, z] = gridDimension.ref.value;
const counts = { drawCount: VolumeBox.indices.length, vertexCount: x * y * z, groupCount, instanceCount };
@@ -255,6 +257,7 @@ export namespace DirectVolume {
...emissive,
...material,
...clipping,
...wiggle,
...transform,
...BaseGeometry.createValues(props, counts),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -85,7 +85,7 @@ export namespace Geometry {
case 'spheres': return geometry.sphereCount * 6;
case 'cylinders': return geometry.cylinderCount * 6;
case 'text': return geometry.charCount * 4;
case 'lines': return geometry.lineCount * 4;
case 'lines': return geometry.vertexCount;
case 'direct-volume':
const [x, y, z] = geometry.gridDimension.ref.value;
return x * y * z;

View File

@@ -14,7 +14,7 @@ import { Theme } from '../../../mol-theme/theme';
import { ValueCell } from '../../../mol-util';
import { Color } from '../../../mol-util/color';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createColors } from '../color-data';
import { GeometryUtils } from '../geometry';
import { createMarkers } from '../marker-data';
@@ -28,6 +28,7 @@ import { NullLocation } from '../../../mol-model/location';
import { QuadPositions } from '../../../mol-gl/compute/util';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { createEmptyWiggle } from '../wiggle-data';
const QuadIndices = new Uint32Array([
0, 1, 2,
@@ -74,6 +75,8 @@ interface Image {
setBoundingSphere(boundingSphere: Sphere3D): void
hasBoundingSphere(): boolean
readonly meta: { [k: string]: unknown }
}
namespace Image {
@@ -136,7 +139,8 @@ namespace Image {
},
hasBoundingSphere() {
return currentHash === hashCode(image);
}
},
meta: {}
};
return image;
}
@@ -197,7 +201,7 @@ namespace Image {
const positionIt = createPositionIterator(image, transform);
const color = createColors(locationIt, positionIt, theme.color);
const marker = props.instanceGranularity
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -205,6 +209,7 @@ namespace Image {
const emissive = createEmptyEmissive();
const material = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
@@ -221,6 +226,7 @@ namespace Image {
...emissive,
...material,
...clipping,
...wiggle,
...transform,
...BaseGeometry.createValues(props, counts),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -8,6 +8,12 @@ import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
import { Color } from '../../mol-util/color/color';
import { Material } from '../../mol-util/material';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ValueCell } from '../../mol-util/value-cell';
export type InteriorData = {
uInteriorColor: ValueCell<Vec4>,
uInteriorSubstance: ValueCell<Vec4>,
}
export function getInteriorParam() {
return PD.Group({
@@ -17,23 +23,36 @@ export function getInteriorParam() {
substanceStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
});
}
export type InteriorProp = ReturnType<typeof getInteriorParam>['defaultValue'];
export type InteriorParam = ReturnType<typeof getInteriorParam>
export type InteriorProps = InteriorParam['defaultValue'];
export function areInteriorPropsEquals(a: InteriorProp, b: InteriorProp): boolean {
export function areInteriorPropsEquals(a: InteriorProps, b: InteriorProps): boolean {
return a.color === b.color
&& a.colorStrength === b.colorStrength
&& Material.areEqual(a.substance, b.substance)
&& a.substanceStrength === b.substanceStrength;
}
export function getInteriorColor(props: InteriorProp, out: Vec4): Vec4 {
export function getInteriorColor(props: InteriorProps, out: Vec4): Vec4 {
Color.toArrayNormalized(props.color, out, 0);
out[3] = props.colorStrength;
return out;
}
export function getInteriorSubstance(props: InteriorProp, out: Vec4): Vec4 {
export function getInteriorSubstance(props: InteriorProps, out: Vec4): Vec4 {
Material.toArrayNormalized(props.substance, out, 0);
out[3] = props.substanceStrength;
return out;
}
export function createInteriorValues(props: InteriorProps) {
return {
uInteriorColor: ValueCell.create(getInteriorColor(props, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props, Vec4())),
};
}
export function updateInteriorValues(values: InteriorData, props: InteriorProps) {
ValueCell.update(values.uInteriorColor, getInteriorColor(props, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props, values.uInteriorSubstance.ref.value));
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { LinesBuilder } from '../lines-builder';
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
const _start = Vec3();
const _end = Vec3();
// 8 corners of the unit cube [0,1]^3: indexed by 3 bits (x=bit0, y=bit1, z=bit2)
const _corners: Vec3[] = [];
for (let i = 0; i < 8; ++i) _corners.push(Vec3());
// 12 box edges: [startCorner, endCorner, axis]
// axis: 0=x, 1=y, 2=z — the axis along which the edge runs
const BoxEdges: [number, number, number][] = [
// X-axis edges (y,z vary)
[0, 1, 0], [2, 3, 0], [4, 5, 0], [6, 7, 0],
// Y-axis edges (x,z vary)
[0, 2, 1], [1, 3, 1], [4, 6, 1], [5, 7, 1],
// Z-axis edges (x,y vary)
[0, 4, 2], [1, 5, 2], [2, 6, 2], [3, 7, 2],
];
/**
* Add a wireframe box to a LinesBuilder.
*/
export function addBox(builder: LinesBuilder, transform: Mat4, group: number) {
// Compute 8 corners in world space from unit cube [0,1]^3
// Corner index bits: bit0=x(0/1), bit1=y(0/1), bit2=z(0/1)
for (let ci = 0; ci < 8; ++ci) {
Vec3.set(_corners[ci],
(ci & 1) ? 1 : 0,
(ci & 2) ? 1 : 0,
(ci & 4) ? 1 : 0,
);
Vec3.transformMat4(_corners[ci], _corners[ci], transform);
}
for (const [si, ei] of BoxEdges) {
Vec3.copy(_start, _corners[si]);
Vec3.copy(_end, _corners[ei]);
// Draw edge line
builder.addVec(_start, _end, group);
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { LinesBuilder } from '../lines-builder';
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
const _c0 = Vec3();
const _c1 = Vec3();
const _c2 = Vec3();
const _c3 = Vec3();
/**
* Add wireframe edges of a quad to a LinesBuilder.
*/
export function addPlane(builder: LinesBuilder, corners: ArrayLike<number>, transform: Mat4, group: number) {
Vec3.fromArray(_c0, corners, 0);
Vec3.fromArray(_c1, corners, 3);
Vec3.fromArray(_c2, corners, 6);
Vec3.fromArray(_c3, corners, 9);
Vec3.transformMat4(_c0, _c0, transform);
Vec3.transformMat4(_c1, _c1, transform);
Vec3.transformMat4(_c2, _c2, transform);
Vec3.transformMat4(_c3, _c3, transform);
builder.addVec(_c0, _c1, group);
builder.addVec(_c1, _c2, group);
builder.addVec(_c2, _c3, group);
builder.addVec(_c3, _c0, group);
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { LinesBuilder } from '../lines-builder';
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
const _p0 = Vec3();
const _p1 = Vec3();
export interface AddSphereOptions {
/** Number of segments per circle (default: 32) */
segments?: number,
/** Number of circles per dimension, evenly spaced along each axis (default: 1) */
circlesPerDimension?: number,
}
const DefaultAddSphereOptions: Required<AddSphereOptions> = {
segments: 32,
circlesPerDimension: 1,
};
/**
* Add a wireframe sphere to a LinesBuilder as orthogonal circles in XY, XZ, YZ planes.
*/
export function addSphere(builder: LinesBuilder, radius: number, transform: Mat4, group: number, options?: AddSphereOptions) {
const segments = options?.segments ?? DefaultAddSphereOptions.segments;
const circlesPerDim = options?.circlesPerDimension ?? DefaultAddSphereOptions.circlesPerDimension;
// For each dimension, draw `circlesPerDim` circles spaced evenly along the axis.
// circlesPerDim=1 → one circle at the center (offset=0)
// circlesPerDim=3 → circles at -r/2, 0, +r/2
for (let dim = 0; dim < 3; ++dim) {
for (let ci = 0; ci < circlesPerDim; ++ci) {
// offset along the perpendicular axis: evenly spaced in [-radius, radius]
const offset = circlesPerDim === 1
? 0
: -radius + (2 * radius * (ci + 1)) / (circlesPerDim + 1);
// Choose a smaller radius for offset circles (cross-section of the sphere)
const r = Math.sqrt(Math.max(0, radius * radius - offset * offset));
if (r < 1e-8) continue;
addCircle(builder, r, offset, dim, transform, group, segments);
}
}
}
function addCircle(builder: LinesBuilder, radius: number, offset: number, perpAxis: number, transform: Mat4, group: number, segments: number) {
// perpAxis: 0=X (circle in YZ), 1=Y (circle in XZ), 2=Z (circle in XY)
for (let i = 0; i < segments; ++i) {
const a0 = (i / segments) * Math.PI * 2;
const a1 = ((i + 1) / segments) * Math.PI * 2;
const cos0 = Math.cos(a0) * radius;
const sin0 = Math.sin(a0) * radius;
const cos1 = Math.cos(a1) * radius;
const sin1 = Math.sin(a1) * radius;
if (perpAxis === 2) {
// XY circle at z=offset
Vec3.set(_p0, cos0, sin0, offset);
Vec3.set(_p1, cos1, sin1, offset);
} else if (perpAxis === 1) {
// XZ circle at y=offset
Vec3.set(_p0, cos0, offset, sin0);
Vec3.set(_p1, cos1, offset, sin1);
} else {
// YZ circle at x=offset
Vec3.set(_p0, offset, cos0, sin0);
Vec3.set(_p1, offset, cos1, sin1);
}
Vec3.transformMat4(_p0, _p0, transform);
Vec3.transformMat4(_p1, _p1, transform);
builder.addVec(_p0, _p1, group);
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -10,6 +10,15 @@ import { Lines } from './lines';
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
import { Cage } from '../../primitive/cage';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const caAdd = ChunkedArray.add;
const caAdd2 = ChunkedArray.add2;
const caAdd3 = ChunkedArray.add3;
const tmpVecA = Vec3();
const tmpVecB = Vec3();
const tmpDir = Vec3();
export interface LinesBuilder {
add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number): void
addVec(start: Vec3, end: Vec3, group: number): void
@@ -19,14 +28,6 @@ export interface LinesBuilder {
getLines(): Lines
}
const tmpVecA = Vec3();
const tmpVecB = Vec3();
const tmpDir = Vec3();
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const caAdd = ChunkedArray.add;
const caAdd3 = ChunkedArray.add3;
export namespace LinesBuilder {
export function create(initialCount = 2048, chunkSize = 1024, lines?: Lines): LinesBuilder {
const groups = ChunkedArray.create(Float32Array, 1, chunkSize, lines ? lines.groupBuffer.ref.value : initialCount);
@@ -89,13 +90,15 @@ export namespace LinesBuilder {
},
getLines: () => {
const lineCount = groups.elementCount / 4;
const vertexCount = groups.elementCount;
const gb = ChunkedArray.compact(groups, true) as Float32Array;
const sb = ChunkedArray.compact(starts, true) as Float32Array;
const eb = ChunkedArray.compact(ends, true) as Float32Array;
const mb = lines && lineCount <= lines.lineCount ? lines.mappingBuffer.ref.value : new Float32Array(lineCount * 8);
const ib = lines && lineCount <= lines.lineCount ? lines.indexBuffer.ref.value : new Uint32Array(lineCount * 6);
if (!lines || lineCount > lines.lineCount) fillMappingAndIndices(lineCount, mb, ib);
return Lines.create(mb, ib, gb, sb, eb, lineCount, lines);
const mb = lines && lineCount <= lines.lineCount && lines.stripCount.ref.value === 0 ? lines.mappingBuffer.ref.value : new Float32Array(lineCount * 8);
const ib = lines && lineCount <= lines.lineCount && lines.stripCount.ref.value === 0 ? lines.indexBuffer.ref.value : new Uint32Array(lineCount * 6);
const ob = lines ? lines.stripBuffer.ref.value : new Uint32Array(0);
if (!lines || lineCount > lines.lineCount || lines.stripCount.ref.value > 0) fillMappingAndIndices(lineCount, mb, ib);
return Lines.create(mb, ib, gb, sb, eb, ob, lineCount, vertexCount, 0, lines);
}
};
}
@@ -117,3 +120,104 @@ function fillMappingAndIndices(n: number, mb: Float32Array, ib: Uint32Array) {
ib[io + 3] = o + 1; ib[io + 4] = o + 3; ib[io + 5] = o + 2;
}
}
//
export interface StripLinesBuilder {
start(group: number): void
add(x: number, y: number, z: number): void
addVec(v: Vec3): void
end(): void
getLines(): Lines
}
export namespace StripLinesBuilder {
export function create(initialCount = 2048, chunkSize = 1024, lines?: Lines): StripLinesBuilder {
const groups = ChunkedArray.create(Float32Array, 1, chunkSize, lines ? lines.groupBuffer.ref.value : initialCount);
const starts = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.startBuffer.ref.value : initialCount);
const ends = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.endBuffer.ref.value : initialCount);
const mapping = ChunkedArray.create(Float32Array, 2, chunkSize, lines ? lines.mappingBuffer.ref.value : initialCount);
const indices = ChunkedArray.create(Uint32Array, 3, chunkSize, lines ? lines.indexBuffer.ref.value : initialCount);
const strips = ChunkedArray.create(Uint32Array, 1, chunkSize, lines ? lines.stripBuffer.ref.value : initialCount);
let stripGroup = 0;
let pointCount = 0;
let firstVertexOffset = 0;
let prevX = 0, prevY = 0, prevZ = 0;
const addPoint = (x: number, y: number, z: number) => {
if (pointCount === 0) {
firstVertexOffset = groups.elementCount;
prevX = x; prevY = y; prevZ = z;
pointCount = 1;
return;
}
const vertexOffset = groups.elementCount;
if (pointCount === 1) {
caAdd3(starts, prevX, prevY, prevZ);
caAdd3(ends, x, y, z);
caAdd(groups, stripGroup);
caAdd2(mapping, -1, -1); // left, start
caAdd3(starts, prevX, prevY, prevZ);
caAdd3(ends, x, y, z);
caAdd(groups, stripGroup);
caAdd2(mapping, 1, -1); // right, start
}
caAdd3(starts, prevX, prevY, prevZ);
caAdd3(ends, x, y, z);
caAdd(groups, stripGroup);
caAdd2(mapping, -1, 1); // left, end
caAdd3(starts, prevX, prevY, prevZ);
caAdd3(ends, x, y, z);
caAdd(groups, stripGroup);
caAdd2(mapping, 1, 1); // right, end
const prevOffset = pointCount === 1 ? firstVertexOffset : vertexOffset - 2;
const currOffset = pointCount === 1 ? vertexOffset + 2 : vertexOffset;
// Triangle 1: prev-left, prev-right, curr-left
caAdd3(indices, prevOffset, prevOffset + 1, currOffset);
// Triangle 2: prev-right, curr-right, curr-left
caAdd3(indices, prevOffset + 1, currOffset + 1, currOffset);
prevX = x; prevY = y; prevZ = z;
pointCount++;
};
return {
start: (group: number) => {
stripGroup = group;
pointCount = 0;
if (strips.elementCount === 0) {
caAdd(strips, 0);
}
},
add: (x: number, y: number, z: number) => {
addPoint(x, y, z);
},
addVec: (v: Vec3) => {
addPoint(v[0], v[1], v[2]);
},
end: () => {
pointCount = 0;
caAdd(strips, groups.elementCount);
},
getLines: () => {
const lineCount = indices.elementCount / 2;
const vertexCount = groups.elementCount;
const stripCount = strips.elementCount - 1;
const gb = ChunkedArray.compact(groups, true) as Float32Array;
const sb = ChunkedArray.compact(starts, true) as Float32Array;
const eb = ChunkedArray.compact(ends, true) as Float32Array;
const mb = ChunkedArray.compact(mapping, true) as Float32Array;
const ib = ChunkedArray.compact(indices, true) as Uint32Array;
const ob = ChunkedArray.compact(strips, true) as Uint32Array;
return Lines.create(mb, ib, gb, sb, eb, ob, lineCount, vertexCount, stripCount, lines);
}
};
}
}

View File

@@ -21,13 +21,15 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
import { Sphere3D } from '../../../mol-math/geometry';
import { Theme } from '../../../mol-theme/theme';
import { Color } from '../../../mol-util/color';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createEmptyOverpaint } from '../overpaint-data';
import { createEmptyTransparency } from '../transparency-data';
import { hashFnv32a } from '../../../mol-data/util';
import { createEmptyClipping } from '../clipping-data';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { createEmptyWiggle } from '../wiggle-data';
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
/** Wide line */
export interface Lines {
@@ -35,6 +37,8 @@ export interface Lines {
/** Number of lines */
lineCount: number,
/** Number of vertices */
vertexCount: number,
/** Mapping buffer as array of xy values wrapped in a value cell */
readonly mappingBuffer: ValueCell<Float32Array>,
@@ -47,6 +51,11 @@ export interface Lines {
/** Line end buffer as array of xyz values wrapped in a value cell */
readonly endBuffer: ValueCell<Float32Array>,
/** Number of strips wrapped in a value cell */
readonly stripCount: ValueCell<number>,
/** Strip buffer as array of vertex offsets wrapped in a value cell */
readonly stripBuffer: ValueCell<Uint32Array>,
/** Bounding sphere of the lines */
readonly boundingSphere: Sphere3D
/** Maps group ids to line indices */
@@ -57,10 +66,10 @@ export interface Lines {
}
export namespace Lines {
export function create(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number, lines?: Lines): Lines {
export function create(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, strips: Uint32Array, lineCount: number, vertexCount: number, stripCount: number, lines?: Lines): Lines {
return lines ?
update(mappings, indices, groups, starts, ends, lineCount, lines) :
fromArrays(mappings, indices, groups, starts, ends, lineCount);
update(mappings, indices, groups, starts, ends, strips, lineCount, vertexCount, stripCount, lines) :
fromArrays(mappings, indices, groups, starts, ends, strips, lineCount, vertexCount, stripCount);
}
export function createEmpty(lines?: Lines): Lines {
@@ -69,7 +78,8 @@ export namespace Lines {
const gb = lines ? lines.groupBuffer.ref.value : new Float32Array(0);
const sb = lines ? lines.startBuffer.ref.value : new Float32Array(0);
const eb = lines ? lines.endBuffer.ref.value : new Float32Array(0);
return create(mb, ib, gb, sb, eb, 0, lines);
const ob = lines ? lines.stripBuffer.ref.value : new Uint32Array(0);
return create(mb, ib, gb, sb, eb, ob, 0, 0, 0, lines);
}
export function fromMesh(mesh: Mesh, lines?: Lines) {
@@ -95,12 +105,14 @@ export namespace Lines {
function hashCode(lines: Lines) {
return hashFnv32a([
lines.lineCount, lines.mappingBuffer.ref.version, lines.indexBuffer.ref.version,
lines.groupBuffer.ref.version, lines.startBuffer.ref.version, lines.endBuffer.ref.version
lines.lineCount, lines.vertexCount,
lines.mappingBuffer.ref.version, lines.indexBuffer.ref.version,
lines.groupBuffer.ref.version, lines.startBuffer.ref.version, lines.endBuffer.ref.version,
lines.stripCount.ref.version, lines.stripBuffer.ref.version
]);
}
function fromArrays(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number): Lines {
function fromArrays(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, strips: Uint32Array, lineCount: number, vertexCount: number, stripCount: number): Lines {
const boundingSphere = Sphere3D();
let groupMapping: GroupMapping;
@@ -111,11 +123,14 @@ export namespace Lines {
const lines = {
kind: 'lines' as const,
lineCount,
vertexCount,
mappingBuffer: ValueCell.create(mappings),
indexBuffer: ValueCell.create(indices),
groupBuffer: ValueCell.create(groups),
startBuffer: ValueCell.create(starts),
endBuffer: ValueCell.create(ends),
stripCount: ValueCell.create(stripCount),
stripBuffer: ValueCell.create(strips),
get boundingSphere() {
const newHash = hashCode(lines);
if (newHash !== currentHash) {
@@ -145,24 +160,27 @@ export namespace Lines {
return lines;
}
function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number, lines: Lines) {
if (lineCount > lines.lineCount) {
function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, strips: Uint32Array, lineCount: number, vertexCount: number, stripCount: number, lines: Lines) {
if (lineCount > lines.lineCount || stripCount !== lines.stripCount.ref.value || stripCount > 0) {
ValueCell.update(lines.mappingBuffer, mappings);
ValueCell.update(lines.indexBuffer, indices);
}
lines.lineCount = lineCount;
lines.vertexCount = vertexCount;
ValueCell.update(lines.groupBuffer, groups);
ValueCell.update(lines.startBuffer, starts);
ValueCell.update(lines.endBuffer, ends);
ValueCell.updateIfChanged(lines.stripCount, stripCount);
ValueCell.update(lines.stripBuffer, strips);
return lines;
}
export function transform(lines: Lines, t: Mat4) {
const start = lines.startBuffer.ref.value;
transformPositionArray(t, start, 0, lines.lineCount * 4);
transformPositionArray(t, start, 0, lines.vertexCount);
ValueCell.update(lines.startBuffer, start);
const end = lines.endBuffer.ref.value;
transformPositionArray(t, end, 0, lines.lineCount * 4);
transformPositionArray(t, end, 0, lines.vertexCount);
ValueCell.update(lines.endBuffer, end);
}
@@ -172,6 +190,7 @@ export namespace Lines {
...BaseGeometry.Params,
sizeFactor: PD.Numeric(2, { min: 0, max: 10, step: 0.1 }),
lineSizeAttenuation: PD.Boolean(false),
animation: getAnimationParam(),
};
export type Params = typeof Params
@@ -212,8 +231,8 @@ export namespace Lines {
const positionIt = createPositionIterator(lines, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const marker = props.instanceGranularity
const size = createSizes(locationIt, positionIt, theme.size);
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -221,8 +240,9 @@ export namespace Lines {
const emissive = createEmptyEmissive();
const material = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.lineCount * 4, groupCount, instanceCount };
const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.vertexCount, groupCount, instanceCount };
const invariantBoundingSphere = Sphere3D.clone(lines.boundingSphere);
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount, 0);
@@ -246,6 +266,7 @@ export namespace Lines {
...emissive,
...material,
...clipping,
...wiggle,
...transform,
...BaseGeometry.createValues(props, counts),
@@ -253,6 +274,10 @@ export namespace Lines {
dLineSizeAttenuation: ValueCell.create(props.lineSizeAttenuation),
uDoubleSided: ValueCell.create(true),
dFlipSided: ValueCell.create(false),
...createAnimationValues(props.animation),
stripCount: lines.stripCount,
stripOffsets: lines.stripBuffer,
};
}
@@ -266,6 +291,7 @@ export namespace Lines {
BaseGeometry.updateValues(values, props);
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
ValueCell.updateIfChanged(values.dLineSizeAttenuation, props.lineSizeAttenuation);
updateAnimationValues(values, props.animation);
}
function updateBoundingSphere(values: LinesValues, lines: Lines) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -29,8 +29,14 @@ interface ColorSmoothingInput {
itemSize: 4 | 3 | 1
}
export function calcMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl?: WebGLContext, texture?: Texture) {
export type ColorSmoothingOptions = {
resolution: number,
stride: number
};
export function calcMeshColorSmoothing(input: ColorSmoothingInput, options: ColorSmoothingOptions, webgl?: WebGLContext, texture?: Texture) {
const { colorType, vertexCount, groupCount, positionBuffer, instanceBuffer, transformBuffer, groupBuffer, itemSize } = input;
const { resolution, stride } = options;
const isInstanceType = colorType.endsWith('Instance');
const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
@@ -70,7 +76,7 @@ export function calcMeshColorSmoothing(input: ColorSmoothingInput, resolution: n
for (let i = 0; i < instanceCount; ++i) {
// - use reordered index for access from GPU
// - use serial index for access from CPU
const instanceIndex = webgl ? instanceBuffer[i] : i;
const instanceIndex = (webgl && isInstanceType) ? instanceBuffer[i] : i;
for (let j = 0; j < vertexCount; j += stride) {
Vec3.fromArray(v, positionBuffer, j * 3);
if (isInstanceType) Vec3.transformMat4Offset(v, v, transformBuffer, 0, 0, i * 16);
@@ -262,7 +268,7 @@ function isSupportedColorType(x: string): x is 'group' | 'groupInstance' {
return x === 'group' || x === 'groupInstance';
}
export function applyMeshColorSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
export function applyMeshColorSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
if (!isSupportedColorType(values.dColorType.ref.value)) return;
const smoothingData = calcMeshColorSmoothing({
@@ -278,7 +284,7 @@ export function applyMeshColorSmoothing(values: MeshValues, resolution: number,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
itemSize: 3
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
if (smoothingData.kind === 'volume') {
ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
@@ -297,7 +303,7 @@ function isSupportedOverpaintType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyMeshOverpaintSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
export function applyMeshOverpaintSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
if (!isSupportedOverpaintType(values.dOverpaintType.ref.value)) return;
const smoothingData = calcMeshColorSmoothing({
@@ -313,7 +319,7 @@ export function applyMeshOverpaintSmoothing(values: MeshValues, resolution: numb
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
itemSize: 4
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
if (smoothingData.kind === 'volume') {
ValueCell.updateIfChanged(values.dOverpaintType, smoothingData.type);
ValueCell.update(values.tOverpaintGrid, smoothingData.texture);
@@ -331,7 +337,7 @@ function isSupportedTransparencyType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyMeshTransparencySmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
export function applyMeshTransparencySmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
if (!isSupportedTransparencyType(values.dTransparencyType.ref.value)) return;
const smoothingData = calcMeshColorSmoothing({
@@ -347,7 +353,7 @@ export function applyMeshTransparencySmoothing(values: MeshValues, resolution: n
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
itemSize: 1
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
if (smoothingData.kind === 'volume') {
ValueCell.updateIfChanged(values.dTransparencyType, smoothingData.type);
ValueCell.update(values.tTransparencyGrid, smoothingData.texture);
@@ -365,7 +371,7 @@ function isSupportedEmissiveType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyMeshEmissiveSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
export function applyMeshEmissiveSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
if (!isSupportedEmissiveType(values.dEmissiveType.ref.value)) return;
const smoothingData = calcMeshColorSmoothing({
@@ -381,7 +387,7 @@ export function applyMeshEmissiveSmoothing(values: MeshValues, resolution: numbe
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
itemSize: 1
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
if (smoothingData.kind === 'volume') {
ValueCell.updateIfChanged(values.dEmissiveType, smoothingData.type);
ValueCell.update(values.tEmissiveGrid, smoothingData.texture);
@@ -399,7 +405,7 @@ function isSupportedSubstanceType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyMeshSubstanceSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
export function applyMeshSubstanceSmoothing(values: MeshValues, options: ColorSmoothingOptions, webgl?: WebGLContext, colorTexture?: Texture) {
if (!isSupportedSubstanceType(values.dSubstanceType.ref.value)) return;
const smoothingData = calcMeshColorSmoothing({
@@ -415,7 +421,7 @@ export function applyMeshSubstanceSmoothing(values: MeshValues, resolution: numb
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
itemSize: 4
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
if (smoothingData.kind === 'volume') {
ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
ValueCell.update(values.tSubstanceGrid, smoothingData.texture);

View File

@@ -20,7 +20,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
import { Theme } from '../../../mol-theme/theme';
import { MeshValues } from '../../../mol-gl/renderable/mesh';
import { Color } from '../../../mol-util/color';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createEmptyOverpaint } from '../overpaint-data';
import { createEmptyTransparency } from '../transparency-data';
import { createEmptyClipping } from '../clipping-data';
@@ -29,7 +29,9 @@ import { arraySetAdd } from '../../../mol-util/array';
import { degToRad } from '../../../mol-math/misc';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
import { createEmptyWiggle } from '../wiggle-data';
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
export interface Mesh {
readonly kind: 'mesh',
@@ -639,6 +641,7 @@ export namespace Mesh {
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
interior: getInteriorParam(),
animation: getAnimationParam(),
};
export type Params = typeof Params
@@ -681,7 +684,7 @@ export namespace Mesh {
const positionIt = createPositionIterator(mesh, transform);
const color = createColors(locationIt, positionIt, theme.color);
const marker = props.instanceGranularity
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -689,6 +692,7 @@ export namespace Mesh {
const emissive = createEmptyEmissive();
const material = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: mesh.triangleCount * 3, vertexCount: mesh.vertexCount, groupCount, instanceCount };
@@ -713,6 +717,7 @@ export namespace Mesh {
...emissive,
...material,
...clipping,
...wiggle,
...transform,
...BaseGeometry.createValues(props, counts),
@@ -725,10 +730,11 @@ export namespace Mesh {
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
meta: ValueCell.create(mesh.meta),
...createInteriorValues(props.interior),
...createAnimationValues(props.animation),
};
}
@@ -749,8 +755,8 @@ export namespace Mesh {
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
updateInteriorValues(values, props.interior);
updateAnimationValues(values, props.animation);
}
function updateBoundingSphere(values: MeshValues, mesh: Mesh) {

View File

@@ -20,13 +20,15 @@ import { Theme } from '../../../mol-theme/theme';
import { PointsValues } from '../../../mol-gl/renderable/points';
import { RenderableState } from '../../../mol-gl/renderable';
import { Color } from '../../../mol-util/color';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createEmptyOverpaint } from '../overpaint-data';
import { createEmptyTransparency } from '../transparency-data';
import { hashFnv32a } from '../../../mol-data/util';
import { createEmptyClipping } from '../clipping-data';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { createEmptyWiggle } from '../wiggle-data';
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
/** Point cloud */
export interface Points {
@@ -136,6 +138,7 @@ export namespace Points {
sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
pointSizeAttenuation: PD.Boolean(false),
pointStyle: PD.Select('square', PD.objectToOptions(StyleTypes)),
animation: getAnimationParam(),
};
export type Params = typeof Params
@@ -174,8 +177,8 @@ export namespace Points {
const positionIt = createPositionIterator(points, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const marker = props.instanceGranularity
const size = createSizes(locationIt, positionIt, theme.size);
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -183,6 +186,7 @@ export namespace Points {
const emissive = createEmptyEmissive();
const material = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: points.pointCount, vertexCount: points.pointCount, groupCount, instanceCount };
@@ -205,12 +209,14 @@ export namespace Points {
...emissive,
...material,
...clipping,
...wiggle,
...transform,
...BaseGeometry.createValues(props, counts),
uSizeFactor: ValueCell.create(props.sizeFactor),
dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation),
dPointStyle: ValueCell.create(props.pointStyle),
...createAnimationValues(props.animation),
};
}
@@ -225,6 +231,7 @@ export namespace Points {
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation);
ValueCell.updateIfChanged(values.dPointStyle, props.pointStyle);
updateAnimationValues(values, props.animation);
}
function updateBoundingSphere(values: PointsValues, points: Points) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -13,7 +13,7 @@ import { SizeTheme } from '../../mol-theme/size';
import { Geometry } from './geometry';
import { unpackRGBToInt, packIntToRGBArray } from '../../mol-util/number-packing';
export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance'
export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 'vertex' | 'vertexInstance';
export type SizeData = {
uSize: ValueCell<number>,
@@ -22,12 +22,14 @@ export type SizeData = {
dSizeType: ValueCell<string>,
}
export function createSizes(locationIt: LocationIterator, sizeTheme: SizeTheme<any>, sizeData?: SizeData): SizeData {
export function createSizes(locationIt: LocationIterator, positionIt: LocationIterator, sizeTheme: SizeTheme<any>, sizeData?: SizeData): SizeData {
switch (Geometry.getGranularity(locationIt, sizeTheme.granularity)) {
case 'uniform': return createUniformSize(locationIt, sizeTheme.size, sizeData);
case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData);
case 'group': return createGroupSize(locationIt, sizeTheme.size, sizeData);
case 'groupInstance': return createGroupInstanceSize(locationIt, sizeTheme.size, sizeData);
case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData);
case 'vertex': return createVertexSize(positionIt, sizeTheme.size, sizeData);
case 'vertexInstance': return createVertexInstanceSize(positionIt, sizeTheme.size, sizeData);
}
}
@@ -41,6 +43,8 @@ export function getMaxSize(sizeData: SizeData): number {
case 'instance':
case 'group':
case 'groupInstance':
case 'vertex':
case 'vertexInstance':
let maxSize = 0;
const array = sizeData.tSize.ref.value.array;
for (let i = 0, il = array.length; i < il; i += 3) {
@@ -134,4 +138,29 @@ export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: Lo
packIntToRGBArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.index * 3);
}
return createTextureSize(sizes, 'groupInstance', sizeData);
}
}
/** Creates size texture with size for each vertex */
export function createVertexSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
const { groupCount } = locationIt;
const sizes = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
locationIt.reset();
while (locationIt.hasNext) {
const v = locationIt.move();
packIntToRGBArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.index * 3);
}
return createTextureSize(sizes, 'vertex', sizeData);
}
/** Creates size texture with size for each vertex instance */
export function createVertexInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
const { groupCount, instanceCount } = locationIt;
const count = instanceCount * groupCount;
const sizes = createTextureImage(Math.max(1, count), 3, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
locationIt.reset();
while (locationIt.hasNext) {
const v = locationIt.move();
packIntToRGBArray(sizeFn(v.location) * sizeDataFactor, sizes.array, v.index * 3);
}
return createTextureSize(sizes, 'vertexInstance', sizeData);
}

View File

@@ -17,7 +17,7 @@ import { TextureImage, calculateInvariantBoundingSphere, calculateTransformBound
import { Sphere3D } from '../../../mol-math/geometry';
import { createSizes, getMaxSize } from '../size-data';
import { Color } from '../../../mol-util/color';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createEmptyOverpaint } from '../overpaint-data';
import { createEmptyTransparency } from '../transparency-data';
import { hashFnv32a } from '../../../mol-data/util';
@@ -27,7 +27,9 @@ import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
import { RenderableState } from '../../../mol-gl/renderable';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
import { createEmptyWiggle } from '../wiggle-data';
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
export interface Spheres {
readonly kind: 'spheres',
@@ -247,6 +249,33 @@ export namespace Spheres {
return lodLevels.map(l => getAdjustedStride(l, sizeFactor)).reverse();
}
export const LodLevelsPresets: { [key in 'performance' | 'balanced' | 'quality' | 'ultra']: LodLevels } = {
performance: [
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
],
balanced: [
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
],
quality: [
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
],
ultra: [
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
],
};
export const Params = {
...BaseGeometry.Params,
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
@@ -262,6 +291,7 @@ export namespace Spheres {
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
interior: getInteriorParam(),
animation: getAnimationParam(),
lodLevels: PD.ObjectList({
minDistance: PD.Numeric(0),
maxDistance: PD.Numeric(0),
@@ -270,7 +300,8 @@ export namespace Spheres {
scaleBias: PD.Numeric(3, { min: 0.1, max: 10, step: 0.1 }),
}, o => `${o.stride}`, {
...BaseGeometry.CullingLodCategory,
defaultValue: [] as LodLevels
defaultValue: [] as LodLevels,
presets: Object.entries(LodLevelsPresets).map(([k, v]) => [v, k])
})
};
export type Params = typeof Params
@@ -310,8 +341,8 @@ export namespace Spheres {
const positionIt = createPositionIterator(spheres, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const marker = props.instanceGranularity
const size = createSizes(locationIt, positionIt, theme.size);
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -319,6 +350,7 @@ export namespace Spheres {
const emissive = createEmptyEmissive();
const material = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: spheres.sphereCount * 2 * 3, vertexCount: spheres.sphereCount * 6, groupCount, instanceCount };
@@ -345,6 +377,7 @@ export namespace Spheres {
...emissive,
...material,
...clipping,
...wiggle,
...transform,
padding: ValueCell.create(padding),
@@ -362,12 +395,13 @@ export namespace Spheres {
uAlphaThickness: ValueCell.create(props.alphaThickness),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
lodLevels: spheres.shaderData.lodLevels,
centerBuffer: spheres.centerBuffer,
groupBuffer: spheres.groupBuffer,
...createInteriorValues(props.interior),
...createAnimationValues(props.animation),
};
}
@@ -391,8 +425,8 @@ export namespace Spheres {
ValueCell.updateIfChanged(values.uAlphaThickness, props.alphaThickness);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
updateInteriorValues(values, props.interior);
updateAnimationValues(values, props.animation);
const lodLevels = getLodLevels(values.lodLevels.ref.value as LodLevelsValue);
if (!areLodLevelsEqual(props.lodLevels, lodLevels)) {

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -76,7 +77,7 @@ export class FontAtlas {
this.props = p;
// create measurements
const fontSize = 32 * (p.fontQuality + 1);
const fontSize = 64 * (p.fontQuality + 1);
this.buffer = fontSize / 8;
this.radius = fontSize / 3;
this.lineHeight = Math.round(fontSize + 2 * this.buffer + this.radius);

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 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 { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -118,8 +119,9 @@ export namespace TextBuilder {
const xLeft = (-xShift - margin - 0.1) * scale;
const xRight = (bWidth - xShift + margin + 0.1) * scale;
const yTop = (bHeight - yShift + margin) * scale;
const yBottom = (-yShift - margin) * scale;
const yMid = 0.5 - yShift - outline * 0.5; // glyph vertical midpoint accounting for outline offset
const yTop = (yMid + bHeight / 2 + margin) * scale;
const yBottom = (yMid - bHeight / 2 - margin) * scale;
// background
if (background) {

View File

@@ -3,6 +3,7 @@
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -24,7 +25,7 @@ import { FontAtlasParams } from './font-atlas';
import { RenderableState } from '../../../mol-gl/renderable';
import { clamp } from '../../../mol-math/interpolate';
import { createRenderObject as _createRenderObject } from '../../../mol-gl/render-object';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createEmptyOverpaint } from '../overpaint-data';
import { createEmptyTransparency } from '../transparency-data';
import { hashFnv32a } from '../../../mol-data/util';
@@ -32,6 +33,7 @@ import { GroupMapping, createGroupMapping } from '../../util';
import { createEmptyClipping } from '../clipping-data';
import { createEmptySubstance } from '../substance-data';
import { createEmptyEmissive } from '../emissive-data';
import { createEmptyWiggle } from '../wiggle-data';
type TextAttachment = (
'bottom-left' | 'bottom-center' | 'bottom-right' |
@@ -216,8 +218,8 @@ export namespace Text {
const positionIt = createPositionIterator(text, transform);
const color = createColors(locationIt, positionIt, theme.color);
const size = createSizes(locationIt, theme.size);
const marker = props.instanceGranularity
const size = createSizes(locationIt, positionIt, theme.size);
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -225,6 +227,7 @@ export namespace Text {
const emissive = createEmptyEmissive();
const substance = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: text.charCount * 2 * 3, vertexCount: text.charCount * 4, groupCount, instanceCount };
@@ -252,6 +255,7 @@ export namespace Text {
...emissive,
...substance,
...clipping,
...wiggle,
...transform,
aTexCoord: text.tcoordBuffer,
@@ -327,16 +331,17 @@ export namespace Text {
}
function getPadding(mappings: Float32Array, depths: Float32Array, charCount: number, scale: number) {
let maxOffset = 0;
let maxOffsetX = 0;
let maxOffsetY = 0;
let maxDepth = 0;
for (let i = 0, il = charCount * 4; i < il; ++i) {
const i2 = 2 * i;
const ox = Math.abs(mappings[i2]);
if (ox > maxOffset) maxOffset = ox;
if (ox > maxOffsetX) maxOffsetX = ox;
const oy = Math.abs(mappings[i2 + 1]);
if (oy > maxOffset) maxOffset = oy;
if (oy > maxOffsetY) maxOffsetY = oy;
const d = Math.abs(depths[i]);
if (d > maxDepth) maxDepth = d;
}
return Math.max(maxDepth, scale * maxOffset);
return Math.max(maxDepth, scale * Math.sqrt(maxOffsetX * maxOffsetX + maxOffsetY * maxOffsetY));
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -14,7 +14,7 @@ import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, Values, DefineSpec
import { quad_vert } from '../../../mol-gl/shader/quad.vert';
import { normalize_frag } from '../../../mol-gl/shader/compute/color-smoothing/normalize.frag';
import { QuadSchema, QuadValues } from '../../../mol-gl/compute/util';
import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
import { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
import { accumulate_frag } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.frag';
import { accumulate_vert } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.vert';
@@ -246,18 +246,30 @@ interface ColorSmoothingInput extends AccumulateInput {
invariantBoundingSphere: Sphere3D
}
export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl: WebGLContext, texture?: Texture) {
export type ColorSmoothingOptions = {
resolution: number,
stride: number
};
export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, options: ColorSmoothingOptions, webgl: WebGLContext, texture?: Texture) {
const { drawBuffers } = webgl.extensions;
if (!drawBuffers) throw new Error('need WebGL draw buffers');
if (isTimingMode) webgl.timer.mark('calcTextureMeshColorSmoothing');
const { gl, resources, state, extensions: { colorBufferHalfFloat, textureHalfFloat } } = webgl;
const { resolution, stride } = options;
const isInstanceType = input.colorType.endsWith('Instance');
const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
const pad = 1 + resolution;
const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
if (!isInstanceType) {
input.instanceCount = 1;
input.instanceBuffer = new Float32Array([0]);
input.transformBuffer = new Float32Array(Mat4.id);
}
const scaleFactor = 1 / resolution;
const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor);
const gridDim = Box3D.size(Vec3(), scaledBox);
@@ -389,7 +401,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
const type = isInstanceType ? 'volumeInstance' : 'volume';
if (isTimingMode) webgl.timer.markEnd('calcTextureMeshColorSmoothing');
// printTextureImage(readTexture(webgl, texture), { scale: 0.75 });
// printTextureImage(readTexture(webgl, texture), { scale: 0.75, id: `${texture.id}` });
return { texture, gridDim, gridTexDim: Vec2.create(width, height), gridTransform, type };
}
@@ -404,10 +416,10 @@ function isSupportedColorType(x: string): x is 'group' | 'groupInstance' {
return x === 'group' || x === 'groupInstance';
}
export function applyTextureMeshColorSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshColorSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedColorType(values.dColorType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingRgbName]) {
webgl.namedTextures[ColorSmoothingRgbName] = webgl.resources.texture('image-uint8', 'rgb', 'ubyte', 'nearest');
@@ -427,7 +439,7 @@ export function applyTextureMeshColorSmoothing(values: TextureMeshValues, resolu
colorType: values.dColorType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
ValueCell.update(values.tColorGrid, smoothingData.texture);
@@ -440,10 +452,10 @@ function isSupportedOverpaintType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedOverpaintType(values.dOverpaintType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingRgbaName]) {
webgl.namedTextures[ColorSmoothingRgbaName] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
@@ -463,7 +475,7 @@ export function applyTextureMeshOverpaintSmoothing(values: TextureMeshValues, re
colorType: values.dOverpaintType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dOverpaintType, smoothingData.type);
ValueCell.update(values.tOverpaintGrid, smoothingData.texture);
@@ -476,10 +488,10 @@ function isSupportedTransparencyType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedTransparencyType(values.dTransparencyType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingAlphaName]) {
webgl.namedTextures[ColorSmoothingAlphaName] = webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'nearest');
@@ -499,7 +511,7 @@ export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues,
colorType: values.dTransparencyType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dTransparencyType, smoothingData.type);
ValueCell.update(values.tTransparencyGrid, smoothingData.texture);
@@ -512,10 +524,10 @@ function isSupportedEmissiveType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedEmissiveType(values.dEmissiveType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingAlphaName]) {
webgl.namedTextures[ColorSmoothingAlphaName] = webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'nearest');
@@ -535,7 +547,7 @@ export function applyTextureMeshEmissiveSmoothing(values: TextureMeshValues, res
colorType: values.dEmissiveType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dEmissiveType, smoothingData.type);
ValueCell.update(values.tEmissiveGrid, smoothingData.texture);
@@ -548,10 +560,10 @@ function isSupportedSubstanceType(x: string): x is 'groupInstance' {
return x === 'groupInstance';
}
export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, options: ColorSmoothingOptions, webgl: WebGLContext, colorTexture?: Texture) {
if (!isSupportedSubstanceType(values.dSubstanceType.ref.value)) return;
stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
options = { ...options, stride: options.stride * 3 }; // triple because TextureMesh is never indexed (no elements buffer)
if (!webgl.namedTextures[ColorSmoothingRgbaName]) {
webgl.namedTextures[ColorSmoothingRgbaName] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
@@ -571,7 +583,7 @@ export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, re
colorType: values.dSubstanceType.ref.value,
boundingSphere: values.boundingSphere.ref.value,
invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
}, resolution, stride, webgl, colorTexture);
}, options, webgl, colorTexture);
ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
ValueCell.update(values.tSubstanceGrid, smoothingData.texture);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Cai Huiyu <szmun.caihy@gmail.com>
@@ -15,7 +15,7 @@ import { createMarkers } from '../marker-data';
import { GeometryUtils } from '../geometry';
import { Theme } from '../../../mol-theme/theme';
import { Color } from '../../../mol-util/color';
import { BaseGeometry } from '../base';
import { BaseGeometry, resolveInstanceGranularity } from '../base';
import { createEmptyOverpaint } from '../overpaint-data';
import { createEmptyTransparency } from '../transparency-data';
import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
@@ -28,7 +28,9 @@ import { createEmptySubstance } from '../substance-data';
import { RenderableState } from '../../../mol-gl/renderable';
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { createEmptyEmissive } from '../emissive-data';
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
import { createEmptyWiggle } from '../wiggle-data';
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
export interface TextureMesh {
readonly kind: 'texture-mesh',
@@ -130,6 +132,7 @@ export namespace TextureMesh {
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
interior: getInteriorParam(),
animation: getAnimationParam(),
};
export type Params = typeof Params
@@ -200,7 +203,7 @@ export namespace TextureMesh {
const positionIt = Utils.createPositionIterator(textureMesh, transform);
const color = createColors(locationIt, positionIt, theme.color);
const marker = props.instanceGranularity
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
? createMarkers(instanceCount, 'instance')
: createMarkers(instanceCount * groupCount, 'groupInstance');
const overpaint = createEmptyOverpaint();
@@ -208,6 +211,7 @@ export namespace TextureMesh {
const emissive = createEmptyEmissive();
const substance = createEmptySubstance();
const clipping = createEmptyClipping();
const wiggle = createEmptyWiggle();
const counts = { drawCount: textureMesh.vertexCount, vertexCount: textureMesh.vertexCount, groupCount, instanceCount };
@@ -234,6 +238,7 @@ export namespace TextureMesh {
...emissive,
...substance,
...clipping,
...wiggle,
...transform,
...BaseGeometry.createValues(props, counts),
@@ -246,10 +251,11 @@ export namespace TextureMesh {
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
meta: ValueCell.create(textureMesh.meta),
...createInteriorValues(props.interior),
...createAnimationValues(props.animation),
};
}
@@ -270,8 +276,8 @@ export namespace TextureMesh {
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
updateInteriorValues(values, props.interior);
updateAnimationValues(values, props.animation);
}
function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ValueCell } from '../../mol-util/value-cell';
import { Vec2 } from '../../mol-math/linear-algebra';
import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
export type WiggleType = 'instance' | 'groupInstance';
export type WiggleData = {
tWiggle: ValueCell<TextureImage<Uint8Array>>
uWiggleTexDim: ValueCell<Vec2>
dWiggle: ValueCell<boolean>,
wiggleAverage: ValueCell<number>,
dWiggleType: ValueCell<string>,
uWiggleStrength: ValueCell<number>,
}
export function applyWiggleValue(array: Uint8Array, start: number, end: number, value: number) {
for (let i = start; i < end; ++i) {
array[i] = value * 255;
}
return true;
}
export function getWiggleAverage(array: Uint8Array, count: number): number {
if (count === 0 || array.length < count) return 0;
let sum = 0;
for (let i = 0; i < count; ++i) {
sum += array[i];
}
return sum / (255 * count);
}
export function clearWiggle(array: Uint8Array, start: number, end: number) {
array.fill(0, start, end);
}
export function createWiggle(count: number, type: WiggleType, wiggleData?: WiggleData): WiggleData {
const wiggle = createTextureImage(Math.max(1, count), 1, Uint8Array, wiggleData && wiggleData.tWiggle.ref.value.array);
if (wiggleData) {
ValueCell.update(wiggleData.tWiggle, wiggle);
ValueCell.update(wiggleData.uWiggleTexDim, Vec2.create(wiggle.width, wiggle.height));
ValueCell.updateIfChanged(wiggleData.dWiggle, count > 0);
ValueCell.updateIfChanged(wiggleData.wiggleAverage, getWiggleAverage(wiggle.array, count));
ValueCell.updateIfChanged(wiggleData.dWiggleType, type);
return wiggleData;
} else {
return {
tWiggle: ValueCell.create(wiggle),
uWiggleTexDim: ValueCell.create(Vec2.create(wiggle.width, wiggle.height)),
dWiggle: ValueCell.create(count > 0),
wiggleAverage: ValueCell.create(0),
dWiggleType: ValueCell.create(type),
uWiggleStrength: ValueCell.create(1),
};
}
}
const emptyWiggleTexture = { array: new Uint8Array(1), width: 1, height: 1 };
export function createEmptyWiggle(wiggleData?: WiggleData): WiggleData {
if (wiggleData) {
ValueCell.update(wiggleData.tWiggle, emptyWiggleTexture);
ValueCell.update(wiggleData.uWiggleTexDim, Vec2.create(1, 1));
return wiggleData;
} else {
return {
tWiggle: ValueCell.create(emptyWiggleTexture),
uWiggleTexDim: ValueCell.create(Vec2.create(1, 1)),
dWiggle: ValueCell.create(false),
wiggleAverage: ValueCell.create(0),
dWiggleType: ValueCell.create('groupInstance'),
uWiggleStrength: ValueCell.create(1),
};
}
}

View File

@@ -52,7 +52,7 @@ describe('renderer', () => {
scene.add(points);
scene.commit();
expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
expect(ctx.stats.resourceCounts.texture).toBe(10);
expect(ctx.stats.resourceCounts.texture).toBe(11);
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 5 : 0);
expect(ctx.stats.resourceCounts.program).toBe(5);
expect(ctx.stats.resourceCounts.shader).toBe(10);
@@ -89,7 +89,7 @@ describe('renderer', () => {
sceneDpoit.commit();
expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 12 : 15);
expect(ctx.stats.resourceCounts.texture).toBe(28);
expect(ctx.stats.resourceCounts.texture).toBe(31);
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 15 : 0);
expect(ctx.stats.resourceCounts.program).toBe(7);
expect(ctx.stats.resourceCounts.shader).toBe(14);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { Renderable, RenderableState, createRenderable } from '../renderable';
import { WebGLContext } from '../webgl/context';
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
import { CylindersShaderCode } from '../shader-code';
import { ValueCell } from '../../mol-util';
@@ -32,9 +32,10 @@ export const CylindersSchema = {
dSolidInterior: DefineSpec('boolean'),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
dDualColor: DefineSpec('boolean'),
...InteriorSchema,
...AnimationSchema,
};
export type CylindersSchema = typeof CylindersSchema
export type CylindersValues = Values<CylindersSchema>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { Renderable, RenderableState, createRenderable } from '../renderable';
import { WebGLContext } from '../webgl/context';
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, ValueSpec, AnimationSchema } from './schema';
import { ValueCell } from '../../mol-util';
import { LinesShaderCode } from '../shader-code';
@@ -22,6 +22,10 @@ export const LinesSchema = {
dLineSizeAttenuation: DefineSpec('boolean'),
uDoubleSided: UniformSpec('b', 'material'),
dFlipSided: DefineSpec('boolean'),
stripCount: ValueSpec('number'),
stripOffsets: ValueSpec('uint32'),
...AnimationSchema,
};
export type LinesSchema = typeof LinesSchema
export type LinesValues = Values<LinesSchema>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { Renderable, RenderableState, createRenderable } from '../renderable';
import { WebGLContext } from '../webgl/context';
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema, ValueSpec, UniformSpec, GlobalDefineSchema, GlobalDefineValues, GlobalDefines } from './schema';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema, ValueSpec, UniformSpec, GlobalDefineSchema, GlobalDefineValues, GlobalDefines, InteriorSchema, AnimationSchema } from './schema';
import { MeshShaderCode } from '../shader-code';
import { ValueCell } from '../../mol-util';
@@ -27,9 +27,10 @@ export const MeshSchema = {
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
meta: ValueSpec('unknown')
meta: ValueSpec('unknown'),
...InteriorSchema,
...AnimationSchema,
} as const;
export type MeshSchema = typeof MeshSchema
export type MeshValues = Values<MeshSchema>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { Renderable, RenderableState, createRenderable } from '../renderable';
import { WebGLContext } from '../webgl/context';
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, AnimationSchema } from './schema';
import { PointsShaderCode } from '../shader-code';
import { ValueCell } from '../../mol-util';
@@ -18,6 +18,8 @@ export const PointsSchema = {
aPosition: AttributeSpec('float32', 3, 0),
dPointSizeAttenuation: DefineSpec('boolean'),
dPointStyle: DefineSpec('string', ['square', 'circle', 'fuzzy']),
...AnimationSchema,
};
export type PointsSchema = typeof PointsSchema
export type PointsValues = Values<PointsSchema>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -179,6 +179,9 @@ export const GlobalUniformSchema = {
uMarkingDepthTest: UniformSpec('b'),
uMarkingType: UniformSpec('i'),
uPickType: UniformSpec('i'),
uTime: UniformSpec('f'),
uEnableAnimation: UniformSpec('b'),
} as const;
export type GlobalUniformSchema = typeof GlobalUniformSchema
export type GlobalUniformValues = Values<GlobalUniformSchema>
@@ -315,6 +318,17 @@ export const ClippingSchema = {
export type ClippingSchema = typeof ClippingSchema
export type ClippingValues = Values<ClippingSchema>
export const WiggleSchema = {
uWiggleTexDim: UniformSpec('v2'),
tWiggle: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
dWiggle: DefineSpec('boolean'),
wiggleAverage: ValueSpec('number'),
dWiggleType: DefineSpec('string', ['instance', 'groupInstance']),
uWiggleStrength: UniformSpec('f', 'material'),
} as const;
export type WiggleSchema = typeof WiggleSchema
export type WiggleValues = Values<WiggleSchema>
export const BaseSchema = {
dGeometryType: DefineSpec('string', ['cylinders', 'directVolume', 'image', 'lines', 'mesh', 'points', 'spheres', 'text', 'textureMesh']),
@@ -325,6 +339,7 @@ export const BaseSchema = {
...EmissiveSchema,
...SubstanceSchema,
...ClippingSchema,
...WiggleSchema,
dClipObjectCount: DefineSpec('number'),
dClipVariant: DefineSpec('string', ['instance', 'pixel']),
@@ -386,3 +401,24 @@ export const BaseSchema = {
} as const;
export type BaseSchema = typeof BaseSchema
export type BaseValues = Values<BaseSchema>
//
export const InteriorSchema = {
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
} as const;
export type InteriorSchema = typeof InteriorSchema
export type InteriorValues = Values<InteriorSchema>
export const AnimationSchema = {
uWiggleSpeed: UniformSpec('f', 'material'),
uWiggleAmplitude: UniformSpec('f', 'material'),
uWiggleFrequency: UniformSpec('f', 'material'),
uWiggleMode: UniformSpec('i', 'material'),
uTumbleSpeed: UniformSpec('f', 'material'),
uTumbleAmplitude: UniformSpec('f', 'material'),
uTumbleFrequency: UniformSpec('f', 'material'),
} as const;
export type AnimationSchema = typeof AnimationSchema
export type AnimationValues = Values<AnimationSchema>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { Renderable, RenderableState, createRenderable } from '../renderable';
import { WebGLContext } from '../webgl/context';
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
import { GlobalUniformSchema, BaseSchema, Values, InternalSchema, SizeSchema, InternalValues, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, TextureSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
import { GlobalUniformSchema, BaseSchema, Values, InternalSchema, SizeSchema, InternalValues, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, TextureSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
import { SpheresShaderCode } from '../shader-code';
import { ValueCell } from '../../mol-util';
@@ -30,12 +30,13 @@ export const SpheresSchema = {
uAlphaThickness: UniformSpec('f'),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
lodLevels: ValueSpec('unknown'),
centerBuffer: ValueSpec('float32'),
groupBuffer: ValueSpec('float32'),
...InteriorSchema,
...AnimationSchema,
};
export type SpheresSchema = typeof SpheresSchema
export type SpheresValues = Values<SpheresSchema>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { Renderable, RenderableState, createRenderable } from '../renderable';
import { WebGLContext } from '../webgl/context';
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema, ValueSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema, ValueSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
import { MeshShaderCode } from '../shader-code';
import { ValueCell } from '../../mol-util';
@@ -27,9 +27,10 @@ export const TextureMeshSchema = {
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),
uBumpAmplitude: UniformSpec('f', 'material'),
uInteriorColor: UniformSpec('v4'),
uInteriorSubstance: UniformSpec('v4'),
meta: ValueSpec('unknown')
meta: ValueSpec('unknown'),
...InteriorSchema,
...AnimationSchema,
};
export type TextureMeshSchema = typeof TextureMeshSchema
export type TextureMeshValues = Values<TextureMeshSchema>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -82,6 +82,13 @@ export function printTextureImage(textureImage: TextureImage<any>, options: Part
} else {
data.set(array);
}
} else if (itemSize === 3) {
for (let i = 0, il = width * height; i < il; ++i) {
data[i * 4] = array[i * 3];
data[i * 4 + 1] = array[i * 3 + 1];
data[i * 4 + 2] = array[i * 3 + 2];
data[i * 4 + 3] = 255;
}
} else {
console.warn(`itemSize '${itemSize}' not supported`);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -63,6 +63,7 @@ interface Renderer {
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean, forceToTransparency?: boolean) => void
clearDepth: (packed?: boolean) => void
update: (camera: ICamera, scene: Scene) => void
setTime: (time: number) => void
renderPick: (group: Scene.Group, camera: ICamera, variant: 'pick' | 'depth', pickType: PickType) => void
renderDepth: (group: Scene.Group, camera: ICamera) => void
@@ -121,6 +122,8 @@ export const RendererParams = {
}] }),
ambientColor: PD.Color(Color.fromNormalizedRgb(1.0, 1.0, 1.0)),
ambientIntensity: PD.Numeric(0.4, { min: 0.0, max: 2.0, step: 0.01 }),
enableAnimation: PD.Boolean(true, { description: 'Enable time-based animations.' }),
};
export type RendererProps = PD.Values<typeof RendererParams>
@@ -277,6 +280,9 @@ namespace Renderer {
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
uCelSteps: ValueCell.create(p.celSteps),
uExposure: ValueCell.create(p.exposure),
uTime: ValueCell.create(0),
uEnableAnimation: ValueCell.create(p.enableAnimation),
};
const globalUniformList = Object.entries(globalUniforms);
@@ -829,6 +835,9 @@ namespace Renderer {
renderWboitTransparent,
renderDpoitTransparent,
setTime: (time: number) => {
ValueCell.updateIfChanged(globalUniforms.uTime, time);
},
setProps: (props: Partial<RendererProps>) => {
if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) {
p.backgroundColor = props.backgroundColor;
@@ -904,6 +913,11 @@ namespace Renderer {
Vec3.scale(ambientColor, Color.toArrayNormalized(p.ambientColor, ambientColor, 0), p.ambientIntensity);
ValueCell.update(globalUniforms.uAmbientColor, ambientColor);
}
if (props.enableAnimation !== undefined && props.enableAnimation !== p.enableAnimation) {
p.enableAnimation = props.enableAnimation;
ValueCell.update(globalUniforms.uEnableAnimation, p.enableAnimation);
}
},
setViewport: (x: number, y: number, width: number, height: number) => {
state.viewport(x, y, width, height);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 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>
@@ -92,12 +92,16 @@ interface Scene extends Object3D {
readonly markerAverage: number
/** Emissive average of primitive renderables */
readonly emissiveAverage: number
/** Wiggle average of primitive renderables */
readonly wiggleAverage: number
/** Opacity average of primitive renderables */
readonly opacityAverage: number
/** Transparency minimum, excluding fully opaque, of primitive renderables */
readonly transparencyMin: number
/** Is `true` if any primitive renderable (possibly) has any opaque part */
readonly hasOpaque: boolean
/** Is `true` if any primitive renderable has animation enabled */
readonly hasAnimation: boolean
}
namespace Scene {
@@ -119,15 +123,19 @@ namespace Scene {
let markerAverageDirty = true;
let emissiveAverageDirty = true;
let wiggleAverageDirty = true;
let opacityAverageDirty = true;
let transparencyMinDirty = true;
let hasOpaqueDirty = true;
let hasAnimationDirty = true;
let markerAverage = 0;
let emissiveAverage = 0;
let wiggleAverage = 0;
let opacityAverage = 0;
let transparencyMin = 0;
let hasOpaque = false;
let hasAnimation = false;
const object3d = Object3D.create();
const { view, position, direction, up } = object3d;
@@ -185,9 +193,11 @@ namespace Scene {
renderables.sort(renderableSort);
markerAverageDirty = true;
emissiveAverageDirty = true;
wiggleAverageDirty = true;
opacityAverageDirty = true;
transparencyMinDirty = true;
hasOpaqueDirty = true;
hasAnimationDirty = true;
return true;
}
@@ -211,9 +221,11 @@ namespace Scene {
boundingSphereVisibleDirty = true;
markerAverageDirty = true;
emissiveAverageDirty = true;
wiggleAverageDirty = true;
opacityAverageDirty = true;
transparencyMinDirty = true;
hasOpaqueDirty = true;
hasAnimationDirty = true;
visibleHash = newVisibleHash;
return true;
} else {
@@ -245,6 +257,18 @@ namespace Scene {
return count > 0 ? emissiveAverage / count : 0;
}
function calculateWiggleAverage() {
if (primitives.length === 0) return 0;
let count = 0;
let wiggleAverage = 0;
for (let i = 0, il = primitives.length; i < il; ++i) {
if (!primitives[i].state.visible) continue;
wiggleAverage += primitives[i].values.wiggleAverage.ref.value;
count += 1;
}
return count > 0 ? wiggleAverage / count : 0;
}
function calculateOpacityAverage() {
if (primitives.length === 0) return 0;
let count = 0;
@@ -301,6 +325,22 @@ namespace Scene {
return false;
}
function calculateHasAnimation() {
for (let i = 0, il = primitives.length; i < il; ++i) {
const p = primitives[i];
if (!p.state.visible) continue;
if ((p.values.uWiggleAmplitude?.ref.value > 0 || p.values.wiggleAverage.ref.value > 0) &&
p.values.uWiggleSpeed?.ref.value > 0 &&
p.values.uWiggleFrequency?.ref.value > 0) return true;
if (p.values.uTumbleAmplitude?.ref.value > 0 &&
p.values.uTumbleSpeed?.ref.value > 0 &&
p.values.uTumbleFrequency?.ref.value > 0) return true;
}
return false;
}
return {
view, position, direction, up,
@@ -341,9 +381,11 @@ namespace Scene {
}
markerAverageDirty = true;
emissiveAverageDirty = true;
wiggleAverageDirty = true;
opacityAverageDirty = true;
transparencyMinDirty = true;
hasOpaqueDirty = true;
hasAnimationDirty = true;
},
add: (o: GraphicsRenderObject) => commitQueue.add(o),
remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
@@ -401,6 +443,13 @@ namespace Scene {
}
return emissiveAverage;
},
get wiggleAverage() {
if (wiggleAverageDirty) {
wiggleAverage = calculateWiggleAverage();
wiggleAverageDirty = false;
}
return wiggleAverage;
},
get opacityAverage() {
if (opacityAverageDirty) {
opacityAverage = calculateOpacityAverage();
@@ -422,6 +471,13 @@ namespace Scene {
}
return hasOpaque;
},
get hasAnimation() {
if (hasAnimationDirty) {
hasAnimation = calculateHasAnimation();
hasAnimationDirty = false;
}
return hasAnimation;
},
};
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -62,6 +62,7 @@ import { clip_instance } from './shader/chunks/clip-instance.glsl';
import { clip_pixel } from './shader/chunks/clip-pixel.glsl';
import { color_frag_params } from './shader/chunks/color-frag-params.glsl';
import { color_vert_params } from './shader/chunks/color-vert-params.glsl';
import { common_animation } from './shader/chunks/common-animation.glsl';
import { common_clip } from './shader/chunks/common-clip.glsl';
import { common_frag_params } from './shader/chunks/common-frag-params.glsl';
import { common_vert_params } from './shader/chunks/common-vert-params.glsl';
@@ -97,6 +98,7 @@ const ShaderChunks: { [k: string]: string } = {
clip_pixel,
color_frag_params,
color_vert_params,
common_animation,
common_clip,
common_frag_params,
common_vert_params,
@@ -207,7 +209,7 @@ export const CylindersShaderCode = ShaderCode('cylinders', cylinders_vert, cylin
import { text_vert } from './shader/text.vert';
import { text_frag } from './shader/text.frag';
export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { drawBuffers: 'optional' }, {}, ignoreDefineUnlit);
export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { fragDepth: 'optional', drawBuffers: 'optional' }, {}, ignoreDefineUnlit);
import { lines_vert } from './shader/lines.vert';
import { lines_frag } from './shader/lines.frag';

View File

@@ -78,7 +78,7 @@ export const apply_light_color = `
}
#pragma unroll_loop_end
outgoingLight += physicalMaterial.diffuseColor * luminance(uAmbientColor);
outgoingLight += physicalMaterial.diffuseColor * uAmbientColor;
#else
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));

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