From dd11cacae45b01495b3f1c559e8d8f009818d588 Mon Sep 17 00:00:00 2001 From: David Sehnal Date: Sat, 2 Aug 2025 18:19:01 +0200 Subject: [PATCH] Markdown Commands and MVS improvements (#1597) * add query command to markdown extensions * fix typo * better postprocessing param support in MVS * molstar_mesh/label/line_params --- CHANGELOG.md | 5 +- .../plugin/managers/markdown-extensions.md | 10 ++- src/extensions/mvs/camera.ts | 11 ++- src/extensions/mvs/components/primitives.ts | 6 ++ .../manager/markdown-extensions.ts | 68 +++++++++++++++++++ 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c15c2fc0..26d01253c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ Note that since we don't clearly distinguish between a public and private interf - MolViewSpec extension: - Generic color schemes (`palette` parameter for color_from_* nodes) - Annotation field remapping (`field_remapping` parameter for color_from_* nodes) - - Representation node: support custom property `molstar_reprepresentation_params`, - - Canvas node: support custom properties `molstar_enable_outline`, `molstar_enable_shadow`, `molstar_enable_ssao` + - Representation node: support custom property `molstar_reprepresentation_params` + - Primitives node: support custom property `molstar_mesh/label/line_params` + - Canvas node: support custom properties `molstar_enable_outline/shadow/ssao`, `molstar_outline/shadow/ssao_params` - `clip` node support for structure and volume representations - `grid_slice` representation support for volumes - Support tethers and background for primitive labels diff --git a/docs/docs/plugin/managers/markdown-extensions.md b/docs/docs/plugin/managers/markdown-extensions.md index 36128aa76..3292443b8 100644 --- a/docs/docs/plugin/managers/markdown-extensions.md +++ b/docs/docs/plugin/managers/markdown-extensions.md @@ -14,12 +14,20 @@ The main use case of this is enriching [MolViewSpec](`https://molstar.org/mol-vi Extends Markdown Hyperlink syntax to support expressions of the form `[title](!c1=v1&c2=v2&...)` into an executable command. The command can be executed either on click, mouse enter, or mouse leave. +Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `encodeURIComponent`, in Python `urllib.parse.quote_plus/urlencode`). + ### Built-in Commands - `center-camera` - Centers the camera - `apply-snapshot=key` - Loads snapshots with the provided key - `focus-refs=ref1,ref2,...` - On click, focuses nodes with the provided refs - `highlight-refs=ref1,ref2,...` - On mouse over, highlights the provided refs +- `query=...&lang=...&action=highlight,focus&focus-radius=...` + - `query` is an expression (e.g., `resn HEM` when using PyMol syntax) + - (optional) `lang` is one of `mol-script` (default), `pymol`, `vmd`, `jmol` + - (optional) `action` is an array of `highlight` (default), `focus` (multiple actions can be specified) + - (optional) `focus-radius` is extra distance applied when focusing the selection (default is `3`) + - Example: `[HEM](!query%3Dresn%20HEM%26lang%3Dpymol%26action%3Dhighlight%2Cfocus)` highlights or focuses the HEM residue (the command must be URL encoded because it contains spaces and possibly other special characters) ## Custom Content @@ -28,7 +36,7 @@ Extends Markdown Image syntax to support expressions of the form `![alt](!c1=v1& ### Built-in Custom Content - `color-swatch=color` - Renders a box with the provided color - Color palettes: - - `color-palette-name=name` - Renders a gradient with the provivided named color palette (see `mol-util/color/lists.ts` for supported color schemes) + - `color-palette-name=name` - Renders a gradient with the provided named color palette (see `mol-util/color/lists.ts` for supported color schemes) - `color-palette-colors=color1,color2` - Renders a gradient with the provided colors - `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px` - `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em` diff --git a/src/extensions/mvs/camera.ts b/src/extensions/mvs/camera.ts index 2fc38b042..555ba9729 100644 --- a/src/extensions/mvs/camera.ts +++ b/src/extensions/mvs/camera.ts @@ -130,21 +130,26 @@ export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: Mol const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor; const outline = !!canvasNode?.custom?.molstar_enable_outline; + const outlineParams = canvasNode?.custom?.molstar_outline_params; + const shadow = !!canvasNode?.custom?.molstar_enable_shadow; + const shadowParams = canvasNode?.custom?.molstar_shadow_params; + const occlusion = !!canvasNode?.custom?.molstar_enable_ssao; + const ssaoParams = canvasNode?.custom?.molstar_ssao_params; return { ...oldCanvasProps, postprocessing: { ...oldCanvasProps.postprocessing, outline: outline - ? { name: 'on', params: ParamDefinition.getDefaultValues(OutlineParams) } + ? { name: 'on', params: { ...ParamDefinition.getDefaultValues(OutlineParams), ...outlineParams } } : oldCanvasProps.postprocessing.outline, shadow: shadow - ? { name: 'on', params: ParamDefinition.getDefaultValues(ShadowParams) } + ? { name: 'on', params: { ...ParamDefinition.getDefaultValues(ShadowParams), ...shadowParams } } : oldCanvasProps.postprocessing.shadow, occlusion: occlusion - ? { name: 'on', params: ParamDefinition.getDefaultValues(SsaoParams) } + ? { name: 'on', params: { ...ParamDefinition.getDefaultValues(SsaoParams), ...ssaoParams } } : oldCanvasProps.postprocessing.occlusion, }, renderer: { diff --git a/src/extensions/mvs/components/primitives.ts b/src/extensions/mvs/components/primitives.ts index c243211ac..6438af78a 100644 --- a/src/extensions/mvs/components/primitives.ts +++ b/src/extensions/mvs/components/primitives.ts @@ -140,11 +140,13 @@ export const MVSBuildPrimitiveShape = MVSTransform({ if (params.kind === 'mesh') { if (!hasPrimitiveKind(a.data, 'mesh')) return StateObject.Null; + const customMeshParams = a.data.node.custom?.molstar_mesh_params; return new SO.Shape.Provider({ label, data: context, params: { ...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1 }), + ...customMeshParams, ...snapshotKey, }, getShape: (_, data, __, prev: any) => buildPrimitiveMesh(data, prev?.geometry), @@ -155,6 +157,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({ const options = a.data.options; const bgColor = options?.label_background_color; + const customLabelParams = a.data.node.custom?.molstar_label_params; return new SO.Shape.Provider({ label, data: context, @@ -167,6 +170,7 @@ export const MVSBuildPrimitiveShape = MVSTransform({ background: isDefined(bgColor), backgroundColor: isDefined(bgColor) ? decodeColor(bgColor) : undefined, }), + ...customLabelParams, ...snapshotKey, }, getShape: (_, data, props, prev: any) => buildPrimitiveLabels(data, prev?.geometry, props), @@ -175,11 +179,13 @@ export const MVSBuildPrimitiveShape = MVSTransform({ } else if (params.kind === 'lines') { if (!hasPrimitiveKind(a.data, 'line')) return StateObject.Null; + const customLineParams = a.data.node.custom?.molstar_line_params; return new SO.Shape.Provider({ label, data: context, params: { ...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1 }), + ...customLineParams, ...snapshotKey, }, getShape: (_, data, __, prev: any) => buildPrimitiveLines(data, prev?.geometry), diff --git a/src/mol-plugin-state/manager/markdown-extensions.ts b/src/mol-plugin-state/manager/markdown-extensions.ts index 9d7f248e1..e4ce8fd08 100644 --- a/src/mol-plugin-state/manager/markdown-extensions.ts +++ b/src/mol-plugin-state/manager/markdown-extensions.ts @@ -8,6 +8,8 @@ import { getCellBoundingSphere } from '../../mol-plugin-state/manager/focus-came import { PluginStateObject } from '../../mol-plugin-state/objects'; import { StateObjectCell } from '../../mol-state'; import { PluginContext } from '../../mol-plugin/context'; +import { Script } from '../../mol-script/script'; +import { QueryContext, QueryFn, StructureElement, StructureSelection } from '../../mol-model/structure'; export type MarkdownExtensionEvent = 'click' | 'mouse-enter' | 'mouse-leave'; @@ -82,6 +84,72 @@ export const BuiltInMarkdownExtension: MarkdownExtension[] = [ } } }, + { + name: 'query', + execute: ({ event, args, manager }) => { + const expression = args['query']; + if (!expression?.length) return; + + // supported languages: mol-script, pymol, vmd, jmol + const language = args['lang'] || 'mol-script'; + // supported actions: highlight, focus + const action = parseArray(args['action'] || 'highlight'); + const focusRadius = parseFloat(args['focus-radius'] || '3'); + + if (event === 'mouse-leave') { + if (action.includes('highlight')) { + manager.plugin.managers.interactivity.lociHighlights.clearHighlights(); + } + return; + } + + let query: QueryFn; + try { + query = Script.toQuery({ + language: language as Script.Language, + expression + }); + } catch (e) { + console.warn(`Failed to parse query '${expression}' (${language})`, e); + return; + } + + const structures = manager.plugin.state.data.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure)); + + if (event === 'mouse-enter') { + if (!action.includes('focus')) { + return; + } + manager.plugin.managers.interactivity.lociHighlights.clearHighlights(); + for (const structure of structures) { + if (!structure.obj?.data) continue; + const selection = query(new QueryContext(structure.obj.data)); + const loci = StructureSelection.toLociWithSourceUnits(selection); + manager.plugin.managers.interactivity.lociHighlights.highlight({ + loci, + }, false); + } + } + + if (event === 'click') { + if (!action.includes('focus')) { + return; + } + const spheres = structures.map(s => { + if (!s.obj?.data) return undefined; + const selection = query(new QueryContext(s.obj.data)); + if (StructureSelection.isEmpty(selection)) return; + + const loci = StructureSelection.toLociWithSourceUnits(selection); + return StructureElement.Loci.getBoundary(loci).sphere; + }).filter(s => !!s); + + if (spheres.length) { + manager.plugin.managers.camera.focusSpheres(spheres, s => s, { extraRadius: focusRadius }); + } + } + }, + }, ]; export class MarkdownExtensionManager {