mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
Merge branch 'master' of https://github.com/molstar/molstar into pr/russell-taylor/1806
This commit is contained in:
20
CHANGELOG.md
20
CHANGELOG.md
@@ -4,12 +4,32 @@ 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 empty transforms default in `ShapeFromPly`
|
||||
- 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
|
||||
|
||||
## [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)
|
||||
|
||||
21944
package-lock.json
generated
21944
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.8.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",
|
||||
@@ -137,47 +137,47 @@
|
||||
"@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": "^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",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.32.0",
|
||||
"swagger-ui-dist": "^5.32.5",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -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>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -98,6 +98,7 @@ export const Canvas3DParams = {
|
||||
transparentBackground: PD.Boolean(false),
|
||||
checkeredTransparentBackground: PD.Boolean(false),
|
||||
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
|
||||
enableAnimation: PD.Boolean(true, { description: 'Enable GPU time-based animations (wiggle/tumble).' }),
|
||||
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
|
||||
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
|
||||
|
||||
@@ -479,6 +480,7 @@ namespace Canvas3D {
|
||||
const hiZ = new HiZPass(webgl, passes.draw, canvas, p.hiZ);
|
||||
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
renderer.setOcclusionTest(hiZ.isOccluded);
|
||||
|
||||
const shaderManager = new ShaderManager(webgl, scene);
|
||||
@@ -675,7 +677,8 @@ namespace Canvas3D {
|
||||
const xrChanged = xrManager.update(xrFrame);
|
||||
if (!xrChanged && xrFrame) return false;
|
||||
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged;
|
||||
const activeAnimation = p.enableAnimation && scene.hasAnimation;
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged || activeAnimation;
|
||||
forceNextRender = false;
|
||||
|
||||
if (passes.illumination.supported && p.illumination.enabled && !xrFrame) {
|
||||
@@ -754,6 +757,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
|
||||
@@ -1067,6 +1071,7 @@ namespace Canvas3D {
|
||||
transparentBackground: p.transparentBackground,
|
||||
checkeredTransparentBackground: p.checkeredTransparentBackground,
|
||||
dpoitIterations: p.dpoitIterations,
|
||||
enableAnimation: p.enableAnimation,
|
||||
pickPadding: p.pickPadding,
|
||||
userInteractionReleaseMs: p.userInteractionReleaseMs,
|
||||
viewport: p.viewport,
|
||||
@@ -1312,6 +1317,10 @@ namespace Canvas3D {
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.checkeredTransparentBackground !== undefined) p.checkeredTransparentBackground = props.checkeredTransparentBackground;
|
||||
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
|
||||
if (props.enableAnimation !== undefined) {
|
||||
p.enableAnimation = props.enableAnimation;
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
}
|
||||
if (props.pickPadding !== undefined) {
|
||||
p.pickPadding = props.pickPadding;
|
||||
pickHelper.setPickPadding(p.pickPadding);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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 { 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),
|
||||
@@ -275,6 +280,7 @@ export namespace Cylinders {
|
||||
dDualColor: ValueCell.create(props.colorMode === 'interpolate'),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -297,6 +303,7 @@ export namespace Cylinders {
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
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,
|
||||
@@ -200,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();
|
||||
@@ -208,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 };
|
||||
|
||||
@@ -224,6 +226,7 @@ namespace Image {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
|
||||
|
||||
@@ -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 { 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),
|
||||
@@ -729,6 +734,7 @@ export namespace Mesh {
|
||||
meta: ValueCell.create(mesh.meta),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -750,6 +756,7 @@ export namespace Mesh {
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
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 { 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),
|
||||
@@ -368,6 +401,7 @@ export namespace Spheres {
|
||||
groupBuffer: spheres.groupBuffer,
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -392,6 +426,7 @@ export namespace Spheres {
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
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,
|
||||
|
||||
@@ -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 { 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),
|
||||
@@ -250,6 +255,7 @@ export namespace TextureMesh {
|
||||
meta: ValueCell.create(textureMesh.meta),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -271,6 +277,7 @@ export namespace TextureMesh {
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
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);
|
||||
|
||||
@@ -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, InteriorSchema } 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';
|
||||
|
||||
@@ -35,6 +35,7 @@ export const CylindersSchema = {
|
||||
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>
|
||||
|
||||
@@ -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, InteriorSchema } 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';
|
||||
|
||||
@@ -30,6 +30,7 @@ export const MeshSchema = {
|
||||
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']),
|
||||
@@ -395,3 +410,15 @@ export const InteriorSchema = {
|
||||
} 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>
|
||||
|
||||
@@ -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, InteriorSchema } 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';
|
||||
|
||||
@@ -36,6 +36,7 @@ export const SpheresSchema = {
|
||||
groupBuffer: ValueSpec('float32'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type SpheresSchema = typeof SpheresSchema
|
||||
export type SpheresValues = Values<SpheresSchema>
|
||||
|
||||
@@ -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, InteriorSchema } 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';
|
||||
|
||||
@@ -30,6 +30,7 @@ export const TextureMeshSchema = {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.411, 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.411, 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.411, 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
|
||||
*/
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
File diff suppressed because one or more lines are too long
25
src/mol-plugin-state/animation/built-in/time.ts
Normal file
25
src/mol-plugin-state/animation/built-in/time.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PluginStateAnimation } from '../model';
|
||||
|
||||
export const AnimateTime = PluginStateAnimation.create({
|
||||
name: 'built-in.animate-time',
|
||||
display: { name: 'Animate Time', description: 'Animate the passage of time in the 3D scene' },
|
||||
isExportable: true,
|
||||
params: () => ({
|
||||
durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
|
||||
}),
|
||||
initialState: () => ({ }),
|
||||
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
|
||||
|
||||
async apply(animState, t, ctx) {
|
||||
return t.current < ctx.params.durationInMs
|
||||
? { kind: 'next', state: animState }
|
||||
: { kind: 'finished' };
|
||||
}
|
||||
});
|
||||
@@ -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 David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -26,6 +26,7 @@ import { StructConn } from '../../../mol-model-formats/structure/property/bonds/
|
||||
import { StructureRepresentationRegistry } from '../../../mol-repr/structure/registry';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
|
||||
|
||||
export interface StructureRepresentationPresetProvider<P = any, S extends _Result = _Result> extends PresetProvider<PluginStateObject.Molecule.Structure, P, S> { }
|
||||
export function StructureRepresentationPresetProvider<P, S extends _Result>(repr: StructureRepresentationPresetProvider<P, S>) { return repr; }
|
||||
@@ -495,6 +496,61 @@ const autoLod = StructureRepresentationPresetProvider({
|
||||
}
|
||||
});
|
||||
|
||||
type MesoscaleGraphicsMode = keyof typeof Spheres.LodLevelsPresets
|
||||
const MesoscaleGraphicsOptions = PD.arrayToOptions(Object.keys(Spheres.LodLevelsPresets) as MesoscaleGraphicsMode[]);
|
||||
function getMesoscaleLodLevels(mode: MesoscaleGraphicsMode) {
|
||||
return Spheres.LodLevelsPresets[mode];
|
||||
}
|
||||
|
||||
const mesoscale = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-mesoscale',
|
||||
display: {
|
||||
name: 'Mesoscale', group: 'Miscellaneous',
|
||||
description: 'Show everything in spacefill representation with instance-granularity and level-of-detail tuned for large particle scenes.'
|
||||
},
|
||||
params: () => ({
|
||||
...CommonParams,
|
||||
graphics: PD.Select<MesoscaleGraphicsMode>('quality', MesoscaleGraphicsOptions),
|
||||
}),
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
if (!structureCell) return {};
|
||||
|
||||
const components = {
|
||||
all: await presetStaticComponent(plugin, structureCell, 'all'),
|
||||
};
|
||||
|
||||
const structure = structureCell.obj!.data;
|
||||
|
||||
const { update, builder, typeParams, color } = reprBuilder(plugin, params, structure);
|
||||
|
||||
const graphics: MesoscaleGraphicsMode = params.graphics ?? 'quality';
|
||||
const lodLevels = getMesoscaleLodLevels(graphics);
|
||||
const approximate = graphics !== 'quality' && graphics !== 'ultra';
|
||||
const alphaThickness = graphics === 'performance' ? 15 : 12;
|
||||
|
||||
const representations = {
|
||||
all: builder.buildRepresentation(update, components.all, {
|
||||
type: 'spacefill',
|
||||
typeParams: {
|
||||
...typeParams,
|
||||
instanceGranularity: true,
|
||||
lodLevels,
|
||||
approximate,
|
||||
alphaThickness,
|
||||
clipPrimitive: true,
|
||||
},
|
||||
color: color || 'entity-id',
|
||||
}, { tag: 'all' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await updateFocusRepr(plugin, structure, params.theme?.focus?.name ?? color, params.theme?.focus?.params);
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
export function presetStaticComponent(plugin: PluginContext, structure: StateObjectRef<PluginStateObject.Molecule.Structure>, type: StaticStructureComponentType, params?: { label?: string, tags?: string[] }) {
|
||||
return plugin.builders.structure.tryCreateComponentStatic(structure, type, params);
|
||||
}
|
||||
@@ -514,5 +570,6 @@ export const PresetStructureRepresentations = {
|
||||
illustrative,
|
||||
'molecular-surface': molecularSurface,
|
||||
'auto-lod': autoLod,
|
||||
mesoscale,
|
||||
};
|
||||
export type PresetStructureRepresentations = typeof PresetStructureRepresentations;
|
||||
168
src/mol-plugin-state/helpers/structure-wiggle.ts
Normal file
168
src/mol-plugin-state/helpers/structure-wiggle.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Structure, StructureElement, Unit } from '../../mol-model/structure';
|
||||
import { PluginStateObject } from '../objects';
|
||||
import { StateTransforms } from '../transforms';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateBuilder, StateObjectCell, StateSelection, StateTransform } from '../../mol-state';
|
||||
import { StructureComponentRef } from '../manager/structure/hierarchy-state';
|
||||
import { EmptyLoci, isEmptyLoci, Loci } from '../../mol-model/loci';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
import { OrderedSet } from '../../mol-data/int';
|
||||
|
||||
type WiggleEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, wiggle?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.WiggleStructureRepresentation3DFromBundle>>) => Promise<void>
|
||||
const WiggleManagerTag = 'wiggle-controls';
|
||||
|
||||
export async function setStructureWiggle(plugin: PluginContext, components: StructureComponentRef[], value: number, lociGetter: (structure: Structure) => Promise<StructureElement.Loci | EmptyLoci>, types?: string[]) {
|
||||
await eachRepr(plugin, components, async (update, repr, wiggleCell) => {
|
||||
if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
|
||||
|
||||
const structure = repr.obj!.data.sourceData;
|
||||
// always use the root structure to get the loci so the wiggle
|
||||
// stays applicable as long as the root structure does not change
|
||||
const loci = await lociGetter(structure.root);
|
||||
if (Loci.isEmpty(loci) || isEmptyLoci(loci)) return;
|
||||
|
||||
const layer = {
|
||||
bundle: StructureElement.Bundle.fromLoci(loci),
|
||||
value,
|
||||
};
|
||||
|
||||
if (wiggleCell) {
|
||||
const bundleLayers = [...wiggleCell.params!.values.layers, layer];
|
||||
const filtered = getFilteredBundle(bundleLayers, structure);
|
||||
update.to(wiggleCell).update(Wiggle.toBundle(filtered));
|
||||
} else {
|
||||
const filtered = getFilteredBundle([layer], structure);
|
||||
update.to(repr.transform.ref)
|
||||
.apply(StateTransforms.Representation.WiggleStructureRepresentation3DFromBundle, Wiggle.toBundle(filtered), { tags: WiggleManagerTag });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearStructureWiggle(plugin: PluginContext, components: StructureComponentRef[], types?: string[]) {
|
||||
await eachRepr(plugin, components, async (update, repr, wiggleCell) => {
|
||||
if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
|
||||
if (wiggleCell) {
|
||||
update.delete(wiggleCell.transform.ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: WiggleEachReprCallback) {
|
||||
const state = plugin.state.data;
|
||||
const update = state.build();
|
||||
for (const c of components) {
|
||||
for (const r of c.representations) {
|
||||
const wiggle = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.WiggleStructureRepresentation3DFromBundle, r.cell.transform.ref).withTag(WiggleManagerTag));
|
||||
await callback(update, r.cell, wiggle[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return update.commit({ doNotUpdateCurrent: true });
|
||||
}
|
||||
|
||||
/** filter wiggle layers for given structure */
|
||||
function getFilteredBundle(layers: Wiggle.BundleLayer[], structure: Structure) {
|
||||
const wiggle = Wiggle.ofBundle(layers, structure.root);
|
||||
const merged = Wiggle.merge(wiggle);
|
||||
return Wiggle.filter(merged, structure) as Wiggle<StructureElement.Loci>;
|
||||
}
|
||||
|
||||
function getUncertaintyValue(unit: Unit, element: number): number {
|
||||
if (Unit.isAtomic(unit)) {
|
||||
return unit.model.atomicConformation.B_iso_or_equiv.value(element);
|
||||
} else if (Unit.isSpheres(unit)) {
|
||||
return unit.model.coarseConformation.spheres.rmsf[element];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Compute min/max uncertainty (B-factor or RMSF) across all units in a structure */
|
||||
function getUncertaintyRange(structure: Structure): { min: number, max: number } {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
for (const unit of structure.units) {
|
||||
const elements = unit.elements;
|
||||
for (let j = 0, jl = elements.length; j < jl; j++) {
|
||||
const v = getUncertaintyValue(unit, elements[j]);
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
}
|
||||
if (!isFinite(min)) min = 0;
|
||||
if (!isFinite(max)) max = 0;
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set per-group wiggle based on B-factor/RMSF uncertainty data.
|
||||
* Values are normalized to [0, 1] within each structure's min-max range.
|
||||
* @param scale - maximum wiggle value (default 1.0, corresponds to Angstroms when combined with wiggleAmplitude)
|
||||
*/
|
||||
export async function setStructureWiggleFromUncertainty(plugin: PluginContext, components: StructureComponentRef[], scale: number = 1, types?: string[]) {
|
||||
await eachRepr(plugin, components, async (update, repr, wiggleCell) => {
|
||||
if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
|
||||
|
||||
const structure = repr.obj!.data.sourceData;
|
||||
const root = structure.root;
|
||||
const { min, max } = getUncertaintyRange(root);
|
||||
const range = max - min;
|
||||
if (range <= 0) return;
|
||||
|
||||
// Group elements by discretized uncertainty bucket (256 levels for Uint8 texture)
|
||||
const buckets = new Map<number, { unit: Unit, indices: number[] }[]>();
|
||||
|
||||
for (const unit of root.units) {
|
||||
const elements = unit.elements;
|
||||
const unitBuckets = new Map<number, number[]>();
|
||||
|
||||
for (let j = 0, jl = elements.length; j < jl; j++) {
|
||||
const v = getUncertaintyValue(unit, elements[j]);
|
||||
const normalized = (v - min) / range;
|
||||
const bucket = Math.min(255, Math.round(normalized * 255));
|
||||
if (!unitBuckets.has(bucket)) unitBuckets.set(bucket, []);
|
||||
unitBuckets.get(bucket)!.push(j);
|
||||
}
|
||||
|
||||
for (const [bucket, indices] of unitBuckets) {
|
||||
if (!buckets.has(bucket)) buckets.set(bucket, []);
|
||||
buckets.get(bucket)!.push({ unit, indices });
|
||||
}
|
||||
}
|
||||
|
||||
// Create one layer per bucket
|
||||
const bundleLayers: Wiggle.BundleLayer[] = [];
|
||||
for (const [bucket, unitIndices] of buckets) {
|
||||
const value = (bucket / 255) * scale;
|
||||
const elements: StructureElement.Loci['elements'][0][] = [];
|
||||
for (const { unit, indices } of unitIndices) {
|
||||
elements.push({
|
||||
unit,
|
||||
indices: OrderedSet.ofSortedArray(new Int32Array(indices) as any as StructureElement.UnitIndex[]),
|
||||
});
|
||||
}
|
||||
const loci = StructureElement.Loci(root, elements);
|
||||
if (!StructureElement.Loci.isEmpty(loci)) {
|
||||
bundleLayers.push({
|
||||
bundle: StructureElement.Bundle.fromLoci(loci),
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (bundleLayers.length === 0) return;
|
||||
|
||||
const filtered = getFilteredBundle(bundleLayers, structure);
|
||||
if (wiggleCell) {
|
||||
update.to(wiggleCell).update(Wiggle.toBundle(filtered));
|
||||
} else {
|
||||
update.to(repr.transform.ref)
|
||||
.apply(StateTransforms.Representation.WiggleStructureRepresentation3DFromBundle, Wiggle.toBundle(filtered), { tags: WiggleManagerTag });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -35,7 +35,9 @@ import { setStructureSubstance } from '../../helpers/structure-substance';
|
||||
import { Material } from '../../../mol-util/material';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { setStructureEmissive } from '../../helpers/structure-emissive';
|
||||
import { setStructureWiggle } from '../../helpers/structure-wiggle';
|
||||
import { areInteriorPropsEquals, getInteriorParam } from '../../../mol-geo/geometry/interior';
|
||||
import { areAnimationPropsEqual, getAnimationParam } from '../../../mol-geo/geometry/animation';
|
||||
|
||||
export { StructureComponentManager };
|
||||
|
||||
@@ -84,13 +86,14 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
|
||||
p.material = options.materialStyle;
|
||||
p.clip = options.clipObjects;
|
||||
p.interior = options.interior;
|
||||
p.animation = options.animation;
|
||||
});
|
||||
if (interactionChanged) await this.updateInterationProps();
|
||||
});
|
||||
}
|
||||
|
||||
private updateReprParams(update: StateBuilder.Root, component: StructureComponentRef) {
|
||||
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior } = this.state.options;
|
||||
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior, animation } = this.state.options;
|
||||
const ignoreHydrogens = hydrogens !== 'all';
|
||||
const ignoreHydrogensVariant = hydrogens === 'only-polar' ? 'non-polar' : 'all';
|
||||
for (const r of component.representations) {
|
||||
@@ -98,7 +101,8 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
|
||||
|
||||
const params = r.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
|
||||
const pInterior = params.type.params.interior;
|
||||
if (!!params.type.params.ignoreHydrogens !== ignoreHydrogens || params.type.params.ignoreHydrogensVariant !== ignoreHydrogensVariant || params.type.params.quality !== quality || params.type.params.ignoreLight !== ignoreLight || !Material.areEqual(params.type.params.material, material) || !PD.areEqual(Clip.Params, params.type.params.clip, clip) || (pInterior && !areInteriorPropsEquals(pInterior, interior))) {
|
||||
const pAnimation = params.type.params.animation;
|
||||
if (!!params.type.params.ignoreHydrogens !== ignoreHydrogens || params.type.params.ignoreHydrogensVariant !== ignoreHydrogensVariant || params.type.params.quality !== quality || params.type.params.ignoreLight !== ignoreLight || !Material.areEqual(params.type.params.material, material) || !PD.areEqual(Clip.Params, params.type.params.clip, clip) || (pInterior && !areInteriorPropsEquals(pInterior, interior)) || (pAnimation && !areAnimationPropsEqual(pAnimation, animation))) {
|
||||
update.to(r.cell).update(old => {
|
||||
old.type.params.ignoreHydrogens = ignoreHydrogens;
|
||||
old.type.params.ignoreHydrogensVariant = ignoreHydrogensVariant;
|
||||
@@ -107,6 +111,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
|
||||
old.type.params.material = material;
|
||||
old.type.params.clip = clip;
|
||||
if (pInterior) old.type.params.interior = interior;
|
||||
if (pAnimation) old.type.params.animation = animation;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -325,10 +330,10 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
|
||||
addRepresentation(components: ReadonlyArray<StructureComponentRef>, type: string) {
|
||||
if (components.length === 0) return;
|
||||
|
||||
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior } = this.state.options;
|
||||
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior, animation } = this.state.options;
|
||||
const ignoreHydrogens = hydrogens !== 'all';
|
||||
const ignoreHydrogensVariant = hydrogens === 'only-polar' ? 'non-polar' : 'all';
|
||||
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip, interior };
|
||||
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip, interior, animation };
|
||||
|
||||
return this.plugin.dataTransaction(async () => {
|
||||
for (const component of components) {
|
||||
@@ -363,10 +368,10 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
|
||||
const xs = structures || this.currentStructures;
|
||||
if (xs.length === 0) return;
|
||||
|
||||
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior } = this.state.options;
|
||||
const { hydrogens, visualQuality: quality, ignoreLight, materialStyle: material, clipObjects: clip, interior, animation } = this.state.options;
|
||||
const ignoreHydrogens = hydrogens !== 'all';
|
||||
const ignoreHydrogensVariant = hydrogens === 'only-polar' ? 'non-polar' : 'all';
|
||||
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip, interior };
|
||||
const typeParams = { ignoreHydrogens, ignoreHydrogensVariant, quality, ignoreLight, material, clip, interior, animation };
|
||||
|
||||
const componentKey = UUID.create22();
|
||||
for (const s of xs) {
|
||||
@@ -417,6 +422,9 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
|
||||
} else if (params.action.name === 'clipping') {
|
||||
const p = params.action.params;
|
||||
await setStructureClipping(this.plugin, s.components, Clipping.Groups.fromNames(p.excludeGroups), getLoci, params.representations);
|
||||
} else if (params.action.name === 'wiggle') {
|
||||
const p = params.action.params;
|
||||
await setStructureWiggle(this.plugin, s.components, p.value, getLoci, params.representations);
|
||||
}
|
||||
}
|
||||
}, { canUndo: 'Apply Theme' });
|
||||
@@ -488,6 +496,7 @@ namespace StructureComponentManager {
|
||||
clipObjects: PD.Group(Clip.Params),
|
||||
interactions: PD.Group(InteractionsProvider.defaultParams, { label: 'Non-covalent Interactions' }),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Options = PD.Values<typeof OptionsParams>
|
||||
|
||||
@@ -533,6 +542,9 @@ namespace StructureComponentManager {
|
||||
clipping: PD.Group({
|
||||
excludeGroups: PD.MultiSelect([] as Clipping.Groups.Names[], PD.objectToOptions(Clipping.Groups.Names)),
|
||||
}, { isFlat: true }),
|
||||
wiggle: PD.Group({
|
||||
value: PD.Numeric(1, { min: 0, max: 5, step: 0.01 }),
|
||||
}, { isFlat: true }),
|
||||
}),
|
||||
representations: PD.MultiSelect([], getRepresentationTypes(plugin, pivot), { emptyValue: 'All' })
|
||||
};
|
||||
|
||||
@@ -1304,7 +1304,7 @@ const ShapeFromPly = PluginStateTransform.BuiltIn({
|
||||
to: SO.Shape.Provider,
|
||||
params(a) {
|
||||
return {
|
||||
transforms: PD.Optional(PD.Value<Mat4[]>([], { isHidden: true })),
|
||||
transforms: PD.Optional(PD.Value([Mat4.identity()], { isHidden: true })),
|
||||
label: PD.Optional(PD.Text('', { isHidden: true }))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { Material } from '../../mol-util/material';
|
||||
import { lerp } from '../../mol-math/interpolate';
|
||||
import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
|
||||
import { Emissive } from '../../mol-theme/emissive';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
|
||||
export { StructureRepresentation3D };
|
||||
export { ExplodeStructureRepresentation3D };
|
||||
@@ -60,6 +61,8 @@ export { SubstanceStructureRepresentation3DFromScript };
|
||||
export { SubstanceStructureRepresentation3DFromBundle };
|
||||
export { ClippingStructureRepresentation3DFromScript };
|
||||
export { ClippingStructureRepresentation3DFromBundle };
|
||||
export { WiggleStructureRepresentation3DFromScript };
|
||||
export { WiggleStructureRepresentation3DFromBundle };
|
||||
export { ThemeStrengthRepresentation3D };
|
||||
export { VolumeRepresentation3D };
|
||||
|
||||
@@ -862,6 +865,109 @@ const ClippingStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn
|
||||
}
|
||||
});
|
||||
|
||||
type WiggleStructureRepresentation3DFromScript = typeof WiggleStructureRepresentation3DFromScript
|
||||
const WiggleStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn({
|
||||
name: 'wiggle-structure-representation-3d-from-script',
|
||||
display: 'Wiggle 3D Representation',
|
||||
from: SO.Molecule.Structure.Representation3D,
|
||||
to: SO.Molecule.Structure.Representation3DState,
|
||||
params: () => ({
|
||||
layers: PD.ObjectList({
|
||||
script: PD.Script(Script('(sel.atom.all)', 'mol-script')),
|
||||
value: PD.Numeric(5, { min: 0, max: 1, step: 0.01 }, { label: 'Wiggle' }),
|
||||
}, e => `Wiggle (${e.value})`, {
|
||||
defaultValue: [{
|
||||
script: Script('(sel.atom.all)', 'mol-script'),
|
||||
value: 1,
|
||||
}]
|
||||
})
|
||||
})
|
||||
})({
|
||||
canAutoUpdate() {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params }) {
|
||||
const structure = a.data.sourceData;
|
||||
const geometryVersion = a.data.repr.geometryVersion;
|
||||
const wiggle = Wiggle.ofScript(params.layers, structure);
|
||||
|
||||
return new SO.Molecule.Structure.Representation3DState({
|
||||
state: { wiggle },
|
||||
initialState: { wiggle: Wiggle.Empty },
|
||||
info: { structure, geometryVersion },
|
||||
repr: a.data.repr
|
||||
}, { label: `Wiggle (${wiggle.layers.length} Layers)` });
|
||||
},
|
||||
update({ a, b, newParams, oldParams }) {
|
||||
const info = b.data.info as { structure: Structure, geometryVersion: number };
|
||||
const newStructure = a.data.sourceData;
|
||||
if (newStructure !== info.structure) return StateTransformer.UpdateResult.Recreate;
|
||||
if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
|
||||
|
||||
const oldWiggle = b.data.state.wiggle!;
|
||||
const newWiggle = Wiggle.ofScript(newParams.layers, newStructure);
|
||||
if (Wiggle.areEqual(oldWiggle, newWiggle)) return StateTransformer.UpdateResult.Unchanged;
|
||||
|
||||
info.geometryVersion = a.data.repr.geometryVersion;
|
||||
b.data.state.wiggle = newWiggle;
|
||||
b.data.repr = a.data.repr;
|
||||
b.label = `Wiggle (${newWiggle.layers.length} Layers)`;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
}
|
||||
});
|
||||
|
||||
type WiggleStructureRepresentation3DFromBundle = typeof WiggleStructureRepresentation3DFromBundle
|
||||
const WiggleStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn({
|
||||
name: 'wiggle-structure-representation-3d-from-bundle',
|
||||
display: 'Wiggle 3D Representation',
|
||||
from: SO.Molecule.Structure.Representation3D,
|
||||
to: SO.Molecule.Structure.Representation3DState,
|
||||
params: () => ({
|
||||
layers: PD.ObjectList({
|
||||
bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
|
||||
value: PD.Numeric(5, { min: 0, max: 1, step: 0.01 }, { label: 'Wiggle' }),
|
||||
}, e => `Wiggle (${e.value})`, {
|
||||
defaultValue: [{
|
||||
bundle: StructureElement.Bundle.Empty,
|
||||
value: 1,
|
||||
}],
|
||||
isHidden: true
|
||||
})
|
||||
})
|
||||
})({
|
||||
canAutoUpdate() {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params }) {
|
||||
const structure = a.data.sourceData;
|
||||
const geometryVersion = a.data.repr.geometryVersion;
|
||||
const wiggle = Wiggle.ofBundle(params.layers, structure);
|
||||
|
||||
return new SO.Molecule.Structure.Representation3DState({
|
||||
state: { wiggle },
|
||||
initialState: { wiggle: Wiggle.Empty },
|
||||
info: { structure, geometryVersion },
|
||||
repr: a.data.repr
|
||||
}, { label: `Wiggle (${wiggle.layers.length} Layers)` });
|
||||
},
|
||||
update({ a, b, newParams, oldParams }) {
|
||||
const info = b.data.info as { structure: Structure, geometryVersion: number };
|
||||
const newStructure = a.data.sourceData;
|
||||
if (newStructure !== info.structure) return StateTransformer.UpdateResult.Recreate;
|
||||
if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
|
||||
|
||||
const oldWiggle = b.data.state.wiggle!;
|
||||
const newWiggle = Wiggle.ofBundle(newParams.layers, newStructure);
|
||||
if (Wiggle.areEqual(oldWiggle, newWiggle)) return StateTransformer.UpdateResult.Unchanged;
|
||||
|
||||
info.geometryVersion = a.data.repr.geometryVersion;
|
||||
b.data.state.wiggle = newWiggle;
|
||||
b.data.repr = a.data.repr;
|
||||
b.label = `Wiggle (${newWiggle.layers.length} Layers)`;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
}
|
||||
});
|
||||
|
||||
type ThemeStrengthRepresentation3D = typeof ThemeStrengthRepresentation3D
|
||||
const ThemeStrengthRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
name: 'theme-strength-representation-3d',
|
||||
@@ -873,6 +979,7 @@ const ThemeStrengthRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
transparencyStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
emissiveStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
substanceStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
wiggleStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
})
|
||||
})({
|
||||
canAutoUpdate() {
|
||||
@@ -885,21 +992,23 @@ const ThemeStrengthRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
overpaint: params.overpaintStrength,
|
||||
transparency: params.transparencyStrength,
|
||||
emissive: params.emissiveStrength,
|
||||
substance: params.substanceStrength
|
||||
substance: params.substanceStrength,
|
||||
wiggle: params.wiggleStrength,
|
||||
},
|
||||
},
|
||||
initialState: {
|
||||
themeStrength: { overpaint: 1, transparency: 1, emissive: 1, substance: 1 },
|
||||
themeStrength: { overpaint: 1, transparency: 1, emissive: 1, substance: 1, wiggle: 1 },
|
||||
},
|
||||
info: { },
|
||||
repr: a.data.repr
|
||||
}, { label: 'Theme Strength', description: `${params.overpaintStrength.toFixed(2)}, ${params.transparencyStrength.toFixed(2)}, ${params.emissiveStrength.toFixed(2)}, ${params.substanceStrength.toFixed(2)}` });
|
||||
}, { label: 'Theme Strength', description: `${params.overpaintStrength.toFixed(2)}, ${params.transparencyStrength.toFixed(2)}, ${params.emissiveStrength.toFixed(2)}, ${params.substanceStrength.toFixed(2)}, ${params.wiggleStrength.toFixed(2)}` });
|
||||
},
|
||||
update({ a, b, newParams, oldParams }) {
|
||||
if (newParams.overpaintStrength === b.data.state.themeStrength?.overpaint &&
|
||||
newParams.transparencyStrength === b.data.state.themeStrength?.transparency &&
|
||||
newParams.emissiveStrength === b.data.state.themeStrength?.emissive &&
|
||||
newParams.substanceStrength === b.data.state.themeStrength?.substance
|
||||
newParams.substanceStrength === b.data.state.themeStrength?.substance &&
|
||||
newParams.wiggleStrength === b.data.state.themeStrength?.wiggle
|
||||
) return StateTransformer.UpdateResult.Unchanged;
|
||||
|
||||
b.data.state.themeStrength = {
|
||||
@@ -907,10 +1016,11 @@ const ThemeStrengthRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
transparency: newParams.transparencyStrength,
|
||||
emissive: newParams.emissiveStrength,
|
||||
substance: newParams.substanceStrength,
|
||||
wiggle: newParams.wiggleStrength,
|
||||
};
|
||||
b.data.repr = a.data.repr;
|
||||
b.label = 'Theme Strength';
|
||||
b.description = `${newParams.overpaintStrength.toFixed(2)}, ${newParams.transparencyStrength.toFixed(2)}, ${newParams.emissiveStrength.toFixed(2)}, ${newParams.substanceStrength.toFixed(2)}`;
|
||||
b.description = `${newParams.overpaintStrength.toFixed(2)}, ${newParams.transparencyStrength.toFixed(2)}, ${newParams.emissiveStrength.toFixed(2)}, ${newParams.substanceStrength.toFixed(2)}, ${newParams.wiggleStrength.toFixed(2)}`;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
},
|
||||
interpolate(src, tar, t) {
|
||||
@@ -919,6 +1029,7 @@ const ThemeStrengthRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
transparencyStrength: lerp(src.transparencyStrength, tar.transparencyStrength, t),
|
||||
emissiveStrength: lerp(src.emissiveStrength, tar.emissiveStrength, t),
|
||||
substanceStrength: lerp(src.substanceStrength, tar.substanceStrength, t),
|
||||
wiggleStrength: lerp(src.wiggleStrength, tar.wiggleStrength, t),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -26,6 +26,7 @@ import { VolumeStreamingControls, VolumeSourceControls } from './structure/volum
|
||||
import { PluginConfig } from '../mol-plugin/config';
|
||||
import { StructureSuperpositionControls } from './structure/superposition';
|
||||
import { StructureQuickStylesControls } from './structure/quick-styles';
|
||||
import { StructureProceduralAnimationControls } from './structure/procedural-animation';
|
||||
import { Markdown } from './controls/markdown';
|
||||
import { Slider } from './controls/slider';
|
||||
import { AnimateStateSnapshotTransition } from '../mol-plugin-state/animation/built-in/state-snapshots';
|
||||
@@ -336,6 +337,7 @@ export class DefaultStructureTools extends PluginUIComponent {
|
||||
<StructureMeasurementsControls />
|
||||
<StructureSuperpositionControls />
|
||||
<StructureQuickStylesControls />
|
||||
<StructureProceduralAnimationControls />
|
||||
<StructureComponentControls />
|
||||
{this.plugin.config.get(PluginConfig.VolumeStreaming.Enabled) && <VolumeStreamingControls />}
|
||||
<VolumeSourceControls />
|
||||
|
||||
@@ -1414,8 +1414,8 @@ class ObjectListItem extends React.PureComponent<ObjectListItemProps, { isExpand
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean }> {
|
||||
state = { isExpanded: false };
|
||||
export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean, showPresets: boolean }> {
|
||||
state = { isExpanded: false, showPresets: false };
|
||||
|
||||
change(value: any) {
|
||||
this.props.onChange({ name: this.props.name, param: this.props.param, value });
|
||||
@@ -1459,12 +1459,29 @@ export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectL
|
||||
e.currentTarget.blur();
|
||||
};
|
||||
|
||||
toggleShowPresets = () => this.setState({ showPresets: !this.state.showPresets });
|
||||
|
||||
presetItems = memoizeLatest((param: PD.ObjectList) => ActionMenu.createItemsFromSelectOptions(param.presets ?? []));
|
||||
|
||||
onSelectPreset: ActionMenu.OnSelect = item => {
|
||||
this.setState({ showPresets: false });
|
||||
this.change(item?.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const v = this.props.value;
|
||||
const label = this.props.param.label || camelCaseToWords(this.props.name);
|
||||
const value = `${v.length} item${v.length !== 1 ? 's' : ''}`;
|
||||
const hasPresets = !!this.props.param.presets;
|
||||
const control = hasPresets
|
||||
? <div className='msp-flex-row'>
|
||||
<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>
|
||||
<IconButton svg={BookmarksOutlinedSvg} onClick={this.toggleShowPresets} toggleState={this.state.showPresets} title='Presets' disabled={this.props.isDisabled} />
|
||||
</div>
|
||||
: <button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>;
|
||||
return <>
|
||||
<ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
|
||||
<ControlRow label={label} control={control} />
|
||||
{hasPresets && this.state.showPresets && <ActionMenu items={this.presetItems(this.props.param)} onSelect={this.onSelectPreset} />}
|
||||
{this.state.isExpanded && <div className='msp-control-offset'>
|
||||
{this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} isDisabled={this.props.isDisabled} />)}
|
||||
<ControlGroup header='New Item'>
|
||||
|
||||
@@ -181,8 +181,8 @@ export class Slider2 extends React.Component<{
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
function classNames(_classes: { [name: string]: boolean | number }) {
|
||||
const classes = [];
|
||||
function classNames(_classes: { [name: string]: boolean | number }): string {
|
||||
const classes: string[] = [];
|
||||
const hasOwn = {}.hasOwnProperty;
|
||||
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
@@ -194,7 +194,7 @@ function classNames(_classes: { [name: string]: boolean | number }) {
|
||||
if (argType === 'string' || argType === 'number') {
|
||||
classes.push(arg);
|
||||
} else if (Array.isArray(arg)) {
|
||||
classes.push(classNames.apply(null, arg));
|
||||
classes.push(classNames.apply(null, arg as any));
|
||||
} else if (argType === 'object') {
|
||||
for (const key in arg) {
|
||||
if (hasOwn.call(arg, key) && arg[key]) {
|
||||
@@ -290,6 +290,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
|
||||
const defaultValue = ('defaultValue' in props ? props.defaultValue : initialValue);
|
||||
const value = (props.value !== undefined ? props.value : defaultValue);
|
||||
|
||||
// @ts-ignore
|
||||
const bounds = (range ? value : [min, value]).map((v: number) => this.trimAlignValue(v));
|
||||
|
||||
let recent;
|
||||
|
||||
113
src/mol-plugin-ui/structure/procedural-animation.tsx
Normal file
113
src/mol-plugin-ui/structure/procedural-animation.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { clearStructureWiggle, setStructureWiggleFromUncertainty } from '../../mol-plugin-state/helpers/structure-wiggle';
|
||||
import { CollapsableControls, PurePluginUIComponent } from '../base';
|
||||
import { Button } from '../controls/common';
|
||||
import { AnimationSvg } from '../controls/icons';
|
||||
|
||||
export class StructureProceduralAnimationControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
isCollapsed: false,
|
||||
header: 'Procedural Animation',
|
||||
brand: { accent: 'gray' as const, svg: AnimationSvg }
|
||||
};
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
return <StructureProceduralAnimation />;
|
||||
}
|
||||
}
|
||||
|
||||
interface StructureProceduralAnimationState {
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
class StructureProceduralAnimation extends PurePluginUIComponent<{}, StructureProceduralAnimationState> {
|
||||
state: StructureProceduralAnimationState = { busy: false };
|
||||
|
||||
private get components() {
|
||||
return this.plugin.managers.structure.hierarchy.selection.structures.flatMap(s => s.components);
|
||||
}
|
||||
|
||||
async applyUncertaintyWiggle() {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
const options = this.plugin.managers.structure.component.state.options;
|
||||
await this.plugin.managers.structure.component.setOptions({
|
||||
...options,
|
||||
animation: {
|
||||
...options.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0
|
||||
}
|
||||
});
|
||||
await setStructureWiggleFromUncertainty(this.plugin, this.components);
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
|
||||
async applyDynamics() {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
const options = this.plugin.managers.structure.component.state.options;
|
||||
await this.plugin.managers.structure.component.setOptions({
|
||||
...options,
|
||||
animation: {
|
||||
...options.animation,
|
||||
wiggleSpeed: 7,
|
||||
wiggleAmplitude: 1,
|
||||
wiggleFrequency: 0.2,
|
||||
}
|
||||
});
|
||||
await clearStructureWiggle(this.plugin, this.components);
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
|
||||
async clearWiggle() {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
const options = this.plugin.managers.structure.component.state.options;
|
||||
await this.plugin.managers.structure.component.setOptions({
|
||||
...options,
|
||||
animation: {
|
||||
...options.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0
|
||||
}
|
||||
});
|
||||
await clearStructureWiggle(this.plugin, this.components);
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<div className='msp-control-group-wrapper'>
|
||||
<div className='msp-control-group-header'><div><b>Apply Wiggle</b></div></div>
|
||||
<div className='msp-flex-row'>
|
||||
<Button title='Set wiggle speed to 8 and amplitude to 1'
|
||||
onClick={() => this.applyDynamics()} disabled={this.state.busy} >
|
||||
Dynamics
|
||||
</Button>
|
||||
<Button title='Set per-group wiggle amplitude based on B-factor / RMSF uncertainty'
|
||||
onClick={() => this.applyUncertaintyWiggle()} disabled={this.state.busy} >
|
||||
Uncertainty
|
||||
</Button>
|
||||
<Button title='Set wiggle/tumble amplitude to zero and remove per-group wiggle layers'
|
||||
onClick={() => this.clearWiggle()} disabled={this.state.busy} >
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
@@ -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 David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -21,6 +21,7 @@ import { PluginContext } from '../../../context';
|
||||
import { Material } from '../../../../mol-util/material';
|
||||
import { Clip } from '../../../../mol-util/clip';
|
||||
import { getInteriorParam } from '../../../../mol-geo/geometry/interior';
|
||||
import { getAnimationParam } from '../../../../mol-geo/geometry/animation';
|
||||
|
||||
const StructureFocusRepresentationParams = (plugin: PluginContext) => {
|
||||
const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(void 0, plugin) as PD.Params;
|
||||
@@ -58,6 +59,7 @@ const StructureFocusRepresentationParams = (plugin: PluginContext) => {
|
||||
material: Material.getParam(),
|
||||
clip: PD.Group(Clip.Params),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,7 +85,7 @@ class StructureFocusRepresentationBehavior extends PluginBehavior.WithSubscriber
|
||||
...reprParams,
|
||||
type: {
|
||||
name: reprParams.type.name,
|
||||
params: { ...reprParams.type.params, ignoreHydrogens: this.params.ignoreHydrogens, ignoreHydrogensVariant: this.params.ignoreHydrogensVariant, ignoreLight: this.params.ignoreLight, material: this.params.material, clip: this.params.clip, interior: this.params.interior }
|
||||
params: { ...reprParams.type.params, ignoreHydrogens: this.params.ignoreHydrogens, ignoreHydrogensVariant: this.params.ignoreHydrogensVariant, ignoreLight: this.params.ignoreLight, material: this.params.material, clip: this.params.clip, interior: this.params.interior, animation: this.params.animation }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { BoxifyVolumeStreaming, CreateVolumeStreamingBehavior, InitVolumeStreami
|
||||
import { AnimateStateInterpolation } from '../mol-plugin-state/animation/built-in/state-interpolation';
|
||||
import { AnimateStructureSpin } from '../mol-plugin-state/animation/built-in/spin-structure';
|
||||
import { AnimateCameraRock } from '../mol-plugin-state/animation/built-in/camera-rock';
|
||||
import { AnimateTime } from '../mol-plugin-state/animation/built-in/time';
|
||||
|
||||
export { PluginSpec };
|
||||
|
||||
@@ -105,6 +106,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
|
||||
PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3DFromScript),
|
||||
PluginSpec.Action(StateTransforms.Representation.ClippingStructureRepresentation3DFromScript),
|
||||
PluginSpec.Action(StateTransforms.Representation.SubstanceStructureRepresentation3DFromScript),
|
||||
PluginSpec.Action(StateTransforms.Representation.WiggleStructureRepresentation3DFromScript),
|
||||
PluginSpec.Action(StateTransforms.Representation.ThemeStrengthRepresentation3D),
|
||||
|
||||
PluginSpec.Action(AssignColorVolume),
|
||||
@@ -144,6 +146,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
|
||||
AnimateStateSnapshotTransition,
|
||||
AnimateAssemblyUnwind,
|
||||
AnimateStructureSpin,
|
||||
AnimateStateInterpolation
|
||||
AnimateStateInterpolation,
|
||||
AnimateTime
|
||||
]
|
||||
});
|
||||
@@ -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>
|
||||
@@ -28,6 +28,7 @@ import { SetUtils } from '../mol-util/set';
|
||||
import { cantorPairing } from '../mol-data/util';
|
||||
import { Substance } from '../mol-theme/substance';
|
||||
import { Emissive } from '../mol-theme/emissive';
|
||||
import { Wiggle } from '../mol-theme/wiggle';
|
||||
import { Location } from '../mol-model/location';
|
||||
|
||||
export type RepresentationProps = { [k: string]: any }
|
||||
@@ -202,10 +203,12 @@ namespace Representation {
|
||||
emissive: Emissive
|
||||
/** Per group material applied to the representation's renderobjects */
|
||||
substance: Substance
|
||||
/** Per group wiggle applied to the representation's renderobjects */
|
||||
wiggle: Wiggle
|
||||
/** Bit mask of per group clipping applied to the representation's renderobjects */
|
||||
clipping: Clipping
|
||||
/** Strength of the representations overpaint, transparency, emmissive, substance*/
|
||||
themeStrength: { overpaint: number, transparency: number, emissive: number, substance: number }
|
||||
/** Strength of the representations overpaint, transparency, emissive, substance, wiggle */
|
||||
themeStrength: { overpaint: number, transparency: number, emissive: number, substance: number, wiggle: number }
|
||||
/** Controls if the representation's renderobjects are synced automatically with GPU or not */
|
||||
syncManually: boolean
|
||||
/** A transformation applied to the representation's renderobjects */
|
||||
@@ -225,8 +228,9 @@ namespace Representation {
|
||||
transparency: Transparency.Empty,
|
||||
emissive: Emissive.Empty,
|
||||
substance: Substance.Empty,
|
||||
wiggle: Wiggle.Empty,
|
||||
clipping: Clipping.Empty,
|
||||
themeStrength: { overpaint: 1, transparency: 1, emissive: 1, substance: 1 },
|
||||
themeStrength: { overpaint: 1, transparency: 1, emissive: 1, substance: 1, wiggle: 1 },
|
||||
markerActions: MarkerActions.All
|
||||
};
|
||||
}
|
||||
@@ -239,6 +243,7 @@ namespace Representation {
|
||||
if (update.transparency !== undefined) state.transparency = update.transparency;
|
||||
if (update.emissive !== undefined) state.emissive = update.emissive;
|
||||
if (update.substance !== undefined) state.substance = update.substance;
|
||||
if (update.wiggle !== undefined) state.wiggle = update.wiggle;
|
||||
if (update.clipping !== undefined) state.clipping = update.clipping;
|
||||
if (update.themeStrength !== undefined) state.themeStrength = update.themeStrength;
|
||||
if (update.syncManually !== undefined) state.syncManually = update.syncManually;
|
||||
@@ -492,6 +497,9 @@ namespace Representation {
|
||||
if (state.substance !== undefined) {
|
||||
// TODO
|
||||
}
|
||||
if (state.wiggle !== undefined) {
|
||||
// TODO
|
||||
}
|
||||
if (state.clipping !== undefined) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
|
||||
import { resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
import { Representation } from '../representation';
|
||||
import { Shape, ShapeGroup } from '../../mol-model/shape';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -129,7 +130,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
|
||||
// console.log('update transform')
|
||||
locationIt = Shape.groupIterator(_shape);
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (props.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', _renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', _renderObject.values);
|
||||
@@ -197,14 +198,15 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, _shape.groupCount, _shape.transforms.length);
|
||||
if (isEveryLoci(loci) || (Shape.isLoci(loci) && loci.shape === _shape)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, _shape.transforms.length));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, _shape.groupCount * _shape.transforms.length));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, _shape, apply);
|
||||
} else {
|
||||
return eachShapeGroup(loci, _shape, apply);
|
||||
@@ -226,7 +228,8 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
|
||||
getLoci(pickingId: PickingId) {
|
||||
const { objectId, groupId, instanceId } = pickingId;
|
||||
if (_renderObject && _renderObject.id === objectId) {
|
||||
if (groupId === PickingId.Null) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, _shape.groupCount, _shape.transforms.length);
|
||||
if (groupId === PickingId.Null || instanceGranularity) {
|
||||
return Shape.Loci(_shape);
|
||||
} else {
|
||||
return ShapeGroup.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId), instance: instanceId }]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 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>
|
||||
@@ -24,6 +24,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Substance } from '../../mol-theme/substance';
|
||||
import { LocationCallback } from '../util';
|
||||
import { Emissive } from '../../mol-theme/emissive';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
|
||||
export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, structure: Structure, props: PD.Values<P>, webgl?: WebGLContext) => ComplexVisual<P>): StructureRepresentation<P> {
|
||||
let version = 0;
|
||||
@@ -138,6 +139,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
|
||||
const remappedClipping = Clipping.remap(state.clipping, _structure);
|
||||
visual.setClipping(remappedClipping);
|
||||
}
|
||||
if (state.wiggle !== undefined && visual) {
|
||||
// Remap loci from equivalent structure to the current structure
|
||||
const remappedWiggle = Wiggle.remap(state.wiggle, _structure);
|
||||
visual.setWiggle(remappedWiggle, webgl);
|
||||
}
|
||||
if (state.themeStrength !== undefined && visual) visual.setThemeStrength(state.themeStrength);
|
||||
if (state.transform !== undefined && visual) visual.setTransform(state.transform);
|
||||
if (state.unitTransforms !== undefined && visual) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Text } from '../../mol-geo/geometry/text/text';
|
||||
import { SizeTheme } from '../../mol-theme/size';
|
||||
import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
|
||||
import { createMarkers } from '../../mol-geo/geometry/marker-data';
|
||||
import { resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams, StructureCylindersParams, StructureTextureMeshParams, StructureSpheresParams, StructurePointsParams, StructureImageParams } from './params';
|
||||
import { Clipping } from '../../mol-theme/clipping';
|
||||
import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
|
||||
@@ -40,6 +41,7 @@ import { isPromiseLike } from '../../mol-util/type-helpers';
|
||||
import { Substance } from '../../mol-theme/substance';
|
||||
import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
|
||||
import { Emissive } from '../../mol-theme/emissive';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
import { Points } from '../../mol-geo/geometry/points/points';
|
||||
import { Image } from '../../mol-geo/geometry/image/image';
|
||||
|
||||
@@ -172,7 +174,7 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
|
||||
if (updateState.updateTransform) {
|
||||
// console.log('update transform')
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (newProps.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
|
||||
@@ -236,14 +238,15 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount);
|
||||
if (lociIsSuperset(loci)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, locationIt.instanceCount));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, currentStructure, apply);
|
||||
} else {
|
||||
return eachLocation(loci, currentStructure, apply, isMarking);
|
||||
@@ -278,7 +281,11 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
|
||||
finalize(ctx);
|
||||
},
|
||||
getLoci(pickingId: PickingId) {
|
||||
return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci;
|
||||
if (!renderObject) return EmptyLoci;
|
||||
if (resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount)) {
|
||||
pickingId = { ...pickingId, groupId: PickingId.Null };
|
||||
}
|
||||
return getLoci(pickingId, currentStructure, renderObject.id);
|
||||
},
|
||||
eachLocation(cb: LocationCallback) {
|
||||
locationIt.reset();
|
||||
@@ -324,7 +331,10 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
|
||||
setClipping(clipping: Clipping) {
|
||||
Visual.setClipping(renderObject, clipping, lociApply, true);
|
||||
},
|
||||
setThemeStrength(strength: { overpaint: number, transparency: number, emissive: number, substance: number }) {
|
||||
setWiggle(wiggle: Wiggle, webgl?: WebGLContext) {
|
||||
Visual.setWiggle(renderObject, wiggle, lociApply, true);
|
||||
},
|
||||
setThemeStrength(strength: { overpaint: number, transparency: number, emissive: number, substance: number, wiggle: number }) {
|
||||
Visual.setThemeStrength(renderObject, strength);
|
||||
},
|
||||
destroy() {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { StructureGroup } from './visual/util/common';
|
||||
import { Substance } from '../../mol-theme/substance';
|
||||
import { LocationCallback } from '../util';
|
||||
import { Emissive } from '../../mol-theme/emissive';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
import { HashMap } from '../../mol-util/map';
|
||||
import { hash2 } from '../../mol-data/util';
|
||||
|
||||
@@ -236,7 +237,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
|
||||
}
|
||||
|
||||
function setVisualState(visual: UnitsVisual<P>, group: Unit.SymmetryGroup, state: Partial<StructureRepresentationState>) {
|
||||
const { visible, alphaFactor, pickable, overpaint, transparency, emissive, substance, clipping, themeStrength, transform, unitTransforms } = state;
|
||||
const { visible, alphaFactor, pickable, overpaint, transparency, emissive, substance, clipping, wiggle, themeStrength, transform, unitTransforms } = state;
|
||||
|
||||
if (visible !== undefined) visual.setVisibility(visible);
|
||||
if (alphaFactor !== undefined) visual.setAlphaFactor(alphaFactor);
|
||||
@@ -246,6 +247,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
|
||||
if (emissive !== undefined) visual.setEmissive(emissive, webgl);
|
||||
if (substance !== undefined) visual.setSubstance(substance, webgl);
|
||||
if (clipping !== undefined) visual.setClipping(clipping);
|
||||
if (wiggle !== undefined) visual.setWiggle(wiggle, webgl);
|
||||
if (themeStrength !== undefined) visual.setThemeStrength(themeStrength);
|
||||
if (transform !== undefined) {
|
||||
if (transform !== _state.transform || !Mat4.areEqual(transform, _state.transform, EPSILON)) {
|
||||
@@ -263,7 +265,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
|
||||
}
|
||||
|
||||
function setState(state: Partial<StructureRepresentationState>) {
|
||||
const { visible, alphaFactor, pickable, overpaint, transparency, emissive, substance, clipping, themeStrength, transform, unitTransforms, syncManually, markerActions } = state;
|
||||
const { visible, alphaFactor, pickable, overpaint, transparency, emissive, substance, clipping, wiggle, themeStrength, transform, unitTransforms, syncManually, markerActions } = state;
|
||||
const newState: Partial<StructureRepresentationState> = {};
|
||||
|
||||
if (visible !== undefined) newState.visible = visible;
|
||||
@@ -284,6 +286,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
|
||||
if (clipping !== undefined && _structure) {
|
||||
newState.clipping = Clipping.remap(clipping, _structure);
|
||||
}
|
||||
if (wiggle !== undefined && _structure) {
|
||||
newState.wiggle = Wiggle.remap(wiggle, _structure);
|
||||
}
|
||||
if (themeStrength !== undefined) newState.themeStrength = themeStrength;
|
||||
if (transform !== undefined && !Mat4.areEqual(transform, _state.transform, EPSILON)) {
|
||||
newState.transform = transform;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Interval } from '../../mol-data/int';
|
||||
import { LocationCallback, VisualUpdateState } from '../util';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { createMarkers } from '../../mol-geo/geometry/marker-data';
|
||||
import { resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
import { MarkerAction } from '../../mol-util/marker-action';
|
||||
import { ValueCell, deepEqual } from '../../mol-util';
|
||||
import { createSizes } from '../../mol-geo/geometry/size-data';
|
||||
@@ -44,6 +45,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { isPromiseLike } from '../../mol-util/type-helpers';
|
||||
import { Substance } from '../../mol-theme/substance';
|
||||
import { Emissive } from '../../mol-theme/emissive';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
|
||||
export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
|
||||
|
||||
@@ -213,7 +215,7 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
|
||||
if (updateState.updateTransform) {
|
||||
// console.log('update transform');
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (newProps.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
|
||||
@@ -312,14 +314,15 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount);
|
||||
if (lociIsSuperset(loci)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, locationIt.instanceCount));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, currentStructureGroup, apply);
|
||||
} else {
|
||||
return eachLocation(loci, currentStructureGroup, apply, isMarking);
|
||||
@@ -354,7 +357,11 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
|
||||
finalize(ctx);
|
||||
},
|
||||
getLoci(pickingId: PickingId) {
|
||||
return renderObject ? getLoci(pickingId, currentStructureGroup, renderObject.id) : EmptyLoci;
|
||||
if (!renderObject) return EmptyLoci;
|
||||
if (resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount)) {
|
||||
pickingId = { ...pickingId, groupId: PickingId.Null };
|
||||
}
|
||||
return getLoci(pickingId, currentStructureGroup, renderObject.id);
|
||||
},
|
||||
eachLocation(cb: LocationCallback) {
|
||||
locationIt.reset();
|
||||
@@ -411,7 +418,10 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
|
||||
setClipping(clipping: Clipping) {
|
||||
Visual.setClipping(renderObject, clipping, lociApply, true);
|
||||
},
|
||||
setThemeStrength(strength: { overpaint: number, transparency: number, emissive: number, substance: number }) {
|
||||
setWiggle(wiggle: Wiggle, webgl?: WebGLContext) {
|
||||
Visual.setWiggle(renderObject, wiggle, lociApply, true);
|
||||
},
|
||||
setThemeStrength(strength: { overpaint: number, transparency: number, emissive: number, substance: number, wiggle: number }) {
|
||||
Visual.setThemeStrength(renderObject, strength);
|
||||
},
|
||||
destroy() {
|
||||
|
||||
@@ -34,6 +34,8 @@ import { applySubstanceMaterial, clearSubstance, createSubstance } from '../mol-
|
||||
import { LocationCallback } from './util';
|
||||
import { Emissive } from '../mol-theme/emissive';
|
||||
import { applyEmissiveValue, clearEmissive, createEmissive, getEmissiveAverage } from '../mol-geo/geometry/emissive-data';
|
||||
import { Wiggle } from '../mol-theme/wiggle';
|
||||
import { applyWiggleValue, clearWiggle, createWiggle, getWiggleAverage } from '../mol-geo/geometry/wiggle-data';
|
||||
|
||||
export interface VisualContext {
|
||||
readonly runtime: RuntimeContext
|
||||
@@ -60,7 +62,8 @@ interface Visual<D, P extends PD.Params> {
|
||||
setEmissive: (emissive: Emissive, webgl?: WebGLContext) => void
|
||||
setSubstance: (substance: Substance, webgl?: WebGLContext) => void
|
||||
setClipping: (clipping: Clipping) => void
|
||||
setThemeStrength: (strength: { overpaint: number, transparency: number, emissive: number, substance: number }) => void
|
||||
setWiggle: (wiggle: Wiggle, webgl?: WebGLContext) => void
|
||||
setThemeStrength: (strength: { overpaint: number, transparency: number, emissive: number, substance: number, wiggle: number }) => void
|
||||
destroy: () => void
|
||||
mustRecreate?: (data: D, props: PD.Values<P>, webgl?: WebGLContext) => boolean
|
||||
}
|
||||
@@ -410,12 +413,44 @@ namespace Visual {
|
||||
ValueCell.updateIfChanged(dClipping, clipping.layers.length > 0);
|
||||
}
|
||||
|
||||
export function setThemeStrength(renderObject: GraphicsRenderObject | undefined, strength: { overpaint: number, transparency: number, emissive: number, substance: number }) {
|
||||
export function setWiggle(renderObject: GraphicsRenderObject | undefined, wiggle: Wiggle, lociApply: LociApply, clear: boolean) {
|
||||
if (!renderObject) return;
|
||||
|
||||
const { tWiggle, dWiggleType, wiggleAverage, dWiggle, uGroupCount, instanceCount, instanceGranularity: instanceGranularity } = renderObject.values;
|
||||
const count = instanceGranularity.ref.value
|
||||
? instanceCount.ref.value
|
||||
: uGroupCount.ref.value * instanceCount.ref.value;
|
||||
|
||||
// ensure texture has right size and type
|
||||
const type = instanceGranularity.ref.value ? 'instance' : 'groupInstance';
|
||||
createWiggle(wiggle.layers.length ? count : 0, type, renderObject.values);
|
||||
const { array } = tWiggle.ref.value;
|
||||
|
||||
// clear if requested
|
||||
if (clear) clearWiggle(array, 0, count);
|
||||
|
||||
for (let i = 0, il = wiggle.layers.length; i < il; ++i) {
|
||||
const { loci, value } = wiggle.layers[i];
|
||||
const apply = (interval: Interval) => {
|
||||
const start = Interval.start(interval);
|
||||
const end = Interval.end(interval);
|
||||
return applyWiggleValue(array, start, end, value);
|
||||
};
|
||||
lociApply(loci, apply, false);
|
||||
}
|
||||
ValueCell.update(tWiggle, tWiggle.ref.value);
|
||||
ValueCell.updateIfChanged(wiggleAverage, getWiggleAverage(array, count));
|
||||
ValueCell.updateIfChanged(dWiggleType, type);
|
||||
ValueCell.updateIfChanged(dWiggle, wiggle.layers.length > 0);
|
||||
}
|
||||
|
||||
export function setThemeStrength(renderObject: GraphicsRenderObject | undefined, strength: { overpaint: number, transparency: number, emissive: number, substance: number, wiggle: number }) {
|
||||
if (renderObject) {
|
||||
ValueCell.updateIfChanged(renderObject.values.uOverpaintStrength, strength.overpaint);
|
||||
ValueCell.updateIfChanged(renderObject.values.uTransparencyStrength, strength.transparency);
|
||||
ValueCell.updateIfChanged(renderObject.values.uEmissiveStrength, strength.emissive);
|
||||
ValueCell.updateIfChanged(renderObject.values.uSubstanceStrength, strength.substance);
|
||||
ValueCell.updateIfChanged(renderObject.values.uWiggleStrength, strength.wiggle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,12 +115,13 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
|
||||
if (state.emissive !== undefined && visual) visual.setEmissive(state.emissive);
|
||||
if (state.substance !== undefined && visual) visual.setSubstance(state.substance);
|
||||
if (state.clipping !== undefined && visual) visual.setClipping(state.clipping);
|
||||
if (state.wiggle !== undefined && visual) visual.setWiggle(state.wiggle);
|
||||
if (state.transform !== undefined && visual) visual.setTransform(state.transform);
|
||||
if (state.themeStrength !== undefined && visual) visual.setThemeStrength(state.themeStrength);
|
||||
}
|
||||
|
||||
function setState(state: Partial<Representation.State>) {
|
||||
const { visible, alphaFactor, pickable, overpaint, transparency, emissive, substance, clipping, transform, themeStrength, syncManually, markerActions } = state;
|
||||
const { visible, alphaFactor, pickable, overpaint, transparency, emissive, substance, clipping, wiggle, transform, themeStrength, syncManually, markerActions } = state;
|
||||
const newState: Partial<Representation.State> = {};
|
||||
|
||||
if (visible !== undefined) newState.visible = visible;
|
||||
@@ -131,6 +132,7 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
|
||||
if (emissive !== undefined) newState.emissive = emissive;
|
||||
if (substance !== undefined) newState.substance = substance;
|
||||
if (clipping !== undefined) newState.clipping = clipping;
|
||||
if (wiggle !== undefined) newState.wiggle = wiggle;
|
||||
if (themeStrength !== undefined) newState.themeStrength = themeStrength;
|
||||
if (transform !== undefined && !Mat4.areEqual(transform, _state.transform, EPSILON)) {
|
||||
newState.transform = transform;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const VolumeSegmentParams = {
|
||||
segments: PD.Converted(
|
||||
(v: number[]) => v.map(x => `${x}`),
|
||||
(v: string[]) => v.map(x => parseInt(x)),
|
||||
PD.MultiSelect(['0'], PD.arrayToOptions(['0']), {
|
||||
PD.MultiSelect<string>(['0'], PD.arrayToOptions(['0']), {
|
||||
isEssential: true
|
||||
})
|
||||
)
|
||||
|
||||
@@ -30,9 +30,10 @@ import { isPromiseLike } from '../../mol-util/type-helpers';
|
||||
import { Substance } from '../../mol-theme/substance';
|
||||
import { createMarkers } from '../../mol-geo/geometry/marker-data';
|
||||
import { Emissive } from '../../mol-theme/emissive';
|
||||
import { Wiggle } from '../../mol-theme/wiggle';
|
||||
import { SizeTheme } from '../../mol-theme/size';
|
||||
import { Sphere3D } from '../../mol-math/geometry/primitives/sphere3d';
|
||||
import { BaseGeometry } from '../../mol-geo/geometry/base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../../mol-geo/geometry/base';
|
||||
|
||||
export const VolumeParams = {
|
||||
...BaseGeometry.Params,
|
||||
@@ -181,7 +182,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
if (updateState.updateTransform || updateState.updateLocation) {
|
||||
// console.log('update transform');
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (newProps.instanceGranularity) {
|
||||
if (resolveInstanceGranularity(newProps.instanceGranularity, groupCount, instanceCount)) {
|
||||
createMarkers(instanceCount, 'instance', renderObject.values);
|
||||
} else {
|
||||
createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
|
||||
@@ -278,14 +279,15 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
}
|
||||
|
||||
function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
|
||||
const instanceGranularity = resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount);
|
||||
if (isEveryLoci(loci)) {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return apply(Interval.ofBounds(0, locationIt.instanceCount));
|
||||
} else {
|
||||
return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
|
||||
}
|
||||
} else {
|
||||
if (currentProps.instanceGranularity) {
|
||||
if (instanceGranularity) {
|
||||
return eachInstance(loci, currentVolume, currentKey, apply);
|
||||
} else {
|
||||
return eachLocation(loci, currentVolume, currentKey, currentProps, apply, geometry);
|
||||
@@ -307,7 +309,11 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
}
|
||||
},
|
||||
getLoci(pickingId: PickingId) {
|
||||
return renderObject ? getLoci(pickingId, currentVolume, currentKey, currentProps, renderObject.id, geometry) : EmptyLoci;
|
||||
if (!renderObject) return EmptyLoci;
|
||||
if (resolveInstanceGranularity(currentProps.instanceGranularity, locationIt.groupCount, locationIt.instanceCount)) {
|
||||
pickingId = { ...pickingId, groupId: PickingId.Null };
|
||||
}
|
||||
return getLoci(pickingId, currentVolume, currentKey, currentProps, renderObject.id, geometry);
|
||||
},
|
||||
eachLocation(cb: LocationCallback) {
|
||||
locationIt.reset();
|
||||
@@ -349,7 +355,10 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
|
||||
setClipping(clipping: Clipping) {
|
||||
return Visual.setClipping(renderObject, clipping, lociApply, true);
|
||||
},
|
||||
setThemeStrength(strength: { overpaint: number, transparency: number, emissive: number, substance: number }) {
|
||||
setWiggle(wiggle: Wiggle) {
|
||||
return Visual.setWiggle(renderObject, wiggle, lociApply, true);
|
||||
},
|
||||
setThemeStrength(strength: { overpaint: number, transparency: number, emissive: number, substance: number, wiggle: number }) {
|
||||
Visual.setThemeStrength(renderObject, strength);
|
||||
},
|
||||
destroy() {
|
||||
|
||||
126
src/mol-state/_spec/state.spec.ts
Normal file
126
src/mol-state/_spec/state.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*/
|
||||
|
||||
import { State, StateObject, StateTransformer } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
interface TypeInfo { name: string; typeClass: 'Root' | 'Data' }
|
||||
const Create = StateObject.factory<TypeInfo>();
|
||||
|
||||
class Root extends Create({ name: 'Root', typeClass: 'Root' }) { }
|
||||
class Leaf extends Create<{ value: number }>({ name: 'Leaf', typeClass: 'Data' }) { }
|
||||
|
||||
const NS = 'state-dispose-spec';
|
||||
let counter = 0;
|
||||
|
||||
function leafTransformer(spy: () => void) {
|
||||
return StateTransformer.create<Root, Leaf, { value: number }>(NS, {
|
||||
name: `create-leaf-${counter++}`,
|
||||
from: [Root],
|
||||
to: [Leaf],
|
||||
display: { name: 'Create Leaf' },
|
||||
params: () => ({} as any),
|
||||
apply({ params }) { return new Leaf({ value: params.value }); },
|
||||
dispose() { spy(); }
|
||||
});
|
||||
}
|
||||
|
||||
function chainedTransformer(spy: () => void) {
|
||||
return StateTransformer.create<Leaf, Leaf, {}>(NS, {
|
||||
name: `chained-leaf-${counter++}`,
|
||||
from: [Leaf],
|
||||
to: [Leaf],
|
||||
display: { name: 'Chained Leaf' },
|
||||
apply({ a }) { return new Leaf({ value: a.data.value + 1 }); },
|
||||
dispose() { spy(); }
|
||||
});
|
||||
}
|
||||
|
||||
function newState() {
|
||||
return State.create(new Root({}), { runTask: <T>(t: Task<T>) => t.run() });
|
||||
}
|
||||
|
||||
describe('State.dispose', () => {
|
||||
it('calls transformer.dispose for every live cell', async () => {
|
||||
const leafSpy = jest.fn();
|
||||
const chainSpy = jest.fn();
|
||||
const A = leafTransformer(leafSpy);
|
||||
const B = chainedTransformer(chainSpy);
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(A as any, { value: 1 }).apply(B as any, {});
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
// root + 2 transformer outputs.
|
||||
expect(state.cells.size).toBe(3);
|
||||
|
||||
state.dispose();
|
||||
|
||||
expect(leafSpy).toHaveBeenCalledTimes(1);
|
||||
expect(chainSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('disposes all sibling subtrees', async () => {
|
||||
const spyA = jest.fn();
|
||||
const spyB = jest.fn();
|
||||
const A = leafTransformer(spyA);
|
||||
const B = leafTransformer(spyB);
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(A as any, { value: 1 });
|
||||
builder.toRoot<Root>().apply(B as any, { value: 2 });
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
state.dispose();
|
||||
|
||||
expect(spyA).toHaveBeenCalledTimes(1);
|
||||
expect(spyB).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not throw when a transformer dispose throws', async () => {
|
||||
const goodSpy = jest.fn();
|
||||
const Throwing = StateTransformer.create<Root, Leaf, { value: number }>(NS, {
|
||||
name: `throwing-leaf-${counter++}`,
|
||||
from: [Root],
|
||||
to: [Leaf],
|
||||
display: { name: 'Throwing Leaf' },
|
||||
apply({ params }) { return new Leaf({ value: params.value }); },
|
||||
dispose() { throw new Error('boom'); }
|
||||
});
|
||||
const Good = leafTransformer(goodSpy);
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(Throwing as any, { value: 1 });
|
||||
builder.toRoot<Root>().apply(Good as any, { value: 2 });
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
expect(() => state.dispose()).not.toThrow();
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
expect(goodSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is a no-op for transformers without a dispose definition', async () => {
|
||||
const NoDispose = StateTransformer.create<Root, Leaf, { value: number }>(NS, {
|
||||
name: `no-dispose-${counter++}`,
|
||||
from: [Root],
|
||||
to: [Leaf],
|
||||
display: { name: 'No-dispose Leaf' },
|
||||
apply({ params }) { return new Leaf({ value: params.value }); }
|
||||
});
|
||||
|
||||
const state = newState();
|
||||
const builder = state.build();
|
||||
builder.toRoot<Root>().apply(NoDispose as any, { value: 1 });
|
||||
await state.runTask(state.updateTree(builder));
|
||||
|
||||
expect(() => state.dispose()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -159,6 +159,23 @@ class State {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Dispose every still-live cell so transformer dispose callbacks
|
||||
// (e.g. WebGL/GL buffer cleanup) actually run. Without this,
|
||||
// calling dispose() on a State that still has cells leaks any
|
||||
// resources held by transformer dispose callbacks because they
|
||||
// would only fire on per-cell deletion (see updateNode/findDeletes).
|
||||
const refs: StateTransform.Ref[] = [];
|
||||
StateTree.doPostOrder(this._tree, this._tree.root, { refs }, (n, _, s) => { s.refs.push(n.ref); });
|
||||
for (let i = refs.length - 1; i >= 0; i--) {
|
||||
const cell = (this.cells as Map<StateTransform.Ref, StateObjectCell>).get(refs[i]);
|
||||
if (!cell) continue;
|
||||
try {
|
||||
dispose(cell.transform, cell.obj, cell.transform.params, cell.cache, this.globalContext);
|
||||
} catch (e) {
|
||||
console.warn('Error in transformer dispose during State.dispose', e);
|
||||
}
|
||||
}
|
||||
|
||||
this.ev.dispose();
|
||||
this.actions.dispose();
|
||||
}
|
||||
|
||||
137
src/mol-theme/wiggle.ts
Normal file
137
src/mol-theme/wiggle.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Loci } from '../mol-model/loci';
|
||||
import { Structure, StructureElement } from '../mol-model/structure';
|
||||
import { Script } from '../mol-script/script';
|
||||
|
||||
export { Wiggle };
|
||||
|
||||
type Wiggle<T extends Loci = Loci> = {
|
||||
readonly kind: T['kind']
|
||||
readonly layers: ReadonlyArray<Wiggle.Layer<T>>
|
||||
}
|
||||
|
||||
function Wiggle<T extends Loci>(kind: T['kind'], layers: ReadonlyArray<Wiggle.Layer<T>>): Wiggle {
|
||||
return { kind, layers };
|
||||
}
|
||||
|
||||
namespace Wiggle {
|
||||
export type Layer<T extends Loci = Loci> = { readonly loci: T, readonly value: number }
|
||||
export const Empty: Wiggle = { kind: 'empty-loci', layers: [] };
|
||||
|
||||
export function areEqual(wA: Wiggle, wB: Wiggle) {
|
||||
if (wA.layers.length === 0 && wB.layers.length === 0) return true;
|
||||
if (wA.layers.length !== wB.layers.length) return false;
|
||||
for (let i = 0, il = wA.layers.length; i < il; ++i) {
|
||||
if (wA.layers[i].value !== wB.layers[i].value) return false;
|
||||
if (!Loci.areEqual(wA.layers[i].loci, wB.layers[i].loci)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isEmpty(wiggle: Wiggle) {
|
||||
return wiggle.layers.length === 0;
|
||||
}
|
||||
|
||||
export function remap(wiggle: Wiggle, structure: Structure): Wiggle {
|
||||
if (wiggle.kind === 'element-loci') {
|
||||
const layers: Wiggle.Layer[] = [];
|
||||
for (const layer of wiggle.layers) {
|
||||
let { loci, value } = layer;
|
||||
loci = StructureElement.Loci.remap(loci as StructureElement.Loci, structure);
|
||||
if (!StructureElement.Loci.isEmpty(loci)) {
|
||||
layers.push({ loci, value });
|
||||
}
|
||||
}
|
||||
return { kind: 'element-loci', layers };
|
||||
} else {
|
||||
return wiggle;
|
||||
}
|
||||
}
|
||||
|
||||
export function merge(wiggle: Wiggle): Wiggle {
|
||||
if (isEmpty(wiggle)) return wiggle;
|
||||
if (wiggle.kind === 'element-loci') {
|
||||
const { structure } = wiggle.layers[0].loci as StructureElement.Loci;
|
||||
const map = new Map<number, StructureElement.Loci>();
|
||||
let shadowed = StructureElement.Loci.none(structure);
|
||||
for (let i = 0, il = wiggle.layers.length; i < il; ++i) {
|
||||
let { loci, value } = wiggle.layers[il - i - 1]; // process from end
|
||||
loci = StructureElement.Loci.subtract(loci as StructureElement.Loci, shadowed);
|
||||
shadowed = StructureElement.Loci.union(loci, shadowed);
|
||||
if (!StructureElement.Loci.isEmpty(loci)) {
|
||||
if (map.has(value)) {
|
||||
loci = StructureElement.Loci.union(loci, map.get(value)!);
|
||||
}
|
||||
map.set(value, loci);
|
||||
}
|
||||
}
|
||||
const layers: Wiggle.Layer<StructureElement.Loci>[] = [];
|
||||
map.forEach((loci, value) => {
|
||||
layers.push({ loci, value });
|
||||
});
|
||||
return { kind: 'element-loci', layers };
|
||||
} else {
|
||||
return wiggle;
|
||||
}
|
||||
}
|
||||
|
||||
export function filter(wiggle: Wiggle, filter: Structure): Wiggle {
|
||||
if (isEmpty(wiggle)) return wiggle;
|
||||
if (wiggle.kind === 'element-loci') {
|
||||
const { structure } = wiggle.layers[0].loci as StructureElement.Loci;
|
||||
const layers: Wiggle.Layer[] = [];
|
||||
for (const layer of wiggle.layers) {
|
||||
let { loci, value } = layer;
|
||||
// filter by first map to the `filter` structure and
|
||||
// then map back to the original structure of the wiggle loci
|
||||
const filtered = StructureElement.Loci.remap(loci as StructureElement.Loci, filter);
|
||||
loci = StructureElement.Loci.remap(filtered, structure);
|
||||
if (!StructureElement.Loci.isEmpty(loci)) {
|
||||
layers.push({ loci, value });
|
||||
}
|
||||
}
|
||||
return { kind: 'element-loci', layers };
|
||||
} else {
|
||||
return wiggle;
|
||||
}
|
||||
}
|
||||
|
||||
export type ScriptLayer = { script: Script, value: number }
|
||||
export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Wiggle {
|
||||
const layers: Wiggle.Layer[] = [];
|
||||
for (let i = 0, il = scriptLayers.length; i < il; ++i) {
|
||||
const { script, value } = scriptLayers[i];
|
||||
const loci = Script.toLoci(script, structure);
|
||||
if (!StructureElement.Loci.isEmpty(loci)) {
|
||||
layers.push({ loci, value });
|
||||
}
|
||||
}
|
||||
return { kind: 'element-loci', layers };
|
||||
}
|
||||
|
||||
export type BundleLayer = { bundle: StructureElement.Bundle, value: number }
|
||||
export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Wiggle {
|
||||
const layers: Wiggle.Layer[] = [];
|
||||
for (let i = 0, il = bundleLayers.length; i < il; ++i) {
|
||||
const { bundle, value } = bundleLayers[i];
|
||||
const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
|
||||
layers.push({ loci, value });
|
||||
}
|
||||
return { kind: 'element-loci', layers };
|
||||
}
|
||||
|
||||
export function toBundle(wiggle: Wiggle<StructureElement.Loci>) {
|
||||
const layers: BundleLayer[] = [];
|
||||
for (let i = 0, il = wiggle.layers.length; i < il; ++i) {
|
||||
const { loci, value } = wiggle.layers[i];
|
||||
const bundle = StructureElement.Bundle.fromLoci(loci);
|
||||
layers.push({ bundle, value });
|
||||
}
|
||||
return { kind: 'element-loci', layers };
|
||||
}
|
||||
}
|
||||
78
src/mol-util/_spec/graphql-client.spec.ts
Normal file
78
src/mol-util/_spec/graphql-client.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Asset, AssetManager } from '../assets';
|
||||
import { ajaxGet } from '../data-source';
|
||||
import { GraphQLClient } from '../graphql-client';
|
||||
|
||||
describe('graphql transport', () => {
|
||||
it('adds JSON headers to GraphQL requests', async () => {
|
||||
const assetManager = new AssetManager();
|
||||
const dispose = jest.fn();
|
||||
|
||||
const resolveSpy = jest.spyOn(assetManager, 'resolve').mockImplementation((asset, type) => {
|
||||
expect(type).toBe('json');
|
||||
expect(Asset.isUrl(asset)).toBe(true);
|
||||
if (!Asset.isUrl(asset)) throw new Error('expected URL asset');
|
||||
|
||||
expect(asset.url).toBe('https://example.org/graphql');
|
||||
expect(asset.headers).toEqual({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Accept': 'application/json'
|
||||
});
|
||||
expect(asset.body).toContain('"query"');
|
||||
expect(asset.body).toContain('"variables"');
|
||||
|
||||
return {
|
||||
id: 0,
|
||||
name: 'mock',
|
||||
run: async () => ({ data: { data: { ok: true } }, dispose }),
|
||||
runAsChild: async () => ({ data: { data: { ok: true } }, dispose }),
|
||||
runInContext: async () => ({ data: { data: { ok: true } }, dispose }),
|
||||
} as any;
|
||||
});
|
||||
|
||||
const client = new GraphQLClient('https://example.org/graphql', assetManager);
|
||||
const result = await client.request(RuntimeContext.Synchronous, 'query Test { test }', { id: '1' });
|
||||
|
||||
expect(resolveSpy).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toEqual({ ok: true });
|
||||
result.dispose();
|
||||
expect(dispose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('preserves POST body and headers in Node.js HTTP requests', async () => {
|
||||
const fetchSpy = jest.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
status: 200,
|
||||
bytes: async () => new TextEncoder().encode(JSON.stringify({ ok: true }))
|
||||
} as any);
|
||||
|
||||
const result = await ajaxGet({
|
||||
url: 'https://example.org/graphql',
|
||||
type: 'json',
|
||||
body: '{"query":"{ test }"}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}).run();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith('https://example.org/graphql', {
|
||||
signal: expect.any(AbortSignal),
|
||||
method: 'POST',
|
||||
body: '{"query":"{ test }"}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
expect(result).toEqual({ ok: true });
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -17,10 +17,10 @@ type _File = File;
|
||||
type Asset = Asset.Url | Asset.File
|
||||
|
||||
namespace Asset {
|
||||
export type Url = { kind: 'url', id: UUID, url: string, title?: string, body?: string, headers?: [string, string][] }
|
||||
export type Url = { kind: 'url', id: UUID, url: string, title?: string, body?: string, headers?: Record<string, string> }
|
||||
export type File = { kind: 'file', id: UUID, name: string, file?: _File }
|
||||
|
||||
export function Url(url: string, options?: { body?: string, title?: string, headers?: [string, string][] }): Url {
|
||||
export function Url(url: string, options?: { body?: string, title?: string, headers?: Record<string, string> }): Url {
|
||||
return { kind: 'url', id: UUID.create22(), url, ...options };
|
||||
}
|
||||
|
||||
@@ -54,15 +54,26 @@ namespace Asset {
|
||||
return typeof url === 'string' ? url : url.url;
|
||||
}
|
||||
|
||||
export function getUrlAsset(manager: AssetManager, url: string | Url, body?: string) {
|
||||
export function getUrlAsset(manager: AssetManager, url: string | Url, body?: string, headers?: Record<string, string>) {
|
||||
if (typeof url === 'string') {
|
||||
const asset = manager.tryFindUrl(url, body);
|
||||
return asset || Url(url, { body });
|
||||
const asset = manager.tryFindUrl(url, body, headers);
|
||||
return asset || Url(url, { body, headers });
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function urlHeadersEqual(a?: Record<string, string>, b?: Record<string, string>) {
|
||||
const aKeys = a ? Object.keys(a) : [];
|
||||
const bKeys = b ? Object.keys(b) : [];
|
||||
if (aKeys.length !== bKeys.length) return false;
|
||||
|
||||
for (const key of aKeys) {
|
||||
if (a![key] !== b![key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class AssetManager {
|
||||
// TODO: add URL based ref-counted cache?
|
||||
// TODO: when serializing, check for duplicates?
|
||||
@@ -73,13 +84,13 @@ class AssetManager {
|
||||
return iterableToArray(this._assets.values());
|
||||
}
|
||||
|
||||
tryFindUrl(url: string, body?: string): Asset.Url | undefined {
|
||||
tryFindUrl(url: string, body?: string, headers?: Record<string, string>): Asset.Url | undefined {
|
||||
const assets = this.assets.values();
|
||||
while (true) {
|
||||
const v = assets.next();
|
||||
if (v.done) return;
|
||||
const asset = v.value.asset;
|
||||
if (Asset.isUrl(asset) && asset.url === url && (asset.body || '') === (body || '')) return asset;
|
||||
if (Asset.isUrl(asset) && asset.url === url && (asset.body || '') === (body || '') && urlHeadersEqual(asset.headers, headers)) return asset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,4 +179,4 @@ class AssetManager {
|
||||
dispose() {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -34,7 +34,7 @@ export interface AjaxGetParams<T extends DataType = 'string'> {
|
||||
url: string,
|
||||
type?: T,
|
||||
title?: string,
|
||||
headers?: [string, string][],
|
||||
headers?: Record<string, string>,
|
||||
body?: string
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ function getRequestResponseType(type: DataType): XMLHttpRequestResponseType {
|
||||
}
|
||||
}
|
||||
|
||||
function ajaxGetInternal<T extends DataType>(title: string | undefined, url: string, type: T, body?: string, headers?: [string, string][]): Task<DataResponse<T>> {
|
||||
function ajaxGetInternal<T extends DataType>(title: string | undefined, url: string, type: T, body?: string, headers?: Record<string, string>): Task<DataResponse<T>> {
|
||||
if (RUNNING_IN_NODEJS) {
|
||||
if (url.startsWith('file://')) {
|
||||
return ajaxGetInternal_file_NodeJS(title, url, type, body, headers);
|
||||
@@ -265,7 +265,7 @@ function ajaxGetInternal<T extends DataType>(title: string | undefined, url: str
|
||||
|
||||
xhttp.open(body ? 'post' : 'get', url, true);
|
||||
if (headers) {
|
||||
for (const [name, value] of headers) {
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
xhttp.setRequestHeader(name, value);
|
||||
}
|
||||
}
|
||||
@@ -311,7 +311,7 @@ function readFileAsync(filename: string): Promise<NonSharedBuffer> {
|
||||
}
|
||||
|
||||
/** Alternative implementation of ajaxGetInternal for NodeJS for file:// protocol */
|
||||
function ajaxGetInternal_file_NodeJS<T extends DataType>(title: string | undefined, url: string, type: T, body?: string, headers?: [string, string][]): Task<DataResponse<T>> {
|
||||
function ajaxGetInternal_file_NodeJS<T extends DataType>(title: string | undefined, url: string, type: T, body?: string, headers?: Record<string, string>): Task<DataResponse<T>> {
|
||||
if (!RUNNING_IN_NODEJS) throw new Error('This function should only be used when running in Node.js');
|
||||
if (!url.startsWith('file://')) throw new Error('This function is only for URLs with protocol file://');
|
||||
|
||||
@@ -327,13 +327,18 @@ function ajaxGetInternal_file_NodeJS<T extends DataType>(title: string | undefin
|
||||
}
|
||||
|
||||
/** Alternative implementation of ajaxGetInternal for NodeJS for http(s):// protocol */
|
||||
function ajaxGetInternal_http_NodeJS<T extends DataType>(title: string | undefined, url: string, type: T, body?: string, headers?: [string, string][]): Task<DataResponse<T>> {
|
||||
function ajaxGetInternal_http_NodeJS<T extends DataType>(title: string | undefined, url: string, type: T, body?: string, headers?: Record<string, string>): Task<DataResponse<T>> {
|
||||
if (!RUNNING_IN_NODEJS) throw new Error('This function should only be used when running in Node.js');
|
||||
|
||||
const aborter = new AbortController();
|
||||
return Task.create(title ?? 'Download', async ctx => {
|
||||
await ctx.update({ message: 'Downloading...', canAbort: true });
|
||||
const response = await fetch(url, { signal: aborter.signal });
|
||||
const response = await fetch(url, {
|
||||
signal: aborter.signal,
|
||||
method: body ? 'POST' : 'GET',
|
||||
body,
|
||||
headers
|
||||
});
|
||||
if (!(response.status >= 200 && response.status < 400)) {
|
||||
throw new Error(`Download failed with status code ${response.status}`);
|
||||
}
|
||||
@@ -402,4 +407,4 @@ async function wrapPromise(index: number, id: string, p: Promise<Asset.Wrapper<'
|
||||
} catch (error) {
|
||||
return { kind: 'error', error, index, id };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
*
|
||||
@@ -57,9 +57,12 @@ export class GraphQLClient {
|
||||
constructor(private url: string, private assetManager: AssetManager) { }
|
||||
|
||||
async request(ctx: RuntimeContext, query: string, variables?: Variables): Promise<Asset.Wrapper<'json'>> {
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
const body = JSON.stringify({ query, variables }, null, 2);
|
||||
const url = Asset.getUrlAsset(this.assetManager, this.url, body);
|
||||
const url = Asset.getUrlAsset(this.assetManager, this.url, body, headers);
|
||||
const result = await this.assetManager.resolve(url, 'json').runInContext(ctx);
|
||||
|
||||
if (!result.data.errors && result.data.data) {
|
||||
@@ -72,4 +75,4 @@ export class GraphQLClient {
|
||||
throw new ClientError({ ...errorResult }, { query, variables });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,12 +234,14 @@ export namespace MonadicParser {
|
||||
export type Result<T> = Success<T> | Failure
|
||||
|
||||
export function seqMap<A, B>(a: MonadicParser<A>, b: MonadicParser<B>, c: any) {
|
||||
const args = [].slice.call(arguments);
|
||||
const args: any[] = [].slice.call(arguments);
|
||||
if (args.length === 0) {
|
||||
throw new Error('seqMap needs at least one argument');
|
||||
}
|
||||
const mapper = args.pop();
|
||||
assertFunction(mapper);
|
||||
if (typeof mapper !== 'function') {
|
||||
throw new Error('not a function: ' + mapper);
|
||||
}
|
||||
return seq.apply(null, args).map(function (results: any) {
|
||||
return mapper.apply(null, results);
|
||||
});
|
||||
@@ -571,9 +573,3 @@ function unsafeUnion(xs: string[], ys: string[]) {
|
||||
function isParser(obj: any): obj is MonadicParser<any> {
|
||||
return obj instanceof MonadicParser;
|
||||
}
|
||||
|
||||
function assertFunction(x: any) {
|
||||
if (typeof x !== 'function') {
|
||||
throw new Error('not a function: ' + x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,10 +295,13 @@ export namespace ParamDefinition {
|
||||
type: 'object-list',
|
||||
element: Params,
|
||||
ctor(): T,
|
||||
getLabel(t: T): string
|
||||
getLabel(t: T): string,
|
||||
presets?: Select<T[]>['options']
|
||||
}
|
||||
export function ObjectList<T>(element: For<T>, getLabel: (e: T) => string, info?: Info & { defaultValue?: T[], ctor?: () => T }): ObjectList<Normalize<T>> {
|
||||
return setInfo<ObjectList<Normalize<T>>>({ type: 'object-list', element: element as any as Params, getLabel, ctor: _defaultObjectListCtor, defaultValue: (info?.defaultValue) || [] }, info);
|
||||
export function ObjectList<T>(element: For<T>, getLabel: (e: T) => string, info?: Info & { defaultValue?: T[], ctor?: () => T, presets?: Select<T[]>['options'] }): ObjectList<Normalize<T>> {
|
||||
const ret = setInfo<ObjectList<Normalize<T>>>({ type: 'object-list', element: element as any as Params, getLabel, ctor: _defaultObjectListCtor, defaultValue: (info?.defaultValue) || [] }, info);
|
||||
if (info?.presets) ret.presets = info.presets as any;
|
||||
return ret;
|
||||
}
|
||||
function _defaultObjectListCtor(this: ObjectList) { return getDefaultValues(this.element) as any; }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"strictFunctionTypes": true,
|
||||
"module": "esnext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"importHelpers": true,
|
||||
"noEmitHelpers": true,
|
||||
@@ -19,7 +19,11 @@
|
||||
"jsx": "react-jsx",
|
||||
"lib": [ "ES2018", "dom", "ES2022.Object" ],
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
"outDir": "lib",
|
||||
"noUncheckedSideEffectImports": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"types": ["webxr", "node"]
|
||||
},
|
||||
"include": [ "src/**/*" ],
|
||||
"exclude": [ "src/**/_spec/*" ],
|
||||
|
||||
Reference in New Issue
Block a user