Compare commits

...

11 Commits

Author SHA1 Message Date
dsehnal
c5f2767efc 5.2.0 2025-10-31 17:26:52 +01:00
dsehnal
66f5a81a5d changelog 2025-10-31 17:25:24 +01:00
David Sehnal
9e90e11bfc MVS Improvements (#1684)
* MVS primitives clipping

* camera near distance
2025-10-31 16:43:59 +01:00
midlik
ab372a89d6 MVS: fix persisting tooltips and other fixes (#1688)
* MVS: Fix tooltips persisting across snapshots

* MVS: Fix CIF annotations with no selector columns being ignored

* Vec3.orthogonalize handle special cases (fixes trackpad lock in MVS)

* Update CHANGELOG
2025-10-31 14:35:56 +01:00
David Sehnal
c6506d515f Fix CIF Parser edge case (#1687)
* Fix CIF Parser edge case

* header
2025-10-28 15:02:35 +01:00
Alexander Rose
7d0f84ff72 Merge pull request #1679 from giagitom/fix-screenshot-helper-change-transparency
Handle transparency mode updates on ImagePass
2025-10-25 14:24:13 -07:00
Alexander Rose
31495ab02a simplify 2025-10-25 14:20:30 -07:00
Alexander Rose
853ad5c916 pass transparency via scene 2025-10-25 14:15:40 -07:00
Alexander Rose
51fc525215 Merge https://github.com/molstar/molstar into pr/giagitom/1679 2025-10-25 13:53:34 -07:00
giagitom
6f9fed180d update headers 2025-10-21 21:56:37 +02:00
giagitom
5ecd176f20 Handle transparency mode updates on ImagePass 2025-10-21 21:50:42 +02:00
16 changed files with 105 additions and 42 deletions

View File

@@ -5,6 +5,16 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [v5.2.0] - 2025-10-31
- Handle transparency updates on ImagePass
- Fix CIF parser edge case when the last token is escaped
- MolViewSpec
- Fix tooltips persisting across snapshots
- Fix CIF annotations with no selector columns being ignored
- Fix trackpad lock when camera up parallel to direction
- Add clipping support for primitives
- Support near camera distance
## [v5.1.0] - 2025-10-25
- Fix createColorScaleByType when offsets are available
- Get bond orders from non-standard CONECT records in PDB files
@@ -13,7 +23,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Support "magic window" style AR (via WebXR)
- Fix `PluginState.getStateTransitionFrameIndex`
- Update `GlycamSaccharideNames` and `Monosaccharides` in `carbohydrates/constants.ts`
- Support custom ref resolvers in `State`
- Support custom ref resolvers in `State`
- Add full-screen mode support to layout manager
- Add `show-toggle-fullscreen` URL param option to Viewer app
- MolViewSpec

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "molstar",
"version": "5.1.2",
"version": "5.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "molstar",
"version": "5.1.2",
"version": "5.2.0",
"license": "MIT",
"dependencies": {
"@types/argparse": "^2.0.17",

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "5.1.2",
"version": "5.2.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {

View File

@@ -62,7 +62,15 @@ export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: Mols
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
const up = Vec3.create(...params.up);
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius, radiusMax: radius };
const snapshot: Partial<Camera.Snapshot> = {
target,
position,
up,
radius,
radiusMax: radius,
minNear: params.near ?? undefined,
};
return snapshot;
}

View File

@@ -377,6 +377,7 @@ function getRowsFromCif(data: CifCategory, schema: MVSAnnotationSchema, fieldRem
const columnArray = getArrayFromCifCategory(data, srcKey, cifSchema[key]); // Avoiding `column.toArray` as it replaces . and ? fields by 0 or ''
if (columnArray) columns[key] = columnArray;
}
if (Object.keys(columns).length === 0) return new Array(data.rowCount).fill({});
return objectOfArraysToArrayOfObjects(columns);
}

View File

@@ -33,6 +33,7 @@ import { Task } from '../../../mol-task';
import { round } from '../../../mol-util';
import { range } from '../../../mol-util/array';
import { Asset } from '../../../mol-util/assets';
import { Clip } from '../../../mol-util/clip';
import { Color } from '../../../mol-util/color';
import { MarkerActions } from '../../../mol-util/marker-action';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -141,7 +142,8 @@ export const MVSBuildPrimitiveShape = MVSTransform({
from: MVSPrimitivesData,
to: SO.Shape.Provider,
params: {
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh')
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh'),
clip: PD.Value<Clip.Props | undefined>(undefined, { isHidden: true })
}
})({
apply({ a, params, dependencies }) {
@@ -160,7 +162,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
label,
data: context,
params: {
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, ...customMeshParams }),
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customMeshParams }),
...snapshotKey,
...markdownCommands,
},
@@ -184,6 +186,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
tetherLength: options?.label_tether_length ?? 1,
background: isDefined(bgColor),
backgroundColor: isDefined(bgColor) ? decodeColor(bgColor) : undefined,
clip: params.clip,
...customLabelParams,
}),
...snapshotKey,
@@ -200,7 +203,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({
label,
data: context,
params: {
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, ...customLineParams }),
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customLineParams }),
...snapshotKey,
...markdownCommands,
},

