Compare commits

...

33 Commits

Author SHA1 Message Date
Alexander Rose
d993082f24 5.4.0 2025-11-16 10:12:28 -08:00
Alexander Rose
5eaa73d56d changelog 2025-11-16 10:11:07 -08:00
Alexander Rose
fc3c7997ea package updates 2025-11-15 16:54:26 -08:00
Alexander Rose
b3aecf8de4 package updates 2025-11-15 16:46:15 -08:00
Alexander Rose
f3581e62ef schema updates 2025-11-15 16:41:09 -08:00
Alexander Rose
88e7fe508f package updates 2025-11-15 16:32:09 -08:00
Alexander Rose
194092ed67 Merge pull request #1707 from rxht/text-input-chinese-support
Add Chinese language support to the text input box.
2025-11-15 16:23:21 -08:00
Alexander Rose
e96157c890 changelog 2025-11-15 16:19:40 -08:00
Alexander Rose
a028c1ef42 Merge branch 'master' of https://github.com/molstar/molstar into pr/rxht/1707 2025-11-15 16:16:32 -08:00
Alexander Rose
8ba19f0be4 Merge pull request #1701 from midlik/bounding-spheres
Bounding spheres include element radius
2025-11-15 09:29:55 -08:00
Alexander Rose
bccc68f6df Merge branch 'master' of https://github.com/molstar/molstar into pr/midlik/1701 2025-11-15 09:06:55 -08:00
Alexander Rose
026a05d03d formating 2025-11-15 09:06:46 -08:00
Alexander Rose
2b4741c8ee Use PluginCommands to set canvas3d props in camera behavior 2025-11-15 08:52:25 -08:00
Alexander Rose
7960ee06d4 Fix default trackball animated spin speed 2025-11-15 08:50:24 -08:00
Alexander Rose
f73f5af131 Fix direct-volume not drawn in illumination mode 2025-11-15 08:48:22 -08:00
rxht
3123110aa4 Add Chinese language support to the text input box. 2025-11-14 10:56:15 +08:00
midlik
154063638d MVS: Allow canvas background interpolation (#1704) 2025-11-13 12:02:10 +01:00
midlik
a720b98365 MVS transformed primitives (#1705)
* MVS: Fix primitives in root not being transformed with reference structure

* MultilayerColorTheme only preferSmoothing when a nested theme prefers

* MVSAnnotationColorTheme do not prefer smoothing

* Update file header
2025-11-11 16:47:35 +01:00
Adam Midlik
d4a2937e0b Merge branch 'master' into bounding-spheres 2025-11-11 15:39:20 +00:00
midlik
b0ca7ffbb7 Fix all-selector color not applying on substructure (#1700)
* Fix all-selector color not applying on substructure

* Fix CHANGELOG

* Size uniform computed from the first included location
2025-11-11 10:32:01 +01:00
David Sehnal
c42b738abe MVS: Fix appendSnapshots when loading MVSX (#1702) 2025-11-11 10:31:06 +01:00
Alexander Rose
ab0d0fec53 Merge pull request #1697 from midlik/label-boundary
Fix bounding sphere computation for 3D text
2025-11-09 17:49:50 -08:00
Alexander Rose
8d96131962 changelog 2025-11-09 17:49:27 -08:00
Alexander Rose
95bbcd8b24 Merge branch 'master' of https://github.com/molstar/molstar into pr/midlik/1697 2025-11-09 17:49:06 -08:00
Alexander Rose
a21f5c2c23 Add viewport button to toggle illumination mode 2025-11-09 16:45:26 -08:00
Adam Midlik
94b7b1281c Merge branch 'master' into bounding-spheres 2025-11-07 10:19:00 +00:00
Adam Midlik
16dba586df Relax camera limits to allow focusing any selection with >1 atom 2025-11-07 10:13:39 +00:00
midlik
72b761f959 Fix ugly camera clipping in snapshot transitions (#1699)
* Fix ugly camera clipping in snapshot transitions

* Update CHANGELOG.md

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2025-11-06 16:04:20 +01:00
Adam Midlik
943d81cbf9 Fix bounding sphere computation for 3D text 2025-11-06 09:28:09 +00:00
Adam Midlik
ea612c3acb Unit and loci bounding sphere includes VDW or coarse sphere radius 2025-10-31 12:50:51 +00:00
Adam Midlik
a1308645e5 Vec3.orthogonalize handle special cases (fixes trackpad lock in MVS) 2025-10-29 14:04:18 +00:00
Adam Midlik
794b705184 MVS: Fix CIF annotations with no selector columns being ignored 2025-10-28 11:56:40 +00:00
Adam Midlik
66264abe50 MVS: Fix tooltips persisting across snapshots 2025-10-28 11:25:29 +00:00
31 changed files with 2757 additions and 3831 deletions

View File

@@ -5,6 +5,23 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [v5.4.0] - 2025-11-16
- Fix ugly camera clipping in snapshot transitions
- Add viewport button to toggle illumination mode
- Fix bounding sphere computation for 3D text
- Structure bounding sphere includes atom VDW radii / coarse sphere radii
- Relax camera limits to allow focusing any selection with >1 atom
- MolViewSpec
- Fix `appendSnapshots` when loading MVSX
- Fix all-selector color not applying on substructure
- Fix primitives in root not being transformed with reference structure
- Color themes do not prefer smoothing (improves performance in animations)
- Allow canvas background interpolation
- Fix `direct-volume` not drawn in illumination mode
- Fix default trackball animated spin speed
- Use `PluginCommands` to set canvas3d props in camera behavior
- Add support for Input Method Editor (IME) to text params input
## [v5.3.0] - 2025-11-5
- Update loading message in MVS Stories Viewer
- Add `Canvas3D.setAttribs`

6281
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.3.0",
"version": "5.4.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -131,36 +131,36 @@
"@types/gl": "^6.0.5",
"@types/jest": "^30.0.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.24",
"@types/react": "^18.3.26",
"@types/react-dom": "^18.3.7",
"@types/webxr": "^0.5.23",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
"@types/webxr": "^0.5.24",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"benchmark": "^2.1.4",
"concurrently": "^9.2.1",
"cpx2": "^8.0.0",
"css-loader": "^7.1.2",
"esbuild": "^0.25.10",
"esbuild": "^0.27.0",
"esbuild-jest-transform": "^2.0.1",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "^9.36.0",
"eslint": "^9.39.1",
"fs-extra": "^11.3.2",
"http-server": "^14.1.1",
"jest": "^30.2.0",
"jpeg-js": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.93.2",
"simple-git": "^3.28.0",
"sass": "^1.94.0",
"simple-git": "^3.30.0",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.2"
"typescript": "^5.9.3"
},
"dependencies": {
"@types/argparse": "^2.0.17",
"@types/benchmark": "^2.1.5",
"@types/compression": "1.8.1",
"@types/express": "^5.0.3",
"@types/node": "^20.19.17",
"@types/express": "^5.0.5",
"@types/node": "^20.19.25",
"@types/node-fetch": "^2.6.13",
"@types/swagger-ui-dist": "3.30.6",
"argparse": "^2.0.1",
@@ -168,14 +168,14 @@
"cors": "^2.8.5",
"express": "^5.1.0",
"h264-mp4-encoder": "^1.0.12",
"immutable": "^5.1.3",
"immutable": "^5.1.4",
"io-ts": "^2.2.22",
"mutative": "^1.3.0",
"node-fetch": "^2.7.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"rxjs": "^7.8.2",
"swagger-ui-dist": "^5.29.0",
"swagger-ui-dist": "^5.30.2",
"tslib": "^2.8.1",
"util.promisify": "^1.1.3"
},

View File

@@ -531,7 +531,7 @@ export class Viewer {
} else if (format === 'mvsx') {
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
await this.plugin.runTask(Task.create('Load MVSX file', async ctx => {
const parsed = await loadMVSX(this.plugin, ctx, data);
const parsed = await loadMVSX(this.plugin, ctx, data, { doNotClearAssets: options?.appendSnapshots });
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl, ...options });
}));
} else {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -33,7 +33,7 @@ import { MVSTreeSchema } from './tree/mvs/mvs-tree';
const DefaultFocusOptions = {
minRadius: 5,
minRadius: 1,
extraRadius: 0,
};
const DefaultCanvasBackgroundColor = ColorNames.white;

View File

@@ -117,7 +117,7 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
return {
factory: MVSAnnotationColorTheme,
granularity: 'groupInstance',
preferSmoothing: true,
preferSmoothing: false,
color: color,
props: props,
description: 'Assigns colors based on custom MolViewSpec annotation data.',

View File

@@ -112,11 +112,18 @@ export const MVSXFormatProvider: DataFormatProvider<{}, StateObjectRef<Mvs>, any
* add all contained files to `plugin`'s asset manager,
* and parse the main file in the archive as MVSJ.
* Return parsed MVS data and `sourceUrl` for resolution of relative URIs. */
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array<ArrayBuffer>, mainFilePath: string = 'index.mvsj'): Promise<{ mvsData: MVSData, sourceUrl: string }> {
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array<ArrayBuffer>, mainFilePathOrOptions?: string | { mainFilePath?: string, doNotClearAssets?: boolean }): Promise<{ mvsData: MVSData, sourceUrl: string }> {
// TODO: on next major version, streamline mainFilePathOrOptions
if (typeof mainFilePathOrOptions === 'string') {
mainFilePathOrOptions = { mainFilePath: mainFilePathOrOptions };
}
const mainFilePath = mainFilePathOrOptions?.mainFilePath ?? 'index.mvsj';
const doNotClearAssets = mainFilePathOrOptions?.doNotClearAssets ?? false;
// Ensure at most one generation of MVSX file assets exists in the asset manager.
// Hopefully, this is a reasonable compromise to ensure MVSX files work in multi-snapshot
// states.
clearMVSXFileAssets(plugin);
if (!doNotClearAssets) clearMVSXFileAssets(plugin);
const archiveId = `ni,MurmurHash3_128;${murmurHash3_128_fromBytes(data, 42)}`;
let files: { [path: string]: Uint8Array<ArrayBuffer> };
@@ -160,7 +167,7 @@ export async function loadMVSData(plugin: PluginContext, data: MVSData | StringL
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
}
await plugin.runTask(Task.create('Load MVSX file', async ctx => {
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array<ArrayBuffer>);
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array<ArrayBuffer>, { doNotClearAssets: options?.appendSnapshots });
await loadMVS(plugin, parsed.mvsData, { sanityChecks: true, ...options, sourceUrl: parsed.sourceUrl });
}));
} else {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -72,7 +72,7 @@ export const DefaultMultilayerColorThemeProps: MultilayerColorThemeProps = { lay
* If a nested theme provider has `ensureCustomProperties` methods, these will not be called automatically
* (the caller must ensure that any required custom properties be attached). */
function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry): ColorTheme<MultilayerColorThemeParams> {
const { colorLayers, granularity } = makeLayers(ctx, props, colorThemeRegistry);
const { colorLayers, granularity, preferSmoothing } = makeLayers(ctx, props, colorThemeRegistry);
function structureElementColor(loc: StructureElement.Location, isSecondary: boolean): Color {
for (const layer of colorLayers) {
@@ -101,7 +101,7 @@ function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorT
return {
factory: (ctx_, props_) => makeMultilayerColorTheme(ctx_, props_, colorThemeRegistry),
granularity,
preferSmoothing: true,
preferSmoothing,
color: color,
props: props,
description: 'Combines colors from multiple color themes.',
@@ -136,6 +136,7 @@ interface ColorLayer {
function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry) {
const colorLayers: ColorLayer[] = [];
let granularityFlags = 0;
let preferSmoothing = false;
for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from the end to get top layer first, bottom layer last
const layer = props.layers[i];
const themeProvider = colorThemeRegistry.get(layer.theme.name);
@@ -175,8 +176,9 @@ function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, col
default:
console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`);
}
if (theme.preferSmoothing) preferSmoothing = true;
}
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags) };
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags), preferSmoothing };
}

View File

@@ -464,12 +464,15 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
const refs = getPrimitiveStructureRefs(tree);
const clip = clippingForNode(tree);
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree as any });
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
return applyPrimitiveVisuals(data, refs, clip);
},
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
const refs = new Set(tree.params.references);
const clip = clippingForNode(tree);
return applyPrimitiveVisuals(data, new Set(tree.params.references), clip);
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
return applyPrimitiveVisuals(data, refs, clip);
},
};

View File

@@ -75,7 +75,7 @@ export const Canvas3DParams = {
cameraClipping: PD.Group({
radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
minNear: PD.Numeric(5, { min: 0.1, max: 100, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
minNear: PD.Numeric(1, { min: 0.1, max: 100, step: 0.1 }, { description: 'Minimal allowed distance of near clipping plane from the camera. Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
}, { pivot: 'radius' }),
viewport: PD.MappedStatic('canvas', {
canvas: PD.Group({}),

View File

@@ -75,7 +75,7 @@ export const TrackballControlsParams = {
animate: PD.MappedStatic('off', {
off: PD.EmptyGroup(),
spin: PD.Group({
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }, { description: 'Number of rotations per second' }),
speed: PD.Numeric(0.1, { min: -2, max: 2, step: 0.01 }, { description: 'Number of rotations per second' }),
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
rock: PD.Group({
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }, { description: 'Number of oscilations per second' }),

View File

@@ -163,7 +163,7 @@ export class IlluminationPass {
const { gl, state } = this.webgl;
const markingEnabled = MarkingPass.isEnabled(props.marking);
const hasTransparent = scene.opacityAverage < 1;
const hasTransparent = scene.opacityAverage < 1 || scene.volumes.renderables.length > 0;
const hasMarking = markingEnabled && scene.markerAverage > 0;
this.transparentTarget.bind();
@@ -347,7 +347,7 @@ export class IlluminationPass {
const dofEnabled = DofPass.isEnabled(props.postprocessing);
const markingEnabled = MarkingPass.isEnabled(props.marking);
const hasTransparent = scene.opacityAverage < 1;
const hasTransparent = scene.opacityAverage < 1 || scene.volumes.renderables.length > 0;
const hasMarking = markingEnabled && scene.markerAverage > 0;
let needsUpdateCompose = false;

View File

@@ -122,7 +122,9 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
/** Creates color uniform */
function createUniformColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
return createValueColor(color(NullLocation, false), colorData);
locationIt.reset();
const loc = locationIt.hasNext ? locationIt.move() : { location: NullLocation, isSecondary: false };
return createValueColor(color(loc.location, loc.isSecondary), colorData);
}
//

View File

@@ -77,7 +77,9 @@ export function createValueSize(value: number, sizeData?: SizeData): SizeData {
/** Creates size uniform */
export function createUniformSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
return createValueSize(sizeFn(NullLocation), sizeData);
locationIt.reset();
const location = locationIt.hasNext ? locationIt.move().location : NullLocation;
return createValueSize(sizeFn(location), sizeData);
}
export function createTextureSize(sizes: TextureImage<Uint8Array>, type: SizeType, sizeData?: SizeData): SizeData {

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -333,5 +334,5 @@ function getPadding(mappings: Float32Array, depths: Float32Array, charCount: num
const d = Math.abs(depths[i]);
if (d > maxDepth) maxDepth = d;
}
return scale * Math.max(maxDepth, maxOffset);
return Math.max(maxDepth, scale * maxOffset);
}

View File

@@ -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.406, IHM 1.28, MA 1.4.7.
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.407, IHM 1.28, MA 1.4.8.
*
* @author molstar/ciftools package
*/

View File

@@ -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.406, IHM 1.28, MA 1.4.7.
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.407, IHM 1.28, MA 1.4.8.
*
* @author molstar/ciftools package
*/

View File

@@ -40,7 +40,7 @@ export const CifCore_Schema = {
* in exceptional circumstances (Brock, 2025) may be reported as
* non-integral.
*
* Reference: Brock, C. P. (2025). Acta Cryst. A81, nnn-nnn.
* Reference: Brock, C. P. (2025). Acta Cryst. A81, 405-408.
*/
formula_units_z: float,
/**
@@ -336,7 +336,12 @@ export const CifCore_Schema = {
*/
site_symmetry_2: str,
/**
* Bond valence calculated from the bond distance.
* Valence assigned to the bond between the sites identified by
* _geom_bond.id calculated according to the bond-valence model
* (Brown, 2002) from the bond distance.
*
* Ref: Brown, I. D. (2002). The Chemical Bond in Inorganic Chemistry:
* the Bond-Valence Model, eq. (3.1). Oxford: Oxford University Press.
*/
valence: float,
},

View File

@@ -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.406, IHM 1.28, MA 1.4.7.
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.407, IHM 1.28, MA 1.4.8.
*
* @author molstar/ciftools package
*/
@@ -5078,7 +5078,7 @@ export const mmCIF_Schema = {
/**
* The type of data held in the dataset.
*/
content_type: Aliased<'target' | 'template structure' | 'polymeric template library' | 'spatial restraints' | 'target-template alignment' | 'coevolution MSA' | 'model coordinates' | 'input structure' | 'reference database' | 'other'>(str),
content_type: Aliased<'target' | 'template structure' | 'polymeric template library' | 'spatial restraints' | 'target-template alignment' | 'coevolution MSA' | 'model coordinates' | 'input structure' | 'reference database' | 'intermediate backbone' | 'intermediate sequence' | 'model quality assessment scores' | 'energy estimate' | 'experimental validation' | 'other'>(str),
/**
* Details for other content types.
*/
@@ -5131,7 +5131,7 @@ export const mmCIF_Schema = {
/**
* The mode of calculation of the QA metric.
*/
mode: Aliased<'local' | 'global' | 'local-pairwise' | 'per-feature' | 'per-feature-pair'>(str),
mode: Aliased<'local' | 'global' | 'local-pairwise' | 'per-feature' | 'per-feature-pair' | 'dihedral'>(str),
/**
* Identifier to the set of software used to calculate the QA metric.
* This data item is a pointer to the _ma_software_group.group_id in the

View File

@@ -1,13 +1,14 @@
/**
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/
import { UUID } from '../../../mol-util/uuid';
import { StructureSequence } from './properties/sequence';
import { AtomicHierarchy, AtomicConformation, AtomicRanges } from './properties/atomic';
import { AtomicHierarchy, AtomicConformation, AtomicRanges, VdwRadius } from './properties/atomic';
import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
import { Entities, ChemicalComponentMap, MissingResidues, StructAsymMap } from './properties/common';
import { CustomProperties } from '../../custom-property';
@@ -184,6 +185,20 @@ export namespace Model {
return center;
}
const AtomicRadiiProp = '__AtomicRadii__';
/** Get array of atomic radii for all atoms in the model (cached). */
export function getAtomicRadii(model: Model): Float32Array {
if (model._dynamicPropertyData[AtomicRadiiProp]) return model._dynamicPropertyData[AtomicRadiiProp];
const nAtoms = model.atomicHierarchy.atoms._rowCount;
const type_symbol = model.atomicHierarchy.atoms.type_symbol.value;
const radii = new Float32Array(nAtoms);
for (let i = 0; i < nAtoms; i++) {
radii[i] = VdwRadius(type_symbol(i));
}
model._dynamicPropertyData[AtomicRadiiProp] = radii;
return radii;
}
function invertIndex(xs: Column<number>) {
const invertedIndex = new Int32Array(xs.rowCount);
let isIdentity = false;

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025s 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>
@@ -98,6 +98,9 @@ export const UnknownSaccharideComponent: SaccharideComponent = {
type: SaccharideType.Unknown
};
/**
* Unless stated otherwise, these follow SNFG recommendations.
*/
const Monosaccharides: SaccharideComponent[] = [
{ abbr: 'Glc', name: 'Glucose', color: SaccharideColors.Blue, type: SaccharideType.Hexose },
{ abbr: 'Man', name: 'Mannose', color: SaccharideColors.Green, type: SaccharideType.Hexose },
@@ -125,7 +128,7 @@ const Monosaccharides: SaccharideComponent[] = [
{ abbr: 'AllN', name: 'Allosamine', color: SaccharideColors.Purple, type: SaccharideType.Hexosamine },
{ abbr: 'TalN', name: 'Talosamine', color: SaccharideColors.LightBlue, type: SaccharideType.Hexosamine },
{ abbr: 'IdoN', name: 'Idosamine', color: SaccharideColors.Brown, type: SaccharideType.Hexosamine },
{ abbr: 'GlcNS', name: 'N-sulfo Glucosamine', color: SaccharideColors.Blue, type: SaccharideType.Hexosamine },
{ abbr: 'GlcNS', name: 'N-sulfo Glucosamine', color: SaccharideColors.Blue, type: SaccharideType.Hexosamine }, // from GLYCAM
{ abbr: 'GlcA', name: 'Glucuronic Acid', color: SaccharideColors.Blue, type: SaccharideType.Hexuronate },
{ abbr: 'ManA', name: 'Mannuronic Acid', color: SaccharideColors.Green, type: SaccharideType.Hexuronate },
@@ -148,7 +151,7 @@ const Monosaccharides: SaccharideComponent[] = [
{ abbr: '6dAltNAc', name: 'N-Acetyl 6-Deoxy Altrosamine', color: SaccharideColors.Pink, type: SaccharideType.DeoxyhexNAc },
{ abbr: '6dTalNAc', name: 'N-Acetyl 6-Deoxy Talosamine', color: SaccharideColors.LightBlue, type: SaccharideType.DeoxyhexNAc },
{ abbr: 'FucNAc', name: 'N-Acetyl Fucosamine', color: SaccharideColors.Red, type: SaccharideType.DeoxyhexNAc },
{ abbr: 'AAT', name: 'AAT', color: SaccharideColors.Red, type: SaccharideType.DeoxyhexNAc },
{ abbr: 'AAT', name: 'AAT', color: SaccharideColors.Red, type: SaccharideType.DeoxyhexNAc }, // from GLYCAM
{ abbr: 'Oli', name: 'Olivose', color: SaccharideColors.Blue, type: SaccharideType.DiDeoxyhexose },
{ abbr: 'Tyv', name: 'Tyvelose', color: SaccharideColors.Green, type: SaccharideType.DiDeoxyhexose },
@@ -181,8 +184,8 @@ const Monosaccharides: SaccharideComponent[] = [
{ abbr: 'MurNAc', name: 'N-Acetyl Muramic Acid', color: SaccharideColors.Purple, type: SaccharideType.Unknown },
{ abbr: 'MurNGc', name: 'N-Glycolyl Muramic Acid', color: SaccharideColors.LightBlue, type: SaccharideType.Unknown },
{ abbr: 'Mur', name: 'Muramic Acid', color: SaccharideColors.Brown, type: SaccharideType.Unknown },
{ abbr: 'K3O', name: 'D-glycero-a-D-talo-oct-2-Ulosonic Acid', color: SaccharideColors.Red, type: SaccharideType.Unknown },
{ abbr: 'dUA', name: '4-deoxy-4,5-didehydro iduronic acid', color: SaccharideColors.Secondary, type: SaccharideType.Unknown },
{ abbr: 'K3O', name: 'D-glycero-a-D-talo-oct-2-Ulosonic Acid', color: SaccharideColors.Red, type: SaccharideType.Unknown }, // from GLYCAM
{ abbr: 'dUA', name: '4-deoxy-4,5-didehydro iduronic acid', color: SaccharideColors.Secondary, type: SaccharideType.Unknown }, // from GLYCAM
{ abbr: 'Api', name: 'Apicose', color: SaccharideColors.Green, type: SaccharideType.Assigned },
{ abbr: 'Fru', name: 'Fructose', color: SaccharideColors.Green, type: SaccharideType.Assigned },

View File

@@ -3,12 +3,14 @@
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Adam Midlik <midlik@gmail.com>
*/
import { SymmetryOperator } from '../../../mol-math/geometry/symmetry-operator';
import { Model } from '../model';
import { GridLookup3D, Lookup3D, Spacegroup } from '../../../mol-math/geometry';
import { IntraUnitBonds, computeIntraUnitBonds } from './unit/bonds';
import { VdwRadius } from '../model/properties/atomic';
import { CoarseElements, CoarseSphereConformation, CoarseGaussianConformation } from '../model/properties/coarse';
import { BitFlags } from '../../../mol-util';
import { UnitRings } from './unit/rings';
@@ -46,7 +48,7 @@ namespace Unit {
export function create<K extends Kind>(id: number, invariantId: number, chainGroupId: number, traits: Traits, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set, props?: K extends Kind.Atomic ? AtomicProperties : CoarseProperties): Unit {
switch (kind) {
case Kind.Atomic: return new Atomic(id, invariantId, chainGroupId, traits, model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation), props ?? AtomicProperties());
case Kind.Atomic: return new Atomic(id, invariantId, chainGroupId, traits, model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, getAtomicRadiusFunc(model)), props ?? AtomicProperties());
case Kind.Spheres: return createCoarse(id, invariantId, chainGroupId, traits, model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)), props ?? CoarseProperties());
case Kind.Gaussians: return createCoarse(id, invariantId, chainGroupId, traits, model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)), props ?? CoarseProperties());
}
@@ -182,6 +184,11 @@ namespace Unit {
return {};
}
function getAtomicRadiusFunc(model: Model) {
const type_symbol = model.atomicHierarchy.atoms.type_symbol.value;
return (i: ElementIndex) => VdwRadius(type_symbol(i));
}
function getSphereRadiusFunc(model: Model) {
const r = model.coarseConformation.spheres.radius;
return (i: ElementIndex) => r[i];
@@ -270,7 +277,7 @@ namespace Unit {
}
const conformation = (this.model.atomicConformation !== model.atomicConformation || operator !== this.conformation.operator)
? SymmetryOperator.createMapping<ElementIndex>(operator, model.atomicConformation)
? SymmetryOperator.createMapping<ElementIndex>(operator, model.atomicConformation, getAtomicRadiusFunc(model))
: this.conformation;
return new Atomic(this.id, this.invariantId, this.chainGroupId, this.traits, model, this.elements, conformation, props);
}
@@ -278,9 +285,10 @@ namespace Unit {
get boundary() {
if (this.props.boundary) return this.props.boundary;
const { x, y, z } = this.model.atomicConformation;
const radius = Model.getAtomicRadii(this.model);
this.props.boundary = Traits.is(this.traits, Trait.FastBoundary)
? getFastBoundary({ x, y, z, indices: this.elements })
: getBoundary({ x, y, z, indices: this.elements });
? getFastBoundary({ x, y, z, radius, indices: this.elements })
: getBoundary({ x, y, z, radius, indices: this.elements });
return this.props.boundary;
}
@@ -456,11 +464,11 @@ namespace Unit {
get boundary() {
if (this.props.boundary) return this.props.boundary;
// TODO: support sphere radius?
const { x, y, z } = this.getCoarseConformation();
const radius = this.getCoarseRadii();
this.props.boundary = Traits.is(this.traits, Trait.FastBoundary)
? getFastBoundary({ x, y, z, indices: this.elements })
: getBoundary({ x, y, z, indices: this.elements });
? getFastBoundary({ x, y, z, radius, indices: this.elements })
: getBoundary({ x, y, z, radius, indices: this.elements });
return this.props.boundary;
}
@@ -494,6 +502,10 @@ namespace Unit {
return getCoarseConformation(this.kind, this.model);
}
private getCoarseRadii() {
return getCoarseRadii(this.kind, this.model);
}
constructor(id: number, invariantId: number, chainGroupId: number, traits: Traits, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties) {
this.kind = kind;
this.objectPrimitive = kind === Kind.Spheres ? 'sphere' : 'gaussian';
@@ -514,6 +526,10 @@ namespace Unit {
return kind === Kind.Spheres ? model.coarseConformation.spheres : model.coarseConformation.gaussians;
}
function getCoarseRadii(kind: Kind, model: Model) {
return kind === Kind.Spheres ? model.coarseConformation.spheres.radius : undefined; // Zero radius for gaussians
}
interface CoarseProperties extends BaseProperties { }
function CoarseProperties(): CoarseProperties {

View File

@@ -4,23 +4,30 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Canvas3DParams } from '../../../mol-canvas3d/canvas3d';
import { PluginContext } from '../../../mol-plugin/context';
import { PluginState } from '../../../mol-plugin/state';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { PluginStateSnapshotManager } from '../../manager/snapshots';
import { PluginStateAnimation } from '../model';
async function setPartialSnapshot(plugin: PluginContext, entry: Partial<PluginStateSnapshotManager.Entry['snapshot']>, first = false) {
if (entry.data) {
await plugin.runTask(plugin.state.data.setSnapshot(entry.data));
// update the canvas3d trackball with the snapshot
if (entry.canvas3d?.props?.trackball) {
plugin.canvas3d?.setProps({
trackball: entry.canvas3d?.props?.trackball
});
}
}
if (entry.canvas3d?.props) {
const settings = PD.normalizeParams(Canvas3DParams, entry.canvas3d.props, 'children');
if (entry.camera?.current || entry.camera?.focus) {
// Avoid multiple camera transitions (creates ugly cases when camera in old and new snapshot is the same)
settings.camera = undefined;
settings.cameraClipping = undefined;
settings.cameraFog = undefined;
}
plugin.canvas3d?.setProps(settings);
}
if (entry.camera?.current) {
plugin.canvas3d?.requestCameraReset({
snapshot: entry.camera.current,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -25,7 +25,7 @@ import { changeCameraRotation, structureLayingTransform } from './focus-camera/o
// TODO: make this customizable somewhere?
const DefaultCameraFocusOptions = {
minRadius: 5,
minRadius: 1,
extraRadius: 4,
durationMs: 250,
};

View File

@@ -195,6 +195,8 @@ const _Warning = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M1
export function WarningSvg() { return _Warning; }
const _ContentCut = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M 9.64 7.64 c 0.23 -0.5 0.36 -1.05 0.36 -1.64 c 0 -2.21 -1.79 -4 -4 -4 S 2 3.79 2 6 s 1.79 4 4 4 c 0.59 0 1.14 -0.13 1.64 -0.36 L 10 12 l -2.36 2.36 C 7.14 14.13 6.59 14 6 14 c -2.21 0 -4 1.79 -4 4 s 1.79 4 4 4 s 4 -1.79 4 -4 c 0 -0.59 -0.13 -1.14 -0.36 -1.64 L 12 14 l 7 7 h 3 v -1 L 9.64 7.64 Z M 6 8 c -1.1 0 -2 -0.89 -2 -2 s 0.9 -2 2 -2 s 2 0.89 2 2 s -0.9 2 -2 2 Z m 0 12 c -1.1 0 -2 -0.89 -2 -2 s 0.9 -2 2 -2 s 2 0.89 2 2 s -0.9 2 -2 2 Z m 6 -7.5 c -0.28 0 -0.5 -0.22 -0.5 -0.5 s 0.22 -0.5 0.5 -0.5 s 0.5 0.22 0.5 0.5 s -0.22 0.5 -0.5 0.5 Z M 19 3 l -6 6 l 2 2 l 7 -7 V 3 Z' /></svg>;
export function ContentCutSvg() { return _ContentCut; }
const _LightMode = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0 c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2 c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1 C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06 c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41 l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41 c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36 c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z' /></svg>;
export function LightModeSvg() { return _LightMode; }
// The following icons are adapted from https://icons.getbootstrap.com/ and
// licensed with https://github.com/twbs/bootstrap/blob/main/LICENSE

View File

@@ -418,12 +418,19 @@ export class TextControl extends SimpleParam<PD.Text> {
function TextCtrl({ props, placeholder, update }: { props: ParamProps<PD.Text>, placeholder: string, update: (v: string) => any }) {
const [value, setValue] = React.useState(props.value);
const [composition, setComposition] = React.useState(false);
React.useEffect(() => setValue(props.value), [props.value]);
const onChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (props.param.disableInteractiveUpdates) setValue(e.target.value);
if (composition || props.param.disableInteractiveUpdates) setValue(e.target.value);
else update(e.target.value);
};
const onCompositionEnd = (e: React.CompositionEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setComposition(false);
const target = e.target as EventTarget & (HTMLInputElement | HTMLTextAreaElement);
if (props.param.disableInteractiveUpdates) setValue(target.value);
else update(target.value);
};
const onBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (props.param.disableInteractiveUpdates) update(e.target.value);
};
@@ -443,13 +450,15 @@ function TextCtrl({ props, placeholder, update }: { props: ParamProps<PD.Text>,
return <div className='msp-control-text-area-wrapper'>
<textarea
value={value ?? ''} placeholder={placeholder} disabled={props.isDisabled}
onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown}
onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown}
onCompositionStart={() => setComposition(true)} onCompositionEnd={onCompositionEnd}
/>
</div>;
} else {
return <input type='text'
value={value ?? ''} placeholder={placeholder} disabled={props.isDisabled}
onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown}
onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown}
onCompositionStart={() => setComposition(true)} onCompositionEnd={onCompositionEnd}
/>;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -14,7 +14,7 @@ import { PluginConfig } from '../mol-plugin/config';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { PluginUIComponent } from './base';
import { Button, ControlGroup, IconButton } from './controls/common';
import { AspectRatioSvg, AutorenewSvg, BuildOutlinedSvg, CameraOutlinedSvg, CloseSvg, FullscreenSvg, HeadsetVRSvg, TuneSvg } from './controls/icons';
import { AspectRatioSvg, AutorenewSvg, BuildOutlinedSvg, CameraOutlinedSvg, CloseSvg, FullscreenSvg, HeadsetVRSvg, LightModeSvg, TuneSvg } from './controls/icons';
import { ToggleSelectionModeButton } from './structure/selection';
import { ViewportCanvas } from './viewport/canvas';
import { DownloadScreenshotControls } from './viewport/screenshot';
@@ -85,6 +85,19 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
}
};
toggleIllumination = () => {
if (!this.plugin.canvas3d) return;
PluginCommands.Canvas3D.SetSettings(this.plugin, {
settings: {
illumination: {
...this.plugin.canvas3d.props.illumination,
enabled: !this.plugin.canvas3d.props.illumination.enabled
}
}
});
};
setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
};
@@ -178,6 +191,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
&& this.plugin.config.get(PluginConfig.Viewport.ShowToggleFullscreen)
&& this.icon(AspectRatioSvg, this.toggleFullscreen, 'Toggle Full Screen', this.plugin.layout.state.expandToFullscreen)}
{this.plugin.config.get(PluginConfig.Viewport.ShowSettings) && this.icon(TuneSvg, this.toggleSettingsExpanded, 'Settings / Controls Info', this.state.isSettingsExpanded)}
{this.plugin.config.get(PluginConfig.Viewport.ShowIllumination) && this.icon(LightModeSvg, this.toggleIllumination, 'Illumination', this.plugin.canvas3d?.props.illumination.enabled || false)}
{xr && this.icon(HeadsetVRSvg, this.toggleXR, xrTitle, xrIsPresenting, !xrIsSupported)}
</div>
{this.plugin.config.get(PluginConfig.Viewport.ShowSelectionMode) && <div>

View File

@@ -190,13 +190,21 @@ export const CameraControls = PluginBehavior.create<CameraControlsProps>({
if (Binding.matchKey(b.keySpinAnimation, code, modifiers, key)) {
const name = tp.animate.name !== 'spin' ? 'spin' : 'off';
if (name === 'off') {
this.ctx.canvas3d.setProps({
trackball: { animate: { name, params: {} } }
PluginCommands.Canvas3D.SetSettings(this.ctx, {
settings: {
trackball: {
...tp,
animate: { name, params: {} }
}
}
});
} else {
this.ctx.canvas3d.setProps({
trackball: { animate: {
name, params: { speed: 1 } }
PluginCommands.Canvas3D.SetSettings(this.ctx, {
settings: {
trackball: {
...tp,
animate: { name, params: { speed: 0.1 } }
}
}
});
}
@@ -205,13 +213,21 @@ export const CameraControls = PluginBehavior.create<CameraControlsProps>({
if (Binding.matchKey(b.keyRockAnimation, code, modifiers, key)) {
const name = tp.animate.name !== 'rock' ? 'rock' : 'off';
if (name === 'off') {
this.ctx.canvas3d.setProps({
trackball: { animate: { name, params: {} } }
PluginCommands.Canvas3D.SetSettings(this.ctx, {
settings: {
trackball: {
...tp,
animate: { name, params: {} }
}
}
});
} else {
this.ctx.canvas3d.setProps({
trackball: { animate: {
name, params: { speed: 0.3, angle: 10 } }
PluginCommands.Canvas3D.SetSettings(this.ctx, {
settings: {
trackball: {
...tp,
animate: { name, params: { speed: 0.3, angle: 10 } }
}
}
});
}
@@ -220,8 +236,13 @@ export const CameraControls = PluginBehavior.create<CameraControlsProps>({
if (Binding.matchKey(b.keyToggleFlyMode, code, modifiers, key)) {
const flyMode = !tp.flyMode;
this.ctx.canvas3d.setProps({
trackball: { flyMode }
PluginCommands.Canvas3D.SetSettings(this.ctx, {
settings: {
trackball: {
...tp,
flyMode
}
}
});
if (this.ctx.canvas3dContext?.canvas) {
@@ -234,10 +255,12 @@ export const CameraControls = PluginBehavior.create<CameraControlsProps>({
}
if (Binding.matchKey(b.keyGlobalIllumination, code, modifiers, key)) {
this.ctx.canvas3d.setProps({
illumination: {
...ip,
enabled: !ip.enabled,
PluginCommands.Canvas3D.SetSettings(this.ctx, {
settings: {
illumination: {
...ip,
enabled: !ip.enabled
}
}
});
}

View File

@@ -61,6 +61,7 @@ export const PluginConfig = {
ShowAnimation: item('viewer.show-animation-button', true),
ShowTrajectoryControls: item('viewer.show-trajectory-controls', true),
ShowScreenshotControls: item('viewer.show-screenshot-controls', true),
ShowIllumination: item('viewer.show-illumination-button', true),
ShowXR: item<'auto' | 'always' | 'never'>('viewer.show-xr', 'always'),
},
Download: {

View File

@@ -89,7 +89,13 @@ class PluginState extends PluginComponent {
if (snapshot.behaviour) await this.plugin.runTask(this.behaviors.setSnapshot(snapshot.behaviour));
if (snapshot.data) await this.plugin.runTask(this.data.setSnapshot(snapshot.data));
if (snapshot.canvas3d?.props) {
const settings = PD.normalizeParams(Canvas3DParams, snapshot.canvas3d.props, 'children');
const settings: Partial<Canvas3DProps> = PD.normalizeParams(Canvas3DParams, snapshot.canvas3d.props, 'children');
if (snapshot.camera?.current || snapshot.camera?.focus) {
// Avoid multiple camera transitions (creates ugly cases when camera in old and new snapshot is the same)
settings.camera = undefined;
settings.cameraClipping = undefined;
settings.cameraFog = undefined;
}
await PluginCommands.Canvas3D.SetSettings(this.plugin, { settings });
}
if (snapshot.canvas3dContext?.props) {