mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
175 Commits
v5.7.0
...
obj-format
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6550771d65 | ||
|
|
93a3eba66d | ||
|
|
41b8584fb7 | ||
|
|
523b17dfde | ||
|
|
c47b4d6078 | ||
|
|
b94073b96f | ||
|
|
905eb3ec2f | ||
|
|
3ae72e5c60 | ||
|
|
055dfd4946 | ||
|
|
2601d2ba63 | ||
|
|
340806d774 | ||
|
|
18ad848de2 | ||
|
|
9de8334af5 | ||
|
|
57580a5e6b | ||
|
|
7da4a85459 | ||
|
|
b7c380fd90 | ||
|
|
bcd304d058 | ||
|
|
fd50a8f8e0 | ||
|
|
27f251e8e4 | ||
|
|
8d2a44983e | ||
|
|
f806ac1444 | ||
|
|
63a585d88a | ||
|
|
a4b5a16fcd | ||
|
|
86bf859a63 | ||
|
|
1b8117d3f1 | ||
|
|
400e2bbc45 | ||
|
|
e2e26c7e9c | ||
|
|
5ca9020cbf | ||
|
|
ea4c411d5c | ||
|
|
ba7e3fe827 | ||
|
|
8f20571a17 | ||
|
|
c25a4247e6 | ||
|
|
1071d3d8ba | ||
|
|
e8dc046570 | ||
|
|
27f9c2aa67 | ||
|
|
a4962231c8 | ||
|
|
8833f29ce5 | ||
|
|
40b6038380 | ||
|
|
59e16e0187 | ||
|
|
ca5a50bd53 | ||
|
|
bccf54fabe | ||
|
|
57a790544c | ||
|
|
df0669598c | ||
|
|
fb912036af | ||
|
|
9efb5cd126 | ||
|
|
08a56ad6ab | ||
|
|
2c2bd6adda | ||
|
|
b010298acb | ||
|
|
7033a1e0b2 | ||
|
|
8ad617acdf | ||
|
|
31ab6aa93e | ||
|
|
0a2dbe14d7 | ||
|
|
89d305aaa1 | ||
|
|
dbb6b90fbc | ||
|
|
c57150f09f | ||
|
|
0b30c7344b | ||
|
|
d7ad5a6e9f | ||
|
|
86a74d1cc2 | ||
|
|
3f0f24cb99 | ||
|
|
b8ddc142ea | ||
|
|
cccaa48589 | ||
|
|
3ad355ad40 | ||
|
|
918186eb24 | ||
|
|
db4742cebf | ||
|
|
19fec3bbc1 | ||
|
|
7d6c77b3bd | ||
|
|
dfcc4e400d | ||
|
|
c9734d83a2 | ||
|
|
93943cc27b | ||
|
|
25836b2de0 | ||
|
|
c6874c922d | ||
|
|
0937c84f47 | ||
|
|
6a7f892d60 | ||
|
|
b4cd2d0a11 | ||
|
|
2067f02830 | ||
|
|
6d86ada6b4 | ||
|
|
f656cf09b7 | ||
|
|
a891b4c551 | ||
|
|
ded844c936 | ||
|
|
44b36637fd | ||
|
|
f590bd0f0a | ||
|
|
9474c80673 | ||
|
|
7b48d691c8 | ||
|
|
b03146852f | ||
|
|
9345f3584a | ||
|
|
4d058aa1a8 | ||
|
|
e7da6092aa | ||
|
|
94f6b864b0 | ||
|
|
6e90447511 | ||
|
|
b91030c4bd | ||
|
|
31819dbf16 | ||
|
|
1665dd7d00 | ||
|
|
9716fecdb9 | ||
|
|
684fd2d237 | ||
|
|
9432b9a7a7 | ||
|
|
3a37c95c17 | ||
|
|
6040b99c19 | ||
|
|
83bef0f0e7 | ||
|
|
95bb3a1f81 | ||
|
|
be677f47cb | ||
|
|
43bf69d09c | ||
|
|
b6cc626431 | ||
|
|
931fdfca9b | ||
|
|
1c10db5656 | ||
|
|
c4ccd8758f | ||
|
|
6c99c575bc | ||
|
|
ae2493b6e3 | ||
|
|
bcd50c294f | ||
|
|
9c0024dbab | ||
|
|
c15b3603c0 | ||
|
|
70647ba972 | ||
|
|
8d19357845 | ||
|
|
8e9817c4d1 | ||
|
|
b16147b88c | ||
|
|
9840d8f816 | ||
|
|
d892ccab4c | ||
|
|
65f88b3293 | ||
|
|
9e6e5eb795 | ||
|
|
2f755efeec | ||
|
|
012e616ec4 | ||
|
|
007d0e7608 | ||
|
|
bf313073b9 | ||
|
|
293928f3de | ||
|
|
2404f398b6 | ||
|
|
43ff6e24c8 | ||
|
|
9e62112366 | ||
|
|
026d6fc618 | ||
|
|
95fcd942dc | ||
|
|
805481db14 | ||
|
|
39175df025 | ||
|
|
cd0f451f6b | ||
|
|
fe1aa1a9bf | ||
|
|
fcfb6e6d5a | ||
|
|
c548c94575 | ||
|
|
2d45f4a77c | ||
|
|
a5ae887842 | ||
|
|
e4b53cdc6a | ||
|
|
c53940e67e | ||
|
|
6d61745f0f | ||
|
|
46d86d93b0 | ||
|
|
11772b64fb | ||
|
|
dbc8ab00c6 | ||
|
|
015fad4371 | ||
|
|
71a484586f | ||
|
|
f0b06ee746 | ||
|
|
b0694b886b | ||
|
|
eaf47b3169 | ||
|
|
ad9046fcf2 | ||
|
|
eabe4d46bc | ||
|
|
003c5f8fb7 | ||
|
|
68748a4a94 | ||
|
|
9bd6b8195d | ||
|
|
05848b651c | ||
|
|
0a8f87dd9f | ||
|
|
925aaa701d | ||
|
|
5be599bad4 | ||
|
|
e22ce53e65 | ||
|
|
4c49431027 | ||
|
|
4192d82ef3 | ||
|
|
ce220737f2 | ||
|
|
eeb7cd2c52 | ||
|
|
748111beb2 | ||
|
|
1f7d41c653 | ||
|
|
b9430ff387 | ||
|
|
6591bab035 | ||
|
|
4da446aec2 | ||
|
|
25c170e36d | ||
|
|
eba18d1dce | ||
|
|
2c87d01a5e | ||
|
|
e41a2baa32 | ||
|
|
c297017749 | ||
|
|
9a0fc1faa6 | ||
|
|
424513f23c | ||
|
|
895d672589 | ||
|
|
0c6253ed16 |
57
CHANGELOG.md
57
CHANGELOG.md
@@ -4,6 +4,63 @@ 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
|
||||
- Add OBJ format 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
|
||||
|
||||
@@ -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)
|
||||
3914
package-lock.json
generated
3914
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.7.0",
|
||||
"version": "5.9.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -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": {
|
||||
@@ -135,51 +136,48 @@
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/webxr": "^0.5.24",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"@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",
|
||||
"cpx2": "^8.0.2",
|
||||
"css-loader": "^7.1.4",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild-jest-transform": "^2.0.1",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
"eslint": "^10.0.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"globals": "^17.3.0",
|
||||
"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.3",
|
||||
"simple-git": "^3.32.3",
|
||||
"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": "^22.19.13",
|
||||
"@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.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.32.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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import { GraphicsMode, MesoscaleGroup, MesoscaleState, getGraphicsModeProps, get
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { ParseCif, ParsePly, ReadFile } from '../../../../mol-plugin-state/transforms/data';
|
||||
import { ModelFromTrajectory, ShapeFromPly, TrajectoryFromGRO, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromMmCif, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../../../mol-plugin-state/transforms/model';
|
||||
import { ModelFromTrajectory, TrajectoryFromGRO, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromMmCif, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../../../mol-plugin-state/transforms/model';
|
||||
import { Euler } from '../../../../mol-math/linear-algebra/3d/euler';
|
||||
import { Asset } from '../../../../mol-util/assets';
|
||||
import { Clip } from '../../../../mol-util/clip';
|
||||
@@ -24,6 +24,7 @@ import { getFileNameInfo } from '../../../../mol-util/file-info';
|
||||
import { NumberArray } from '../../../../mol-util/type-helpers';
|
||||
import { BaseGeometry } from '../../../../mol-geo/geometry/base';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { ShapeFromPly } from '../../../../mol-plugin-state/transforms/shape';
|
||||
|
||||
function getSpacefillParams(color: Color, sizeFactor: number, graphics: GraphicsMode, clipVariant: Clip.Variant) {
|
||||
const gmp = getGraphicsModeProps(graphics === 'custom' ? 'quality' : graphics);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>}
|
||||
</>;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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/');
|
||||
|
||||
@@ -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$/)) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal file
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal 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);
|
||||
}
|
||||
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal file
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal 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);
|
||||
}
|
||||
266
src/extensions/debug-helpers/image-helper.ts
Normal file
266
src/extensions/debug-helpers/image-helper.ts
Normal 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);
|
||||
}
|
||||
71
src/extensions/debug-helpers/index.ts
Normal file
71
src/extensions/debug-helpers/index.ts
Normal 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,
|
||||
});
|
||||
164
src/extensions/debug-helpers/mesh-helper.ts
Normal file
164
src/extensions/debug-helpers/mesh-helper.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 }),
|
||||
|
||||
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal file
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -502,4 +504,5 @@ export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<Molsta
|
||||
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
|
||||
NonCovalentInteractionsExtension,
|
||||
IsHiddenCustomStateExtension,
|
||||
VolumeStreamingExtension,
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
85
src/mol-canvas3d/helper/debug-registry.ts
Normal file
85
src/mol-canvas3d/helper/debug-registry.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
64
src/mol-geo/geometry/animation.ts
Normal file
64
src/mol-geo/geometry/animation.ts
Normal 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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -222,7 +225,7 @@ export namespace Cylinders {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
51
src/mol-geo/geometry/lines/builder/box.ts
Normal file
51
src/mol-geo/geometry/lines/builder/box.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
34
src/mol-geo/geometry/lines/builder/plane.ts
Normal file
34
src/mol-geo/geometry/lines/builder/plane.ts
Normal 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);
|
||||
}
|
||||
82
src/mol-geo/geometry/lines/builder/sphere.ts
Normal file
82
src/mol-geo/geometry/lines/builder/sphere.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -188,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
|
||||
|
||||
@@ -229,7 +232,7 @@ export namespace Lines {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -237,6 +240,7 @@ export namespace Lines {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.vertexCount, groupCount, instanceCount };
|
||||
|
||||
@@ -262,6 +266,7 @@ export namespace Lines {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -269,6 +274,7 @@ 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,
|
||||
@@ -285,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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -175,7 +178,7 @@ export namespace Points {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
@@ -311,7 +342,7 @@ export namespace Spheres {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
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)) {
|
||||
|
||||
@@ -25,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';
|
||||
@@ -33,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' |
|
||||
@@ -218,7 +219,7 @@ export namespace Text {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -226,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 };
|
||||
|
||||
@@ -253,6 +255,7 @@ export namespace Text {
|
||||
...emissive,
|
||||
...substance,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
aTexCoord: text.tcoordBuffer,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
79
src/mol-geo/geometry/wiggle-data.ts
Normal file
79
src/mol-geo/geometry/wiggle-data.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, ValueSpec } 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';
|
||||
|
||||
@@ -24,6 +24,8 @@ export const LinesSchema = {
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
stripCount: ValueSpec('number'),
|
||||
stripOffsets: ValueSpec('uint32'),
|
||||
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type LinesSchema = typeof LinesSchema
|
||||
export type LinesValues = Values<LinesSchema>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
export const assign_position = `
|
||||
mat4 model = uModel * aTransform;
|
||||
#ifdef dGeometryType_image
|
||||
mat4 transform = aTransform;
|
||||
#else
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
#endif
|
||||
mat4 model = uModel * transform;
|
||||
mat4 modelView = uView * model;
|
||||
#ifdef dGeometryType_textureMesh
|
||||
vec3 position = readFromTexture(tPosition, vertexId, uGeoTexDim).xyz;
|
||||
#else
|
||||
vec3 position = aPosition;
|
||||
#endif
|
||||
#ifndef dGeometryType_image
|
||||
position = applyWiggle(position, group, aInstance);
|
||||
#endif
|
||||
vec4 position4 = vec4(position, 1.0);
|
||||
// for accessing tColorGrid in vert shader and for clipping in frag shader
|
||||
vModelPosition = (model * position4).xyz;
|
||||
|
||||
102
src/mol-gl/shader/chunks/common-animation.glsl.ts
Normal file
102
src/mol-gl/shader/chunks/common-animation.glsl.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const common_animation = `
|
||||
uniform float uWiggleSpeed;
|
||||
uniform float uWiggleAmplitude;
|
||||
uniform float uWiggleFrequency;
|
||||
uniform int uWiggleMode;
|
||||
uniform float uTumbleSpeed;
|
||||
uniform float uTumbleAmplitude;
|
||||
uniform float uTumbleFrequency;
|
||||
|
||||
#ifdef dWiggle
|
||||
uniform vec2 uWiggleTexDim;
|
||||
uniform sampler2D tWiggle;
|
||||
uniform float uWiggleStrength;
|
||||
#endif
|
||||
|
||||
vec3 applyWiggle(vec3 pos, float groupId, float instanceId) {
|
||||
if (!uEnableAnimation) return pos;
|
||||
float amplitude = uWiggleAmplitude;
|
||||
#ifdef dWiggle
|
||||
#if defined(dWiggleType_instance)
|
||||
amplitude += readFromTexture(tWiggle, instanceId, uWiggleTexDim).a * uWiggleStrength;
|
||||
#elif defined(dWiggleType_groupInstance)
|
||||
amplitude += readFromTexture(tWiggle, instanceId * float(uGroupCount) + groupId, uWiggleTexDim).a * uWiggleStrength;
|
||||
#endif
|
||||
#endif
|
||||
if (amplitude > 0.0 && uWiggleSpeed > 0.0 && uWiggleFrequency > 0.0) {
|
||||
float t = uTime * uWiggleSpeed;
|
||||
vec3 s;
|
||||
if (uWiggleMode == 0) {
|
||||
// Position mode: spatial position correlates nearby atoms
|
||||
s = pos;
|
||||
} else {
|
||||
// Group mode: per-group independent noise
|
||||
// Hash groupId into a well-distributed 3D seed to avoid repetition
|
||||
s = vec3(
|
||||
fract(sin(groupId * 127.1) * 43758.5453) * 1000.0,
|
||||
fract(sin(groupId * 269.5) * 21639.7182) * 1000.0,
|
||||
fract(sin(groupId * 419.2) * 32517.3926) * 1000.0
|
||||
);
|
||||
}
|
||||
s *= uWiggleFrequency;
|
||||
pos.x += (fbm(vec3(s.x, s.y + t, s.z)) / 0.4375 - 1.0) * amplitude;
|
||||
pos.y += (fbm(vec3(s.x + 37.0, s.y, s.z + t)) / 0.4375 - 1.0) * amplitude;
|
||||
pos.z += (fbm(vec3(s.x + t, s.y + 73.0, s.z)) / 0.4375 - 1.0) * amplitude;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
mat4 applyTumble(mat4 transform, float instanceIndex, float objectId) {
|
||||
if (!uEnableAnimation) return transform;
|
||||
if (uTumbleAmplitude > 0.0 && uTumbleSpeed > 0.0 && uTumbleFrequency > 0.0) {
|
||||
// Scale amplitude inversely with bounding-sphere radius (Stokes-Einstein: D ~ 1/r)
|
||||
float amplitude = uTumbleAmplitude / max(uInvariantBoundingSphere.w, 1.0);
|
||||
float t = uTime * uTumbleSpeed;
|
||||
float seed = (instanceIndex * 127.1 + objectId * 311.7) * uTumbleFrequency;
|
||||
|
||||
// Per-instance rotation angles from layered noise (Brownian-like)
|
||||
float angleX = (fbm(vec3(seed, t, 0.0)) / 0.4375 - 1.0) * amplitude;
|
||||
float angleY = (fbm(vec3(seed, 0.0, t)) / 0.4375 - 1.0) * amplitude;
|
||||
float angleZ = (fbm(vec3(0.0, seed, t)) / 0.4375 - 1.0) * amplitude;
|
||||
|
||||
float cx = cos(angleX); float sx = sin(angleX);
|
||||
float cy = cos(angleY); float sy = sin(angleY);
|
||||
float cz = cos(angleZ); float sz = sin(angleZ);
|
||||
|
||||
// Combined rotation matrix (Rz * Ry * Rx)
|
||||
mat3 rot = mat3(
|
||||
cy * cz, cx * sz + sx * sy * cz, sx * sz - cx * sy * cz,
|
||||
-cy * sz, cx * cz - sx * sy * sz, sx * cz + cx * sy * sz,
|
||||
sy, -sx * cy, cx * cy
|
||||
);
|
||||
|
||||
// Per-instance translation offset from layered noise (Brownian-like)
|
||||
vec3 offset = vec3(
|
||||
(fbm(vec3(seed + 31.7, t, 0.0)) / 0.4375 - 1.0),
|
||||
(fbm(vec3(seed + 31.7, 0.0, t)) / 0.4375 - 1.0),
|
||||
(fbm(vec3(0.0, seed + 31.7, t)) / 0.4375 - 1.0)
|
||||
) * amplitude;
|
||||
|
||||
// Bounding-sphere center transformed by the linear part only (no translation)
|
||||
vec3 localCenter = mat3(transform) * uInvariantBoundingSphere.xyz;
|
||||
|
||||
// Rotate basis vectors
|
||||
mat4 result = transform;
|
||||
result[0].xyz = rot * transform[0].xyz;
|
||||
result[1].xyz = rot * transform[1].xyz;
|
||||
result[2].xyz = rot * transform[2].xyz;
|
||||
|
||||
// Adjust translation so rotation pivots around the transformed center
|
||||
result[3].xyz = transform[3].xyz + localCenter - rot * localCenter + offset;
|
||||
|
||||
return result;
|
||||
}
|
||||
return transform;
|
||||
}
|
||||
`;
|
||||
@@ -121,34 +121,6 @@ vec3 perturbNormal(in vec3 position, in vec3 normal, in float height, in float s
|
||||
return normalize(abs(det) * normal - scale * surfGrad);
|
||||
}
|
||||
|
||||
float hash(in float h) {
|
||||
return fract(sin(h) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(in vec3 x) {
|
||||
vec3 p = floor(x);
|
||||
vec3 f = fract(x);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float n = p.x + p.y * 157.0 + 113.0 * p.z;
|
||||
return mix(
|
||||
mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
|
||||
mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y),
|
||||
mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
|
||||
mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y), f.z);
|
||||
}
|
||||
|
||||
float fbm(in vec3 p) {
|
||||
float f = 0.0;
|
||||
f += 0.5 * noise(p);
|
||||
p *= 2.01;
|
||||
f += 0.25 * noise(p);
|
||||
p *= 2.02;
|
||||
f += 0.125 * noise(p);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
#ifdef dXrayShaded
|
||||
float calcXrayShadedAlpha(in float alpha, const in vec3 normal) {
|
||||
#if defined(dXrayShaded_on)
|
||||
|
||||
@@ -12,6 +12,8 @@ uniform vec4 uLod;
|
||||
|
||||
uniform bool uDoubleSided;
|
||||
uniform int uPickType;
|
||||
uniform float uTime;
|
||||
uniform bool uEnableAnimation;
|
||||
|
||||
#if dClipObjectCount != 0
|
||||
uniform int uClipObjectType[dClipObjectCount];
|
||||
|
||||
@@ -279,4 +279,32 @@ mat3 adjoint(const in mat4 m) {
|
||||
#define isNaN isnan
|
||||
#define isInf isinf
|
||||
#endif
|
||||
|
||||
float hash(in float h) {
|
||||
return fract(sin(h) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(in vec3 x) {
|
||||
vec3 p = floor(x);
|
||||
vec3 f = fract(x);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float n = p.x + p.y * 157.0 + 113.0 * p.z;
|
||||
return mix(
|
||||
mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
|
||||
mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y),
|
||||
mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
|
||||
mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y), f.z);
|
||||
}
|
||||
|
||||
float fbm(in vec3 p) {
|
||||
float f = 0.0;
|
||||
f += 0.5 * noise(p);
|
||||
p *= 2.01;
|
||||
f += 0.25 * noise(p);
|
||||
p *= 2.02;
|
||||
f += 0.125 * noise(p);
|
||||
|
||||
return f;
|
||||
}
|
||||
`;
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform mat4 uModelView;
|
||||
|
||||
@@ -46,11 +47,14 @@ void main() {
|
||||
#include assign_clipping_varying
|
||||
#include assign_size
|
||||
|
||||
mat4 modelTransform = uModel * aTransform;
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
vec3 wigStart = applyWiggle(aStart, aGroup, aInstance);
|
||||
vec3 wigEnd = applyWiggle(aEnd, aGroup, aInstance);
|
||||
mat4 modelTransform = uModel * transform;
|
||||
|
||||
vTransform = modelTransform;
|
||||
vStart = (modelTransform * vec4(aStart, 1.0)).xyz;
|
||||
vEnd = (modelTransform * vec4(aEnd, 1.0)).xyz;
|
||||
vStart = (modelTransform * vec4(wigStart, 1.0)).xyz;
|
||||
vEnd = (modelTransform * vec4(wigEnd, 1.0)).xyz;
|
||||
vSize = size * aScale * uModelScale;
|
||||
vCap = aCap;
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ uniform sampler2D tMarker;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying float vInstance;
|
||||
varying vec3 vPosition;
|
||||
|
||||
#ifdef dUsePalette
|
||||
uniform sampler2D tPalette;
|
||||
@@ -138,7 +139,7 @@ uniform float uIsoLevel;
|
||||
#endif
|
||||
|
||||
void main() {
|
||||
if (uTrimType != 0 && getSignedDistance(vModelPosition, uTrimType, uTrimCenter, uTrimRotation, uTrimScale, uTrimTransform) > 0.0) discard;
|
||||
if (uTrimType != 0 && getSignedDistance(vPosition, uTrimType, uTrimCenter, uTrimRotation, uTrimScale, uTrimTransform) > 0.0) discard;
|
||||
|
||||
#include fade_lod
|
||||
#include clip_pixel
|
||||
|
||||
@@ -17,6 +17,7 @@ attribute float aInstance;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying float vInstance;
|
||||
varying vec3 vPosition;
|
||||
|
||||
void main() {
|
||||
int vertexId = VertexID;
|
||||
@@ -25,5 +26,6 @@ void main() {
|
||||
|
||||
vUv = aUv;
|
||||
vInstance = aInstance;
|
||||
vPosition = aPosition;
|
||||
}
|
||||
`;
|
||||
@@ -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>
|
||||
*
|
||||
@@ -16,6 +16,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform float uPixelRatio;
|
||||
uniform vec4 uViewport;
|
||||
@@ -48,18 +49,20 @@ void main(){
|
||||
#include assign_clipping_varying
|
||||
#include assign_size
|
||||
|
||||
mat4 modelView = uView * uModel * aTransform;
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
vec3 wigStart = applyWiggle(aStart, group, aInstance);
|
||||
vec3 wigEnd = applyWiggle(aEnd, group, aInstance);
|
||||
mat4 modelView = uView * uModel * transform;
|
||||
|
||||
// camera space
|
||||
vec4 start = modelView * vec4(aStart, 1.0);
|
||||
vec4 end = modelView * vec4(aEnd, 1.0);
|
||||
vec4 start = modelView * vec4(wigStart, 1.0);
|
||||
vec4 end = modelView * vec4(wigEnd, 1.0);
|
||||
|
||||
// assign position
|
||||
vec4 position4 = vec4((aMapping.y < 0.5) ? aStart : aEnd, 1.0);
|
||||
vec4 mvPosition = modelView * position4;
|
||||
vViewPosition = mvPosition.xyz;
|
||||
vec4 position4 = vec4((aMapping.y < 0.5) ? wigStart : wigEnd, 1.0);
|
||||
vViewPosition = (aMapping.y < 0.5) ? start.xyz : end.xyz;
|
||||
|
||||
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
|
||||
vModelPosition = (uModel * transform * position4).xyz; // for clipping in frag shader
|
||||
|
||||
// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
|
||||
// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp sampler2D;
|
||||
#include common_vert_params
|
||||
#include color_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
#include texture3d_from_2d_linear
|
||||
|
||||
#ifdef dGeometryType_textureMesh
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform float uPixelRatio;
|
||||
uniform vec4 uViewport;
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform mat4 uModelView;
|
||||
uniform mat4 uInvProjection;
|
||||
@@ -68,7 +69,7 @@ const mat4 D = mat4(
|
||||
* "GPU-Based Ray-Casting of Quadratic Surfaces" http://dl.acm.org/citation.cfm?id=2386396
|
||||
* by Christian Sigg, Tim Weyrich, Mario Botsch, Markus Gross.
|
||||
*/
|
||||
void quadraticProjection(const in vec3 position, const in float radius, const in vec2 mapping) {
|
||||
void quadraticProjection(const in vec3 position, const in float radius, const in vec2 mapping, const in mat4 transform) {
|
||||
vec2 xbc, ybc;
|
||||
|
||||
mat4 T = mat4(
|
||||
@@ -78,7 +79,7 @@ void quadraticProjection(const in vec3 position, const in float radius, const in
|
||||
position.x, position.y, position.z, 1.0
|
||||
);
|
||||
|
||||
mat4 R = transpose4(uProjection * uModelView * aTransform * T);
|
||||
mat4 R = transpose4(uProjection * uModelView * transform * T);
|
||||
float A = dot(R[3], D * R[3]);
|
||||
float B = -2.0 * dot(R[0], D * R[3]);
|
||||
float C = dot(R[0], D * R[0]);
|
||||
@@ -119,6 +120,9 @@ void main(void){
|
||||
vec3 position = positionGroup.rgb;
|
||||
float group = positionGroup.a;
|
||||
|
||||
position = applyWiggle(position, group, aInstance);
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
|
||||
#include assign_color_varying
|
||||
#include assign_marker_varying
|
||||
#include assign_clipping_varying
|
||||
@@ -127,7 +131,7 @@ void main(void){
|
||||
vRadius = size * uModelScale;
|
||||
|
||||
vec4 position4 = vec4(position, 1.0);
|
||||
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
|
||||
vModelPosition = (uModel * transform * position4).xyz; // for clipping in frag shader
|
||||
|
||||
float d;
|
||||
if (uLod.w != 0.0 && (uLod.x != 0.0 || uLod.y != 0.0)) {
|
||||
@@ -143,7 +147,7 @@ void main(void){
|
||||
}
|
||||
}
|
||||
|
||||
vec4 mvPosition = uModelView * aTransform * position4;
|
||||
vec4 mvPosition = uModelView * transform * position4;
|
||||
|
||||
#ifdef dApproximate
|
||||
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
|
||||
@@ -156,7 +160,7 @@ void main(void){
|
||||
gl_Position = uProjection * mvCorner;
|
||||
} else if (uIsAsymmetricProjection) {
|
||||
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
|
||||
quadraticProjection(position, vRadius / uModelScale, mapping);
|
||||
quadraticProjection(position, vRadius / uModelScale, mapping, transform);
|
||||
} else {
|
||||
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
|
||||
sphereProjection(mvPosition.xyz, vRadius, mapping);
|
||||
|
||||
@@ -104,7 +104,7 @@ export interface COMPAT_vertex_array_object {
|
||||
bindVertexArray(arrayObject: WebGLVertexArrayObject | null): void;
|
||||
createVertexArray(): WebGLVertexArrayObject | null;
|
||||
deleteVertexArray(arrayObject: WebGLVertexArrayObject): void;
|
||||
isVertexArray(value: any): value is WebGLVertexArrayObject;
|
||||
isVertexArray(value: any): boolean
|
||||
}
|
||||
|
||||
export function getVertexArrayObject(gl: GLRenderingContext): COMPAT_vertex_array_object | null {
|
||||
@@ -484,7 +484,7 @@ export interface COMPAT_disjoint_timer_query {
|
||||
/** Records the current time into the corresponding query object. */
|
||||
queryCounter: (query: WebGLQuery, target: number) => void
|
||||
/** Returns information about a query target. */
|
||||
getQuery: (target: number, pname: number) => WebGLQuery | number
|
||||
getQuery: (target: number, pname: number) => WebGLQuery | null
|
||||
/** Return the state of a query object. */
|
||||
getQueryParameter: (query: WebGLQuery, pname: number) => number | boolean
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ export function uint8ToString(array: Uint8Array) {
|
||||
if (array.length > ChunkSize) {
|
||||
const c = [];
|
||||
for (let i = 0; i < array.length; i += ChunkSize) {
|
||||
// @ts-ignore
|
||||
c.push(String.fromCharCode.apply(null, array.subarray(i, i + ChunkSize)));
|
||||
}
|
||||
return c.join('');
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return String.fromCharCode.apply(null, array);
|
||||
}
|
||||
}
|
||||
385
src/mol-io/reader/_spec/obj.spec.ts
Normal file
385
src/mol-io/reader/_spec/obj.spec.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { parseObj } from '../obj/parser';
|
||||
|
||||
// Simple triangle
|
||||
const objTriangle = `# simple triangle
|
||||
v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
f 1 2 3
|
||||
`;
|
||||
|
||||
// Quad that gets fan-triangulated into 2 triangles
|
||||
const objQuad = `# quad fan-triangulated
|
||||
v -1.0 -1.0 0.0
|
||||
v 1.0 -1.0 0.0
|
||||
v 1.0 1.0 0.0
|
||||
v -1.0 1.0 0.0
|
||||
f 1 2 3 4
|
||||
`;
|
||||
|
||||
// Vertex normals
|
||||
const objWithNormals = `# vertex normals
|
||||
v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
vn 0.0 0.0 1.0
|
||||
vn 0.0 0.0 1.0
|
||||
vn 0.0 0.0 1.0
|
||||
f 1//1 2//2 3//3
|
||||
`;
|
||||
|
||||
// v/vt/vn format (texture coords are ignored but should not break parsing)
|
||||
const objWithTexture = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
vn 0.0 0.0 1.0
|
||||
f 1/1/1 2/2/1 3/3/1
|
||||
`;
|
||||
|
||||
// Multiple materials / usemtl groups
|
||||
const objMultiMaterial = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
v 2.0 0.0 0.0
|
||||
v 2.5 1.0 0.0
|
||||
usemtl red
|
||||
f 1 2 3
|
||||
usemtl green
|
||||
f 2 4 5
|
||||
`;
|
||||
|
||||
// Negative indices (relative addressing)
|
||||
const objNegativeIndices = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
f -3 -2 -1
|
||||
`;
|
||||
|
||||
// Comments and blank lines should be ignored
|
||||
const objWithComments = `# header comment
|
||||
# another comment
|
||||
|
||||
v 0.0 0.0 0.0
|
||||
# inline comment after data
|
||||
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
|
||||
f 1 2 3
|
||||
`;
|
||||
|
||||
// Unsupported directives (g, o, s, mtllib, vt, vp) should be silently skipped
|
||||
const objUnsupportedDirectives = `mtllib material.mtl
|
||||
o MyObject
|
||||
g mygroup
|
||||
s 1
|
||||
v 0.0 0.0 0.0
|
||||
vt 0.0 0.0
|
||||
vp 0.0 1.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
f 1 2 3
|
||||
`;
|
||||
|
||||
// Cube (6 faces × 2 triangles = 12 triangles)
|
||||
const objCube = `# unit cube
|
||||
v -1 -1 -1
|
||||
v 1 -1 -1
|
||||
v 1 1 -1
|
||||
v -1 1 -1
|
||||
v -1 -1 1
|
||||
v 1 -1 1
|
||||
v 1 1 1
|
||||
v -1 1 1
|
||||
# bottom (-z)
|
||||
f 1 2 3
|
||||
f 1 3 4
|
||||
# top (+z)
|
||||
f 5 6 7
|
||||
f 5 7 8
|
||||
# front (+x)
|
||||
f 2 6 7
|
||||
f 2 7 3
|
||||
# back (-x)
|
||||
f 5 1 4
|
||||
f 5 4 8
|
||||
# left (-y)
|
||||
f 1 5 6
|
||||
f 1 6 2
|
||||
# right (+y)
|
||||
f 4 3 7
|
||||
f 4 7 8
|
||||
`;
|
||||
|
||||
// CRLF line endings
|
||||
const objCRLF = '# crlf triangle\r\nv 0.0 0.0 0.0\r\nv 1.0 0.0 0.0\r\nv 0.5 1.0 0.0\r\nf 1 2 3\r\n';
|
||||
|
||||
// Tabs and leading whitespace before keywords
|
||||
const objLeadingWhitespace = '\tv 0.0 0.0 0.0\n v 1.0 0.0 0.0\n\t v 0.5 1.0 0.0\n\tf 1 2 3\n';
|
||||
|
||||
// Degenerate face (fewer than 3 vertices) should be skipped with a warning
|
||||
const objDegenerateFace = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
f 1 2
|
||||
f 1 2 3
|
||||
`;
|
||||
|
||||
// Mixed face-vertices: some reference a normal, some do not, within one mesh
|
||||
const objMixedNormals = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
vn 0.0 0.0 1.0
|
||||
f 1//1 2 3//1
|
||||
`;
|
||||
|
||||
// Negative normal indices (relative addressing for normals)
|
||||
const objNegativeNormalIndices = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
vn 0.0 0.0 1.0
|
||||
f 1//-1 2//-1 3//-1
|
||||
`;
|
||||
|
||||
// usemtl reuse: a material name is referenced again after another material
|
||||
const objReusedMaterial = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
v 2.0 0.0 0.0
|
||||
v 2.5 1.0 0.0
|
||||
usemtl red
|
||||
f 1 2 3
|
||||
usemtl green
|
||||
f 2 4 5
|
||||
usemtl red
|
||||
f 1 3 4
|
||||
`;
|
||||
|
||||
// Empty file
|
||||
const objEmpty = '';
|
||||
|
||||
// File with vertices but no faces
|
||||
const objNoFaces = `v 0.0 0.0 0.0
|
||||
v 1.0 0.0 0.0
|
||||
v 0.5 1.0 0.0
|
||||
`;
|
||||
|
||||
describe('obj reader', () => {
|
||||
it('parses a simple triangle', async () => {
|
||||
const parsed = await parseObj(objTriangle).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
// First vertex
|
||||
expect(obj.positions[0]).toBeCloseTo(0.0);
|
||||
expect(obj.positions[1]).toBeCloseTo(0.0);
|
||||
expect(obj.positions[2]).toBeCloseTo(0.0);
|
||||
// Triangle indices (0-based)
|
||||
expect(Array.from(obj.positionIndices)).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
it('fan-triangulates a quad into two triangles', async () => {
|
||||
const parsed = await parseObj(objQuad).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(4);
|
||||
expect(obj.triangleCount).toBe(2);
|
||||
// Fan from vertex 0: (0,1,2) and (0,2,3)
|
||||
expect(Array.from(obj.positionIndices)).toEqual([0, 1, 2, 0, 2, 3]);
|
||||
});
|
||||
|
||||
it('parses vertex normals with v//vn format', async () => {
|
||||
const parsed = await parseObj(objWithNormals).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.normalCount).toBe(3);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
expect(Array.from(obj.normalIndices)).toEqual([0, 1, 2]);
|
||||
// Normal z component of first normal
|
||||
expect(obj.normals[2]).toBeCloseTo(1.0);
|
||||
});
|
||||
|
||||
it('parses v/vt/vn format (texture coords ignored)', async () => {
|
||||
const parsed = await parseObj(objWithTexture).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.normalCount).toBe(1);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
expect(Array.from(obj.normalIndices)).toEqual([0, 0, 0]);
|
||||
});
|
||||
|
||||
it('assigns material groups via usemtl', async () => {
|
||||
const parsed = await parseObj(objMultiMaterial).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.triangleCount).toBe(2);
|
||||
// 'default' is always first; 'red' and 'green' added on use
|
||||
expect(obj.groups[1]).toBe('red');
|
||||
expect(obj.groups[2]).toBe('green');
|
||||
// First triangle belongs to 'red' (index 1), second to 'green' (index 2)
|
||||
expect(obj.groupIndices[0]).toBe(1);
|
||||
expect(obj.groupIndices[1]).toBe(2);
|
||||
});
|
||||
|
||||
it('handles negative (relative) vertex indices', async () => {
|
||||
const parsed = await parseObj(objNegativeIndices).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
// -3, -2, -1 with posCount=3 → 0, 1, 2
|
||||
expect(Array.from(obj.positionIndices)).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
it('ignores comments and blank lines', async () => {
|
||||
const parsed = await parseObj(objWithComments).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
});
|
||||
|
||||
it('silently skips unsupported directives', async () => {
|
||||
const parsed = await parseObj(objUnsupportedDirectives).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
});
|
||||
|
||||
it('parses a cube (12 triangles, 8 vertices)', async () => {
|
||||
const parsed = await parseObj(objCube).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(8);
|
||||
expect(obj.triangleCount).toBe(12);
|
||||
expect(obj.positionIndices.length).toBe(36); // 12 * 3
|
||||
});
|
||||
|
||||
it('returns no normals when none are defined', async () => {
|
||||
const parsed = await parseObj(objTriangle).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.normalCount).toBe(0);
|
||||
// All normal indices should be -1
|
||||
expect(Array.from(obj.normalIndices)).toEqual([-1, -1, -1]);
|
||||
});
|
||||
|
||||
it('default group is always present', async () => {
|
||||
const parsed = await parseObj(objTriangle).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.groups[0]).toBe('default');
|
||||
expect(obj.groupIndices[0]).toBe(0);
|
||||
});
|
||||
|
||||
it('parses CRLF line endings', async () => {
|
||||
const parsed = await parseObj(objCRLF).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
expect(Array.from(obj.positionIndices)).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
it('handles tabs and leading whitespace before keywords', async () => {
|
||||
const parsed = await parseObj(objLeadingWhitespace).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
expect(Array.from(obj.positionIndices)).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
it('skips a degenerate face and emits a warning', async () => {
|
||||
const parsed = await parseObj(objDegenerateFace).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
// Only the valid triangle survives
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
expect(Array.from(obj.positionIndices)).toEqual([0, 1, 2]);
|
||||
// A warning was recorded for the degenerate face
|
||||
expect(parsed.warnings.length).toBeGreaterThan(0);
|
||||
expect(parsed.warnings.some(w => w.includes('degenerate'))).toBe(true);
|
||||
});
|
||||
|
||||
it('parses faces with mixed normal/no-normal vertices', async () => {
|
||||
const parsed = await parseObj(objMixedNormals).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.normalCount).toBe(1);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
// First and third vertices reference normal 0; the middle has none (-1)
|
||||
expect(Array.from(obj.normalIndices)).toEqual([0, -1, 0]);
|
||||
});
|
||||
|
||||
it('handles negative (relative) normal indices', async () => {
|
||||
const parsed = await parseObj(objNegativeNormalIndices).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.normalCount).toBe(1);
|
||||
expect(obj.triangleCount).toBe(1);
|
||||
// -1 with normCount=1 → 0
|
||||
expect(Array.from(obj.normalIndices)).toEqual([0, 0, 0]);
|
||||
});
|
||||
|
||||
it('reuses an already-seen usemtl material name', async () => {
|
||||
const parsed = await parseObj(objReusedMaterial).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.triangleCount).toBe(3);
|
||||
// 'red' (1) and 'green' (2) are the only added groups; no duplicate 'red'
|
||||
expect(obj.groups[1]).toBe('red');
|
||||
expect(obj.groups[2]).toBe('green');
|
||||
expect(obj.groups.indexOf('red')).toBe(1);
|
||||
expect(obj.groups.lastIndexOf('red')).toBe(1);
|
||||
// Third face reuses 'red' (index 1)
|
||||
expect(Array.from(obj.groupIndices)).toEqual([1, 2, 1]);
|
||||
});
|
||||
|
||||
it('parses an empty file', async () => {
|
||||
const parsed = await parseObj(objEmpty).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(0);
|
||||
expect(obj.normalCount).toBe(0);
|
||||
expect(obj.triangleCount).toBe(0);
|
||||
expect(obj.groups[0]).toBe('default');
|
||||
});
|
||||
|
||||
it('parses a file with vertices but no faces', async () => {
|
||||
const parsed = await parseObj(objNoFaces).run();
|
||||
if (parsed.isError) throw new Error(parsed.message);
|
||||
const obj = parsed.result;
|
||||
|
||||
expect(obj.positionCount).toBe(3);
|
||||
expect(obj.triangleCount).toBe(0);
|
||||
expect(obj.positionIndices.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -18,7 +18,7 @@ export async function readCcp4Header(file: FileHandle): Promise<{ header: Ccp4He
|
||||
// 53 MAP Character string 'MAP ' to identify file type
|
||||
const MAP = String.fromCharCode(
|
||||
buffer.readUInt8(52 * 4), buffer.readUInt8(52 * 4 + 1),
|
||||
buffer.readUInt8(52 * 4 + 2), buffer.readUInt8(52 * 4 + 3)
|
||||
buffer.readUInt8(52 * 4 + 2), buffer.readUInt8(52 * 4 + 3) || 32 // null as space
|
||||
);
|
||||
if (MAP !== 'MAP ') {
|
||||
throw new Error('ccp4 format error, missing "MAP " string');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.410, IHM 1.28, MA 1.4.9.
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.414, IHM 1.28, MA 1.4.9.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.410, IHM 1.28, MA 1.4.9.
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.414, IHM 1.28, MA 1.4.9.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.410, IHM 1.28, MA 1.4.9.
|
||||
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.414, IHM 1.28, MA 1.4.9.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
@@ -2198,7 +2198,7 @@ export const mmCIF_Schema = {
|
||||
* The value of occupancy flag indicates whether the residue
|
||||
* is unobserved (= 1) or the coordinates have an occupancy of zero (=0)
|
||||
*/
|
||||
occupancy_flag: Aliased<'1' | '0'>(int),
|
||||
occupancy_flag: Aliased<1 | 0>(int),
|
||||
/**
|
||||
* Part of the identifier for the unobserved or zero occupancy residue.
|
||||
*
|
||||
@@ -3178,7 +3178,7 @@ export const mmCIF_Schema = {
|
||||
* The denominator used while calculating the sequence identity provided in
|
||||
* _ihm_starting_comparative_models.template_sequence_identity.
|
||||
*/
|
||||
template_sequence_identity_denominator: Aliased<'1' | '2' | '3' | '4' | '5'>(int),
|
||||
template_sequence_identity_denominator: Aliased<1 | 2 | 3 | 4 | 5>(int),
|
||||
/**
|
||||
* The dataset list id corresponding to the template used to obtain the comparative model.
|
||||
* This data item is a pointer to _ihm_dataset_list.id in the IHM_DATASET_LIST category.
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Dsn6File, Dsn6Header } from './schema';
|
||||
import { ReaderResult as Result } from '../result';
|
||||
import { FileHandle } from '../../common/file-handle';
|
||||
import { SimpleBuffer } from '../../../mol-io/common/simple-buffer';
|
||||
import { uint8ToString } from '../../common/binary';
|
||||
|
||||
export const dsn6HeaderSize = 512;
|
||||
|
||||
@@ -70,7 +71,7 @@ function getBlocks(header: Dsn6Header) {
|
||||
|
||||
export async function readDsn6Header(file: FileHandle): Promise<{ header: Dsn6Header, littleEndian: boolean }> {
|
||||
const { buffer } = await file.readBuffer(0, dsn6HeaderSize);
|
||||
const brixStr = String.fromCharCode.apply(null, buffer) as string;
|
||||
const brixStr = uint8ToString(buffer);
|
||||
const isBrix = brixStr.startsWith(':-)');
|
||||
const littleEndian = isBrix || buffer.readInt16LE(18 * 2) === 100;
|
||||
const header = isBrix ? parseBrixHeader(brixStr) : parseDsn6Header(buffer, littleEndian);
|
||||
|
||||
@@ -1,22 +1,323 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ReaderResult as Result } from '../result';
|
||||
import { Task, RuntimeContext } from '../../../mol-task';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { ChunkedArray } from '../../../mol-data/util';
|
||||
import { ObjFile } from './schema';
|
||||
import { StringLike } from '../../common/string-like';
|
||||
import { Tokenizer } from '../common/text/tokenizer';
|
||||
import { parseInt, parseFloat } from '../common/text/number-parser';
|
||||
|
||||
async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Mesh>> {
|
||||
// TODO
|
||||
const mesh: Mesh = Mesh.createEmpty();
|
||||
// Mesh.computeNormalsImmediate(mesh)
|
||||
return Result.success(mesh);
|
||||
// OBJ file format specification: http://www.martinreddy.net/gfx/3d/OBJ.spec
|
||||
|
||||
interface State {
|
||||
tokenizer: Tokenizer
|
||||
positions: ChunkedArray<number, 3>
|
||||
normals: ChunkedArray<number, 3>
|
||||
positionIndices: ChunkedArray<number, 3>
|
||||
normalIndices: ChunkedArray<number, 3>
|
||||
groupIndices: ChunkedArray<number, 1>
|
||||
groups: string[]
|
||||
currentGroupIdx: number
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
export function parse(data: string) {
|
||||
return Task.create<Result<Mesh>>('Parse OBJ', async ctx => {
|
||||
function State(data: StringLike): State {
|
||||
return {
|
||||
tokenizer: Tokenizer(data),
|
||||
positions: ChunkedArray.create(Float32Array, 3, 1024),
|
||||
normals: ChunkedArray.create(Float32Array, 3, 1024),
|
||||
positionIndices: ChunkedArray.create(Int32Array, 3, 1024),
|
||||
normalIndices: ChunkedArray.create(Int32Array, 3, 1024),
|
||||
groupIndices: ChunkedArray.create(Uint32Array, 1, 1024),
|
||||
groups: ['default'],
|
||||
currentGroupIdx: 0,
|
||||
warnings: []
|
||||
};
|
||||
}
|
||||
|
||||
// Character codes used for keyword identification without materializing strings
|
||||
const CC_v = 118; // 'v'
|
||||
const CC_n = 110; // 'n'
|
||||
const CC_f = 102; // 'f'
|
||||
const CC_u = 117; // 'u'
|
||||
const CC_s = 115; // 's'
|
||||
const CC_e = 101; // 'e'
|
||||
const CC_m = 109; // 'm'
|
||||
const CC_t = 116; // 't'
|
||||
const CC_l = 108; // 'l'
|
||||
const CC_HASH = 35; // '#'
|
||||
const CC_NEWLINE = 10; // '\n'
|
||||
const CC_CR = 13; // '\r'
|
||||
const CC_SPACE = 32; // ' '
|
||||
const CC_TAB = 9; // '\t'
|
||||
const CC_SLASH = 47; // '/'
|
||||
|
||||
/** Skip to the end of the current line without returning it. */
|
||||
function skipLine(tokenizer: Tokenizer): void {
|
||||
const { data } = tokenizer;
|
||||
while (tokenizer.position < tokenizer.length) {
|
||||
const c = data.charCodeAt(tokenizer.position);
|
||||
if (c === CC_NEWLINE) { ++tokenizer.position; ++tokenizer.lineNumber; return; }
|
||||
if (c === CC_CR) {
|
||||
++tokenizer.position;
|
||||
++tokenizer.lineNumber;
|
||||
if (tokenizer.position < tokenizer.length && data.charCodeAt(tokenizer.position) === CC_NEWLINE) ++tokenizer.position;
|
||||
return;
|
||||
}
|
||||
++tokenizer.position;
|
||||
}
|
||||
}
|
||||
|
||||
/** Skip inline whitespace (space/tab only — does not cross newlines). */
|
||||
function skipInlineWS(tokenizer: Tokenizer): void {
|
||||
const { data } = tokenizer;
|
||||
while (tokenizer.position < tokenizer.length) {
|
||||
const c = data.charCodeAt(tokenizer.position);
|
||||
if (c !== CC_SPACE && c !== CC_TAB) return;
|
||||
++tokenizer.position;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one whitespace-delimited token on the current line.
|
||||
* Returns false when end-of-line / end-of-file is reached before any character.
|
||||
* Leaves tokenizer.tokenStart/tokenEnd set to the token boundaries.
|
||||
*/
|
||||
function readInlineToken(tokenizer: Tokenizer): boolean {
|
||||
skipInlineWS(tokenizer);
|
||||
const { data } = tokenizer;
|
||||
if (tokenizer.position >= tokenizer.length) return false;
|
||||
const c = data.charCodeAt(tokenizer.position);
|
||||
if (c === CC_NEWLINE || c === CC_CR || c === CC_HASH) return false;
|
||||
tokenizer.tokenStart = tokenizer.position;
|
||||
while (tokenizer.position < tokenizer.length) {
|
||||
const ch = data.charCodeAt(tokenizer.position);
|
||||
if (ch === CC_SPACE || ch === CC_TAB || ch === CC_NEWLINE || ch === CC_CR || ch === CC_HASH) break;
|
||||
++tokenizer.position;
|
||||
}
|
||||
tokenizer.tokenEnd = tokenizer.position;
|
||||
return tokenizer.tokenEnd > tokenizer.tokenStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read up to `maxCount` face-vertex tokens from the current line into `facePos` / `faceNorm`.
|
||||
* Returns the number of tokens read.
|
||||
* Face vertex format: posIdx[/[texIdx][/normIdx]] (all 1-based, may be negative).
|
||||
*/
|
||||
function readFaceTokens(
|
||||
tokenizer: Tokenizer,
|
||||
facePos: Int32Array, faceNorm: Int32Array,
|
||||
maxCount: number,
|
||||
posCount: number, normCount: number
|
||||
): number {
|
||||
const { data } = tokenizer;
|
||||
let count = 0;
|
||||
while (count < maxCount && readInlineToken(tokenizer)) {
|
||||
const start = tokenizer.tokenStart;
|
||||
const end = tokenizer.tokenEnd;
|
||||
|
||||
// Find first slash within [start, end)
|
||||
let slash1 = -1;
|
||||
for (let i = start; i < end; ++i) {
|
||||
if (data.charCodeAt(i) === CC_SLASH) { slash1 = i; break; }
|
||||
}
|
||||
|
||||
let posIdx: number;
|
||||
let normIdx = -1;
|
||||
|
||||
if (slash1 === -1) {
|
||||
// "v"
|
||||
const p = parseInt(data, start, end);
|
||||
posIdx = p < 0 ? posCount + p : p - 1;
|
||||
} else {
|
||||
const p = parseInt(data, start, slash1);
|
||||
posIdx = p < 0 ? posCount + p : p - 1;
|
||||
|
||||
// Find second slash
|
||||
let slash2 = -1;
|
||||
for (let i = slash1 + 1; i < end; ++i) {
|
||||
if (data.charCodeAt(i) === CC_SLASH) { slash2 = i; break; }
|
||||
}
|
||||
|
||||
if (slash2 !== -1 && slash2 + 1 < end) {
|
||||
// "v/vt/vn" or "v//vn"
|
||||
const n = parseInt(data, slash2 + 1, end);
|
||||
normIdx = n < 0 ? normCount + n : n - 1;
|
||||
}
|
||||
// else "v/vt" — no normal
|
||||
}
|
||||
|
||||
facePos[count] = posIdx;
|
||||
faceNorm[count] = normIdx;
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Reusable scratch buffers for face vertex data (polygons up to MAX_FACE_VERTICES vertices)
|
||||
const MAX_FACE_VERTICES = 256;
|
||||
const _facePos = new Int32Array(MAX_FACE_VERTICES);
|
||||
const _faceNorm = new Int32Array(MAX_FACE_VERTICES);
|
||||
|
||||
function handleVertex(state: State): void {
|
||||
const { tokenizer } = state;
|
||||
let x = 0, y = 0, z = 0;
|
||||
if (readInlineToken(tokenizer)) x = parseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
|
||||
if (readInlineToken(tokenizer)) y = parseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
|
||||
if (readInlineToken(tokenizer)) z = parseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
|
||||
ChunkedArray.add3(state.positions, x, y, z);
|
||||
skipLine(tokenizer);
|
||||
}
|
||||
|
||||
function handleNormal(state: State): void {
|
||||
const { tokenizer } = state;
|
||||
let x = 0, y = 0, z = 0;
|
||||
if (readInlineToken(tokenizer)) x = parseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
|
||||
if (readInlineToken(tokenizer)) y = parseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
|
||||
if (readInlineToken(tokenizer)) z = parseFloat(tokenizer.data, tokenizer.tokenStart, tokenizer.tokenEnd);
|
||||
ChunkedArray.add3(state.normals, x, y, z);
|
||||
skipLine(tokenizer);
|
||||
}
|
||||
|
||||
function handleFace(state: State): void {
|
||||
const { tokenizer } = state;
|
||||
const posCount = state.positions.elementCount;
|
||||
const normCount = state.normals.elementCount;
|
||||
|
||||
const n = readFaceTokens(tokenizer, _facePos, _faceNorm, MAX_FACE_VERTICES, posCount, normCount);
|
||||
if (n < 3) {
|
||||
state.warnings.push(`Line ${tokenizer.lineNumber}: degenerate face with ${n} vertices, skipped`);
|
||||
skipLine(tokenizer);
|
||||
return;
|
||||
}
|
||||
// Warn if the polygon exceeded the scratch buffer capacity and was truncated.
|
||||
if (n === MAX_FACE_VERTICES && readInlineToken(tokenizer)) {
|
||||
state.warnings.push(`Line ${tokenizer.lineNumber}: face with more than ${MAX_FACE_VERTICES} vertices truncated`);
|
||||
}
|
||||
|
||||
// Fan-triangulate: (0,1,2), (0,2,3), ...
|
||||
const p0 = _facePos[0], n0 = _faceNorm[0];
|
||||
for (let i = 1; i < n - 1; ++i) {
|
||||
ChunkedArray.add3(state.positionIndices, p0, _facePos[i], _facePos[i + 1]);
|
||||
ChunkedArray.add3(state.normalIndices, n0, _faceNorm[i], _faceNorm[i + 1]);
|
||||
ChunkedArray.add(state.groupIndices, state.currentGroupIdx);
|
||||
}
|
||||
skipLine(tokenizer);
|
||||
}
|
||||
|
||||
function handleUsemtl(state: State): void {
|
||||
const { tokenizer } = state;
|
||||
// Read the rest of the line as the material name (trimmed)
|
||||
skipInlineWS(tokenizer);
|
||||
tokenizer.tokenStart = tokenizer.position;
|
||||
const { data } = tokenizer;
|
||||
while (tokenizer.position < tokenizer.length) {
|
||||
const c = data.charCodeAt(tokenizer.position);
|
||||
if (c === CC_NEWLINE || c === CC_CR) break;
|
||||
++tokenizer.position;
|
||||
}
|
||||
// Trim trailing whitespace
|
||||
let end = tokenizer.position;
|
||||
while (end > tokenizer.tokenStart && (data.charCodeAt(end - 1) === CC_SPACE || data.charCodeAt(end - 1) === CC_TAB)) --end;
|
||||
const name = end > tokenizer.tokenStart ? data.substring(tokenizer.tokenStart, end) : 'default';
|
||||
|
||||
const existing = state.groups.indexOf(name);
|
||||
if (existing !== -1) {
|
||||
state.currentGroupIdx = existing;
|
||||
} else {
|
||||
state.currentGroupIdx = state.groups.length;
|
||||
state.groups.push(name);
|
||||
}
|
||||
skipLine(tokenizer);
|
||||
}
|
||||
|
||||
async function parseInternal(data: StringLike, ctx: RuntimeContext): Promise<Result<ObjFile>> {
|
||||
const state = State(data);
|
||||
const { tokenizer } = state;
|
||||
const updateChunk = 100000;
|
||||
|
||||
while (tokenizer.position < tokenizer.length) {
|
||||
// Skip full-line whitespace and newlines between lines
|
||||
const c0 = tokenizer.data.charCodeAt(tokenizer.position);
|
||||
if (c0 === CC_NEWLINE) { ++tokenizer.position; ++tokenizer.lineNumber; continue; }
|
||||
if (c0 === CC_CR) {
|
||||
++tokenizer.position; ++tokenizer.lineNumber;
|
||||
if (tokenizer.position < tokenizer.length && tokenizer.data.charCodeAt(tokenizer.position) === CC_NEWLINE) ++tokenizer.position;
|
||||
continue;
|
||||
}
|
||||
if (c0 === CC_SPACE || c0 === CC_TAB) { skipInlineWS(tokenizer); continue; }
|
||||
if (c0 === CC_HASH) { skipLine(tokenizer); continue; }
|
||||
|
||||
// Identify keyword by inspecting character codes — no string allocation
|
||||
const c1 = tokenizer.position + 1 < tokenizer.length ? tokenizer.data.charCodeAt(tokenizer.position + 1) : -1;
|
||||
|
||||
if (c0 === CC_f && (c1 === CC_SPACE || c1 === CC_TAB)) {
|
||||
// "f " — face
|
||||
tokenizer.position += 2;
|
||||
handleFace(state);
|
||||
} else if (c0 === CC_v) {
|
||||
if (c1 === CC_SPACE || c1 === CC_TAB) {
|
||||
// "v " — vertex position
|
||||
tokenizer.position += 2;
|
||||
handleVertex(state);
|
||||
} else if (c1 === CC_n) {
|
||||
// "vn" — vertex normal
|
||||
tokenizer.position += 2;
|
||||
handleNormal(state);
|
||||
} else {
|
||||
// "vt", "vp", etc — skip
|
||||
skipLine(tokenizer);
|
||||
}
|
||||
} else if (
|
||||
c0 === CC_u &&
|
||||
tokenizer.position + 6 < tokenizer.length &&
|
||||
tokenizer.data.charCodeAt(tokenizer.position + 1) === CC_s &&
|
||||
tokenizer.data.charCodeAt(tokenizer.position + 2) === CC_e &&
|
||||
tokenizer.data.charCodeAt(tokenizer.position + 3) === CC_m &&
|
||||
tokenizer.data.charCodeAt(tokenizer.position + 4) === CC_t &&
|
||||
tokenizer.data.charCodeAt(tokenizer.position + 5) === CC_l
|
||||
) {
|
||||
// "usemtl"
|
||||
tokenizer.position += 6;
|
||||
handleUsemtl(state);
|
||||
} else {
|
||||
// "o", "g", "s", "mtllib", "call", etc. — skip entire line
|
||||
skipLine(tokenizer);
|
||||
}
|
||||
|
||||
if (ctx.shouldUpdate && tokenizer.lineNumber % updateChunk === 0) {
|
||||
await ctx.update({ message: 'Parsing OBJ', current: tokenizer.position, max: tokenizer.length });
|
||||
}
|
||||
}
|
||||
|
||||
const posArr = ChunkedArray.compact(state.positions) as Float32Array;
|
||||
const normArr = ChunkedArray.compact(state.normals) as Float32Array;
|
||||
const posIdxArr = ChunkedArray.compact(state.positionIndices) as Int32Array;
|
||||
const normIdxArr = ChunkedArray.compact(state.normalIndices) as Int32Array;
|
||||
const grpIdxArr = ChunkedArray.compact(state.groupIndices) as Uint32Array;
|
||||
|
||||
const result: ObjFile = {
|
||||
positions: posArr,
|
||||
normals: normArr,
|
||||
positionIndices: posIdxArr,
|
||||
normalIndices: normIdxArr,
|
||||
groupIndices: grpIdxArr,
|
||||
groups: state.groups,
|
||||
positionCount: state.positions.elementCount,
|
||||
normalCount: state.normals.elementCount,
|
||||
triangleCount: posIdxArr.length / 3
|
||||
};
|
||||
|
||||
return Result.success(result, state.warnings);
|
||||
}
|
||||
|
||||
export function parseObj(data: StringLike) {
|
||||
return Task.create<Result<ObjFile>>('Parse OBJ', async ctx => {
|
||||
return await parseInternal(data, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
45
src/mol-io/reader/obj/schema.ts
Normal file
45
src/mol-io/reader/obj/schema.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Intermediate representation of a parsed OBJ file.
|
||||
*
|
||||
* Positions and normals are stored as raw arrays from the file.
|
||||
* Faces have been triangulated (fan-triangulation) and are stored as
|
||||
* separate flat arrays of per-triangle-vertex position and normal indices.
|
||||
* Normal indices of -1 indicate that the vertex has no explicit normal.
|
||||
*/
|
||||
export interface ObjFile {
|
||||
/** Raw position data from `v` lines, interleaved [x0,y0,z0, x1,y1,z1, ...] */
|
||||
readonly positions: Float32Array
|
||||
/** Raw normal data from `vn` lines, interleaved [nx0,ny0,nz0, ...]. Length 0 if no normals. */
|
||||
readonly normals: Float32Array
|
||||
|
||||
/**
|
||||
* Per-face-vertex position index (0-based), length = triangleCount * 3.
|
||||
* Three consecutive values define one triangle: [p0, p1, p2, p3, p4, p5, ...]
|
||||
*/
|
||||
readonly positionIndices: Int32Array
|
||||
|
||||
/**
|
||||
* Per-face-vertex normal index (0-based), length = triangleCount * 3.
|
||||
* -1 means no explicit normal for that face-vertex.
|
||||
*/
|
||||
readonly normalIndices: Int32Array
|
||||
|
||||
/**
|
||||
* Material/group index per triangle, length = triangleCount.
|
||||
* Index into the `groups` array.
|
||||
*/
|
||||
readonly groupIndices: Uint32Array
|
||||
|
||||
/** Ordered list of material group names, populated from `usemtl` directives. */
|
||||
readonly groups: ReadonlyArray<string>
|
||||
|
||||
readonly positionCount: number
|
||||
readonly normalCount: number
|
||||
readonly triangleCount: number
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user