View File

@@ -479,7 +479,7 @@ function getClipObject(node: MolstarNode<'clip'>): Clip.Props['objects'][number]
}
}
export function clippingForNode(node: MolstarSubtree<'representation' | 'volume_representation'>): Clip.Props | undefined {
export function clippingForNode(node: MolstarSubtree<'representation' | 'volume_representation' | 'primitives' | 'primitives_from_uri'>): Clip.Props | undefined {
const children = getChildren(node).filter(c => c.kind === 'clip');
if (!children.length) return;

View File

@@ -8,7 +8,7 @@
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { Download, ParseCif, ParseCcp4, ParseDx } from '../../mol-plugin-state/transforms/data';
import { Download, ParseCcp4, ParseCif, ParseDx } from '../../mol-plugin-state/transforms/data';
import { CoordinatesFromLammpstraj, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { VolumeFromCcp4, VolumeFromDensityServerCif, VolumeFromDx } from '../../mol-plugin-state/transforms/volume';
@@ -17,6 +17,7 @@ import { PluginContext } from '../../mol-plugin/context';
import { PluginState } from '../../mol-plugin/state';
import { StateObjectSelector, StateTree } from '../../mol-state';
import { RuntimeContext, Task } from '../../mol-task';
import { Clip } from '../../mol-util/clip';
import { MolViewSpec } from './behavior';
import { createPluginStateSnapshotCamera, modifyCanvasProps, resetCanvasProps } from './camera';
import { MVSAnnotationsProvider } from './components/annotation-prop';
@@ -31,7 +32,7 @@ import { generateStateTransition } from './helpers/animation';
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
import { AnnotationFromSourceKind, AnnotationFromUriKind, clippingForNode, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
import { MVSAnimationNode, MVSAnimationSchema } from './tree/animation/animation-tree';
import { validateTree } from './tree/generic/tree-validation';
@@ -325,18 +326,16 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
const transformed = transformAndInstantiateStructure(struct, node);
const annotationTooltips = collectAnnotationTooltips(node, context);
const inlineTooltips = collectInlineTooltips(node, context);
if (annotationTooltips.length + inlineTooltips.length > 0) {
UpdateTarget.apply(struct, CustomStructureProperties, {
properties: {
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
},
autoAttach: [
MVSAnnotationTooltipsProvider.descriptor.name,
CustomTooltipsProvider.descriptor.name,
],
});
}
UpdateTarget.apply(struct, CustomStructureProperties, {
properties: {
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
},
autoAttach: [
MVSAnnotationTooltipsProvider.descriptor.name,
CustomTooltipsProvider.descriptor.name,
],
}); // CustomStructureProperties must be applied even when `annotationTooltips` and `inlineTooltips` are empty, otherwise tooltips would persists across MVS snapshots
const inlineLabels = collectInlineLabels(node, context);
if (inlineLabels.length > 0) {
const nearestReprNode = context.nearestReprMap?.get(node);
@@ -426,21 +425,23 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
},
primitives(updateParent: UpdateTarget, tree: MolstarSubtree<'primitives'>, context: MolstarLoadingContext): UpdateTarget {
const refs = getPrimitiveStructureRefs(tree);
const clip = clippingForNode(tree);
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree as any });
return applyPrimitiveVisuals(data, refs);
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 });
return applyPrimitiveVisuals(data, new Set(tree.params.references));
const clip = clippingForNode(tree);
return applyPrimitiveVisuals(data, new Set(tree.params.references), clip);
},
};
function applyPrimitiveVisuals(data: UpdateTarget, refs: Set<string>) {
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh' }, { state: { isGhost: true } }), refs);
function applyPrimitiveVisuals(data: UpdateTarget, refs: Set<string>, clip: Clip.Props | undefined) {
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh', clip }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(mesh, MVSShapeRepresentation3D);
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels' }, { state: { isGhost: true } }), refs);
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels', clip }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(labels, MVSShapeRepresentation3D);
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines' }, { state: { isGhost: true } }), refs);
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines', clip }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(lines, MVSShapeRepresentation3D);
return data;
}

View File

@@ -403,12 +403,24 @@ export class Primitives extends _Base<'primitives'> implements FocusMixin {
return this;
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
/** Add a 'clip' node and return builder pointing back to the representation node. 'clip' node instructs to apply clipping to a visual representation. */
clip(params: MVSNodeParams<'clip'> & CustomAndRef): Primitives {
this.addChild('clip', params);
return this;
}
}
/** MVS builder pointing to a 'primitives_from_uri' node */
class PrimitivesFromUri extends _Base<'primitives_from_uri'> implements FocusMixin {
focus = bindMethod(this, FocusMixinImpl, 'focus');
/** Add a 'clip' node and return builder pointing back to the representation node. 'clip' node instructs to apply clipping to a visual representation. */
clip(params: MVSNodeParams<'clip'> & CustomAndRef): PrimitivesFromUri {
this.addChild('clip', params);
return this;
}
}

View File

@@ -226,7 +226,7 @@ export const MVSTreeSchema = TreeSchema({
/** This node instructs to apply clipping to a visual representation. */
clip: {
description: 'This node instructs to apply clipping to a visual representation.',
parent: ['representation', 'volume_representation'],
parent: ['representation', 'volume_representation', 'primitives', 'primitives_from_uri'],
params: MVSClipParams,
},
/** This node instructs to apply opacity/transparency to a visual representation. */
@@ -324,6 +324,8 @@ export const MVSTreeSchema = TreeSchema({
position: RequiredField(Vector3, 'Coordinates of the camera.'),
/** Vector which will be aligned with the screen Y axis. */
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
/** Near clipping plane distance from the position. */
near: OptionalField(nullable(float), null, 'Near clipping plane distance from the position.'),
}),
},
/** This node sets canvas properties. */

View File

@@ -1359,7 +1359,7 @@ namespace Canvas3D {
}
},
getImagePass: (props: Partial<ImageProps> = {}) => {
return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, passes.draw.transparency, props);
return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, props);
},
getRenderObjects(): GraphicsRenderObject[] {
const renderObjects: GraphicsRenderObject[] = [];

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 Gianluca Tomasello <giagitom@gmail.com>
*/
import { WebGLContext } from '../../mol-gl/webgl/context';
@@ -57,10 +58,10 @@ export class ImagePass {
get width() { return this._width; }
get height() { return this._height; }
constructor(private webgl: WebGLContext, assetManager: AssetManager, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, transparency: 'wboit' | 'dpoit' | 'blended', props: Partial<ImageProps>) {
constructor(private webgl: WebGLContext, assetManager: AssetManager, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, props: Partial<ImageProps>) {
this.props = { ...PD.getDefaultValues(ImageParams), ...props };
this.drawPass = new DrawPass(webgl, assetManager, 128, 128, transparency);
this.drawPass = new DrawPass(webgl, assetManager, 128, 128, scene.transparency);
this.illuminationPass = new IlluminationPass(webgl, this.drawPass);
this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
@@ -104,6 +105,7 @@ export class ImagePass {
}
async render(runtime: RuntimeContext) {
this.drawPass.setTransparency(this.scene.transparency);
ShaderManager.ensureRequired(this.webgl, this.scene, this.props);
Camera.copySnapshot(this._camera.state, this.camera.state);
Viewport.set(this._camera.viewport, 0, 0, this._width, this._height);

View File

@@ -70,6 +70,7 @@ interface Scene extends Object3D {
readonly renderables: ReadonlyArray<GraphicsRenderable>
readonly boundingSphere: Sphere3D
readonly boundingSphereVisible: Sphere3D
readonly transparency: Transparency
readonly primitives: Scene.Group
readonly volumes: Scene.Group
@@ -383,6 +384,9 @@ namespace Scene {
}
return boundingSphereVisible;
},
get transparency() {
return transparency;
},
get markerAverage() {
if (markerAverageDirty) {
markerAverage = calculateMarkerAverage();

View File

@@ -108,9 +108,9 @@ function resetValueChanges(valueChanges: ValueChanges) {
//
export type Transparency = 'blended' | 'wboit' | 'dpoit' | undefined
export type Transparency = 'blended' | 'wboit' | 'dpoit'
function getRenderVariant(variant: string, transparency: Transparency): string {
function getRenderVariant(variant: string, transparency: Transparency | undefined): string {
if (variant === 'color') {
switch (transparency) {
case 'blended': return 'colorBlended';
@@ -136,7 +136,7 @@ export function createComputeRenderItem(ctx: WebGLContext, drawMode: DrawMode, s
*
* - assumes that `values.drawCount` and `values.instanceCount` exist
*/
export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId: number, renderVariants: T[], transparency: Transparency): RenderItem<T> {
export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId: number, renderVariants: T[], transparency: Transparency | undefined): RenderItem<T> {
const id = getNextRenderItemId();
const { stats, state, resources } = ctx;
const { instancedArrays, vertexArrayObject, multiDrawInstancedBaseVertexBaseInstance, drawInstancedBaseVertexBaseInstance } = ctx.extensions;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2019 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>
@@ -109,7 +109,7 @@ function eatEscaped(state: TokenizerState, esc: number) {
++state.position;
return;
default:
if (next === void 0) { // = "end of stream"
if (!Number.isFinite(next)) { // = "end of stream"
// get rid of the quotes.
state.tokenStart++;
state.tokenEnd = state.position;

View File

@@ -573,8 +573,8 @@ export namespace Vec3 {
const a0 = a[0], a1 = a[1], a2 = a[2];
const b0 = b[0], b1 = b[1], b2 = b[2];
return (Math.abs(a0 - b0) <= EPSILON * Math.max(1.0, Math.abs(a0), Math.abs(b0)) &&
Math.abs(a1 - b1) <= EPSILON * Math.max(1.0, Math.abs(a1), Math.abs(b1)) &&
Math.abs(a2 - b2) <= EPSILON * Math.max(1.0, Math.abs(a2), Math.abs(b2)));
Math.abs(a1 - b1) <= EPSILON * Math.max(1.0, Math.abs(a1), Math.abs(b1)) &&
Math.abs(a2 - b2) <= EPSILON * Math.max(1.0, Math.abs(a2), Math.abs(b2)));
}
const rotTemp = zero();
@@ -620,8 +620,28 @@ export namespace Vec3 {
}
/** Get a vector that is similar to `b` but orthogonal to `a` */
export function orthogonalize(out: Vec3, a: Vec3, b: Vec3) {
return normalize(out, cross(out, cross(out, a, b), a));
export function orthogonalize(out: Vec3, a: Vec3, b: Vec3): Vec3 {
// Regular case (`b` not parallel to `a`)
normalize(out, cross(out, cross(out, a, b), a));
if (!Vec3.isZero(out)) return out;
// `b` was parallel to `a`, try orthogonalize(a, X)
out[0] = 1; out[1] = 0; out[2] = 0;
normalize(out, cross(out, cross(out, a, out), a));
if (!Vec3.isZero(out)) return out;
// `X` was parallel to `a`, try orthogonalize(a, Y)
out[0] = 0; out[1] = 1; out[2] = 0;
normalize(out, cross(out, cross(out, a, out), a));
if (!Vec3.isZero(out)) return out;
// `a` was zero, return normalized `b`
normalize(out, b);
if (!Vec3.isZero(out)) return out;
// `b` was zero, return whatever
out[0] = 1; out[1] = 0; out[2] = 0;
return out;
}
/**