Files
Jungfraujoch/frontend/src/components/structure/loadMaps.ts
T
leonarski_f 2df1d8a2ad Mol* viewer: aaredb's structure-viewer setup, on a black stage
The owner's verdict on the screenshots: aaredb (gitea.psi.ch/mx/aaredb, the PSI MX group's own
structure viewer) has it right, so its Mol* setup is taken over, files copied from
frontend/src/components/Structure at 8ece2c2 into frontend/src/components/structure:
- createPlugin.ts: headless DefaultPluginSpec plugin, manual camera reset; background black (owner)
  instead of aaredb's 0x0d1a26.
- MolstarCanvas.tsx: fresh canvas per mount, one-tick StrictMode deferral, resize, WebGL context-loss
  recovery, the Coot control profile wired.
- cootControls.ts / cootTrackball.ts: Coot mouse language - left drag rotates (Coot 0.9.8 trackball),
  right drag zooms, middle drag pans, middle click recentres on a residue, scroll clips the slab,
  shift+scroll zooms (camera sync and residue pick left out: no split view here).
- loadModel.ts: cartoon coloured by chain from a palette clear of the map colours, polymer sticks and
  waters built hidden, ligands as ball-and-stick; display modes cartoon / sticks / both and a waters
  switch; reads mmCIF too, and gives the atom coordinates for carving.
- loadMaps.ts: isosurface wireframe meshes at ABSOLUTE levels from the full cell's mean and RMS,
  2Fo-Fc steel blue, Fo-Fc green/red at +/-sigma, not pickable; plus the anomalous map as a magenta
  +sigma mesh in the same style.
- densityCrop.ts: the density worker's crop to the model's box + 6 A, the 4.5 A carve of the 2Fo-Fc map
  around the atoms (never a difference map) and the CCP4 encoding, applied to the broker's one-cell map.
The panel and defaults follow aaredb's StructureView: map rows with switch and sigma slider (2Fo-Fc 1.0,
0.5-4 by 0.1; Fo-Fc 3.0, 1-8 by 0.5; anomalous 4.0, 1-8 by 0.5), debounced sigma updates, model mode
buttons, a controls legend. This replaces the camera-following 10 A map sphere: aaredb's equivalent is
the carved map with the scroll-driven clip slab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit 5763b619fe4ec9999ac790960a5eb033f511c546)
2026-10-22 20:18:51 +02:00

110 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Adapted from aaredb (gitea.psi.ch/mx/aaredb, the PSI MX group's own code), frontend/src/components/Structure/molstar/loadMaps.ts (+ the anomalous layer) at 8ece2c2.
import { PluginContext } from 'molstar/lib/mol-plugin/context';
import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
import { createVolumeRepresentationParams } from 'molstar/lib/mol-plugin-state/helpers/volume-representation-params';
import { Volume } from 'molstar/lib/mol-model/volume';
import { Color } from 'molstar/lib/mol-util/color';
import { setSubtreeVisibility } from 'molstar/lib/mol-plugin/behavior/static/state';
import type { ComputedMap } from './densityCrop';
// Density volumes in the Mol* state tree. σ sliders map user-σ to ABSOLUTE iso
// values via the map's full-grid mean/rms (reported by the worker) — Mol*'s own
// relative sigma would re-derive stats from whatever grid it holds, which stops
// being crystallographically meaningful the moment we crop. A σ change is a
// representation param update (re-mesh only — no FFT, no re-upload).
export const MAP_COLORS = {
twofofc: 0x4d94d6, // steel-blue
fofcPos: 0x35b96a, // green
fofcNeg: 0xd6564d, // red
anom: 0xe040e0, // magenta (anomalous difference map, +sigma only)
} as const;
export interface MapLayer {
volumeRef: string;
reprRefs: string[]; // one for 2fofc and anom, two (+/−) for fofc
rmsd: number;
mean: number;
}
const abs = (map: { mean: number; rmsd: number }, sigma: number) =>
Volume.IsoValue.absolute(map.mean + sigma * map.rmsd);
async function addIso(
plugin: PluginContext,
volumeRef: string,
map: ComputedMap,
sigma: number,
color: number,
): Promise<string> {
const volumeCell = plugin.state.data.select(volumeRef)[0];
const repr = plugin
.build()
.to(volumeRef)
.apply(
StateTransforms.Representation.VolumeRepresentation3D,
createVolumeRepresentationParams(plugin, volumeCell?.obj?.data, {
type: 'isosurface',
typeParams: { isoValue: abs(map, sigma), visuals: ['wireframe'], sizeFactor: 1 },
color: 'uniform',
colorParams: { value: Color(color) },
}),
);
await repr.commit();
// The mesh must NOT be pickable: a pickable isosurface shields the model,
// so residue clicks and middle-click recenter would hit map cells instead
// of atoms. Pickability is runtime state on the INNER representation
// (cell.obj.data.repr — the cell data itself has no setState), and the
// pick buffer needs a redraw to drop the mesh.
const reprCell = plugin.state.data.select(repr.ref)[0];
const reprObj = (reprCell?.obj?.data as { repr?: { setState?: (s: { pickable: boolean }) => void } })?.repr;
reprObj?.setState?.({ pickable: false });
plugin.canvas3d?.requestDraw?.();
return repr.ref;
}
export async function loadMapLayer(
plugin: PluginContext,
map: ComputedMap,
kind: '2fofc' | 'fofc' | 'anom',
sigma: number,
): Promise<MapLayer> {
const data = await plugin.builders.data.rawData({
data: new Uint8Array(map.ccp4),
label: kind,
});
const parsed = await plugin.dataFormats.get('ccp4')!.parse(plugin, data);
const volumeRef: string = parsed.volume.ref;
const reprRefs: string[] = [];
if (kind === '2fofc') {
reprRefs.push(await addIso(plugin, volumeRef, map, sigma, MAP_COLORS.twofofc));
} else if (kind === 'anom') {
reprRefs.push(await addIso(plugin, volumeRef, map, sigma, MAP_COLORS.anom));
} else {
reprRefs.push(await addIso(plugin, volumeRef, map, +sigma, MAP_COLORS.fofcPos));
reprRefs.push(await addIso(plugin, volumeRef, map, -sigma, MAP_COLORS.fofcNeg));
}
return { volumeRef, reprRefs, rmsd: map.rmsd, mean: map.mean };
}
/** σ slider update: param-only change, re-mesh without recompute. */
export async function updateLayerSigma(
plugin: PluginContext,
layer: MapLayer,
sigma: number,
): Promise<void> {
const sigmas = layer.reprRefs.length === 2 ? [+sigma, -sigma] : [sigma];
const b = plugin.build();
layer.reprRefs.forEach((ref, i) => {
b.to(ref).update((old: { type: { params: { isoValue: unknown } } }) => {
old.type.params.isoValue = abs(layer, sigmas[i]);
});
});
await b.commit();
}
export function setLayerVisible(plugin: PluginContext, layer: MapLayer, visible: boolean): void {
setSubtreeVisibility(plugin.state.data, layer.volumeRef, !visible); // arg = isHidden
}