Compare commits

...

5 Commits

Author SHA1 Message Date
dsehnal
c04580377b 5.0.0-dev.13 2025-09-03 09:20:26 +02:00
David Sehnal
a492b38368 fix mutative use & assign NODE_ENV=production for prd builds (#1642)
* fix mutative use & assign NODE_ENV=production for prd builds

* fix type
2025-09-03 09:07:23 +02:00
midlik
518f21531e SequenceColor extension (#1611)
* MinimizeRmsd.Result include nAlignedElements

* SequenceColor extension

* SequenceColor extension - forceUpdate when custom prop changes

* SequenceColor extension - proper caching

* Update CHANGELOG

* SequenceColor extension - registry

* SequenceColor extension - refactor

* SequenceColor extension - minor changes

* SequenceColor extension - switch to experimentalSequenceColorTheme

* SequenceColor extension - ensureCustomProperties

* SequenceColor extension - clean

* SequenceColor extension - avoid repeated allocation for Location

* SequenceColor extension - memoizeLatest, but wrong

* SequenceColor extension - memoizeLatest fixed

* SequenceColor extension - remove unnecessary loci caching

* SequenceColor extension - clean up
2025-09-03 07:29:40 +02:00
David Sehnal
36fd40ee09 VolumeServer: Default to P1 spacegroup (#1640)
* CCP4 parser defaultToP1 option

* volume server: default to P1

* tweaks

* tweak
2025-09-02 17:47:58 +02:00
Alexander Rose
0e968ae59c Fix ColorScale for continuous case without offsets 2025-09-01 16:09:12 -07:00
16 changed files with 112 additions and 33 deletions

View File

@@ -102,6 +102,9 @@ Note that since we don't clearly distinguish between a public and private interf
- Fix transform params not being normalized when used together with param hash version
- Replace `immer` with `mutative`
- Fix renderer transparency check
- VolumeServer & "VolumeCIF": default to P 1 spacegroup
- Fix `ColorScale` for continuous case without offsets (broke in v4.13.0)
- Experimental: support for custom color themes in Sequence Panel
## [v4.18.0] - 2025-06-08
- MolViewSpec extension:

4
package-lock.json generated
View File

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

View File

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

View File

@@ -131,8 +131,8 @@ function getPaths(app) {
async function createBundle(app) {
const { name, kind } = app;
const { prefix, entry, outfile } = getPaths(app);
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
const ctx = await esbuild.context({
entryPoints: [entry],
@@ -161,6 +161,7 @@ async function createBundle(app) {
color: true,
logLevel: 'info',
define: {
'process.env.NODE_ENV': JSON.stringify(NODE_ENV_PRD ? 'production' : 'development'),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
__MOLSTAR_PLUGIN_VERSION__: JSON.stringify(VERSION),
__MOLSTAR_BUILD_TIMESTAMP__: `${TIMESTAMP}`,

View File

@@ -15,7 +15,8 @@ export { MinimizeRmsd };
namespace MinimizeRmsd {
export interface Result {
bTransform: Mat4,
rmsd: number
rmsd: number,
nAlignedElements: number,
}
export interface Positions { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> }
@@ -33,7 +34,7 @@ namespace MinimizeRmsd {
}
export function compute(data: Input, result?: MinimizeRmsd.Result) {
if (typeof result === 'undefined') result = { bTransform: Mat4.zero(), rmsd: 0.0 };
result ??= { bTransform: Mat4.zero(), rmsd: 0.0, nAlignedElements: 0 };
findMinimalRmsdTransformImpl(new RmsdTransformState(data, result));
return result;
}
@@ -170,4 +171,5 @@ function findMinimalRmsdTransformImpl(state: RmsdTransformState): void {
rmsd = rmsd < 0.0 ? 0.0 : Math.sqrt(rmsd / state.a.x.length);
makeTransformMatrix(state);
state.result.rmsd = rmsd;
state.result.nAlignedElements = state.a.x.length;
}

View File

@@ -16,7 +16,7 @@ export function volumeFromDensityServerData(source: DensityServer_Data_Database,
return Task.create<Volume>('Create Volume', async ctx => {
const { volume_data_3d_info: info, volume_data_3d: values } = source;
const cell = SpacegroupCell.create(
info.spacegroup_number.value(0),
info.spacegroup_number.value(0) || 'P 1',
Vec3.ofArray(info.spacegroup_cell_size.value(0)),
Vec3.scale(Vec3.zero(), Vec3.ofArray(info.spacegroup_cell_angles.value(0)), Math.PI / 180)
);

View File

@@ -18,7 +18,7 @@ export function volumeFromSegmentationData(source: Segmentation_Data_Database, p
return Task.create<Volume>('Create Segmentation Volume', async ctx => {
const { volume_data_3d_info: info, segmentation_data_3d: values } = source;
const cell = SpacegroupCell.create(
info.spacegroup_number.value(0),
info.spacegroup_number.value(0) || 'P 1',
Vec3.ofArray(info.spacegroup_cell_size.value(0)),
Vec3.scale(Vec3(), Vec3.ofArray(info.spacegroup_cell_angles.value(0)), Math.PI / 180)
);

View File

@@ -11,6 +11,7 @@ import { StateTransformParameters } from './state/common';
export class PluginUIContext extends PluginContext {
readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
readonly customUIState: Record<string, any> = {};
private initCustomParamEditors() {
if (!this.spec.customParamEditors) return;

View File

@@ -47,7 +47,7 @@ export class ChainSequenceWrapper extends SequenceWrapper<StructureUnit> {
return Interval.Empty;
}
getLoci(seqIdx: number) {
override getLoci(seqIdx: number) {
return this.loci;
}

View File

@@ -50,7 +50,7 @@ export class ElementSequenceWrapper extends SequenceWrapper<StructureUnit> {
return Interval.Empty;
}
getLoci(seqIdx: number) {
override getLoci(seqIdx: number) {
const { units } = this.data;
const lociElements: StructureElement.Loci['elements'][0][] = [];
let offset = 0;

View File

@@ -53,7 +53,7 @@ export class HeteroSequenceWrapper extends SequenceWrapper<StructureUnit> {
return Interval.Empty;
}
getLoci(seqIdx: number) {
override getLoci(seqIdx: number) {
const elements: StructureElement.Loci['elements'][0][] = [];
const rI = this.residueIndices.get(seqIdx);
if (rI !== undefined) {

View File

@@ -67,7 +67,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> {
return Interval.Empty;
}
getLoci(seqIdx: number) {
override getLoci(seqIdx: number) {
const query = createResidueQuery(this.data.units[0].chainGroupId, this.data.units[0].conformation.operator.name, this.seqId(seqIdx));
return StructureSelection.toLociWithSourceUnits(StructureQuery.run(query, this.data.structure));
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2024 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>
@@ -7,16 +7,21 @@
*/
import * as React from 'react';
import { Subject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { OrderedSet } from '../../mol-data/int';
import { ColorTypeLocation } from '../../mol-geo/geometry/color-data';
import { EveryLoci } from '../../mol-model/loci';
import { StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { Representation } from '../../mol-repr/representation';
import { Task } from '../../mol-task';
import { ColorTheme, LocationColor } from '../../mol-theme/color';
import { Color } from '../../mol-util/color';
import { ButtonsType, getButton, getButtons, getModifiers, ModifiersKeys } from '../../mol-util/input/input-observer';
import { MarkerAction } from '../../mol-util/marker-action';
import { memoizeLatest } from '../../mol-util/memoize';
import { PluginUIComponent } from '../base';
import { SequenceWrapper } from './wrapper';
@@ -36,12 +41,19 @@ const DefaultMarkerColors = {
focused: '',
};
type ColorThemeProvider = ColorTheme.Provider<any, string, ColorTypeLocation> | undefined
// TODO: this is somewhat inefficient and should be done using a canvas.
export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
protected parentDiv = React.createRef<HTMLDivElement>();
protected lastMouseOverSeqIdx = -1;
protected highlightQueue = new Subject<{ seqIdx: number, buttons: number, button: number, modifiers: ModifiersKeys }>();
protected markerColors = { ...DefaultMarkerColors };
/** @experimental */
private customColorThemeWrapper: ColorThemeWrapper | undefined = undefined;
/** @experimental Custom function that assigns color to residues in the Sequence component (unless highlighted or selected) */
private customColorFunction: ((idx: number) => string) | undefined = undefined;
protected lociHighlightProvider = (loci: Representation.Loci, action: MarkerAction) => {
const changed = this.props.sequenceWrapper.markResidue(loci.loci, action);
@@ -81,6 +93,14 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
this.updateColors();
this.updateMarker();
});
const experimentalSequenceColorTheme: BehaviorSubject<ColorThemeProvider> | undefined = this.plugin.customUIState.experimentalSequenceColorTheme;
if (experimentalSequenceColorTheme) {
this.subscribe(experimentalSequenceColorTheme, theme => {
if (!theme && !this.customColorThemeWrapper) return;
this.customColorThemeWrapper = ColorThemeWrapper(this.plugin, theme, () => this.forceUpdate());
this.forceUpdate();
});
}
}
updateColors() {
@@ -199,9 +219,12 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
protected getBackgroundColor(seqIdx: number) {
const seqWrapper = this.props.sequenceWrapper;
if (seqWrapper.isHighlighted(seqIdx)) return this.markerColors.highlighted;
if (seqWrapper.isSelected(seqIdx)) return this.markerColors.selected;
if (seqWrapper.isFocused(seqIdx)) return this.markerColors.focused;
if (seqWrapper.isHighlighted(seqIdx) && this.markerColors.highlighted) return this.markerColors.highlighted;
if (seqWrapper.isSelected(seqIdx) && this.markerColors.selected) return this.markerColors.selected;
if (seqWrapper.isFocused(seqIdx) && this.markerColors.focused) return this.markerColors.focused;
if (this.customColorFunction) {
return this.customColorFunction(seqIdx);
}
return '';
}
@@ -322,9 +345,12 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
render() {
const sw = this.props.sequenceWrapper;
const elems: JSX.Element[] = [];
if (this.customColorThemeWrapper) {
this.customColorFunction = this.customColorThemeWrapper.getColorFunction(sw);
}
const hasNumbers = !this.props.hideSequenceNumbers, period = this.sequenceNumberPeriod;
for (let i = 0, il = sw.length; i < il; ++i) {
const label = sw.residueLabel(i);
@@ -355,3 +381,57 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
</div>;
}
}
type ColorThemeWrapper = ReturnType<typeof ColorThemeWrapper>
function ColorThemeWrapper(plugin: PluginContext, theme: ColorThemeProvider, forceUpdate: () => void) {
const tmpLocation = StructureElement.Location.create();
function computeColor(sequenceWrapper: SequenceWrapper.Any, idx: number, locationColor: LocationColor) {
const loci = sequenceWrapper.getLoci(idx);
if (!loci || StructureElement.Loci.isEmpty(loci)) return '';
StructureElement.Loci.getFirstLocation(loci, tmpLocation);
const color = locationColor(tmpLocation, false);
if (color < 0) return ''; // Color(-1) is used as special value NoColor
return Color.toHexStyle(color);
}
const getColorFunction = memoizeLatest((sequenceWrapper: SequenceWrapper.Any) => {
if (!theme) return undefined;
const structure = sequenceWrapper.getLoci(0)?.structure;
if (!structure) return undefined;
let themeColor: LocationColor | undefined = undefined;
if (theme.ensureCustomProperties) {
// The following task runs asynchronously
plugin.runTask(Task.create('Attach custom properties for coloring theme', async runtime => {
try {
await theme.ensureCustomProperties?.attach({ assetManager: plugin.managers.asset, runtime }, { structure });
} catch (err) {
console.warn(`Failed to attach custom properties needed for coloring theme ${theme.name}:`, err);
} finally {
themeColor = theme.factory({ structure }, theme.defaultValues).color;
forceUpdate();
}
}));
} else {
themeColor = theme.factory({ structure }, theme.defaultValues).color;
}
const cache: { [idx: number]: string } = {};
return (idx: number) => {
if (themeColor) { // custom properties ready
return cache[idx] ??= computeColor(sequenceWrapper, idx, themeColor);
} else { // custom properties not ready
return '';
}
};
});
return {
themeName: theme?.name,
getColorFunction,
};
}

View File

@@ -80,7 +80,7 @@ export namespace ColorScale {
}
} else {
switch (type) {
case 'continuous': color = (value: number) => valueToColor(value, colors, min, max, diff); break;
case 'continuous': color = (value: number) => valueToColor(value, colors, min, diff); break;
case 'discrete': color = (value: number) => valueToDiscreteColor(value, colors, min, max, diff); break;
}
}
@@ -113,8 +113,8 @@ export namespace ColorScale {
return Color.interpolate(src[i - 1], src[i], t1);
}
function valueToColor(value: number, colors: ColorListEntry[], min: number, max: number, diff: number) {
const t = Math.min(colors.length - 1, Math.max(0, ((value - min) / diff) * colors.length - 1));
function valueToColor(value: number, colors: ColorListEntry[], min: number, diff: number) {
const t = Math.min(colors.length - 1, Math.max(0, ((value - min) / diff) * (colors.length - 1)));
const tf = Math.floor(t);
const c1 = colors[tf] as Color;
const c2 = colors[Math.ceil(t)] as Color;

View File

@@ -4,20 +4,12 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { create, rawReturn } from 'mutative';
let currentRecipe: any = undefined;
function recipeWrapper(draft: any) {
const r = currentRecipe(draft);
if (r !== undefined && r !== draft) return rawReturn(r);
return r;
}
import { create } from 'mutative/dist/index.js';
/** Apply changes to an immutable-like object */
export function produce<T>(base: T, recipe: (draft: T) => T | void): T {
currentRecipe = recipe;
if (typeof base === 'object' && !('prototype' in (base as any))) {
return create({ ...base }, recipeWrapper) as T;
return create({ ...base }, recipe as any) as T;
}
return create(base, recipeWrapper) as T;
return create(base, recipe as any) as T;
}

View File

@@ -92,7 +92,7 @@ async function createDataContext(file: FileHandle): Promise<Data.DataContext> {
return {
file,
header,
spacegroup: SpacegroupCell.create(header.spacegroup.number, Vec3.ofArray(header.spacegroup.size), Vec3.scale(Vec3.zero(), Vec3.ofArray(header.spacegroup.angles), Math.PI / 180)),
spacegroup: SpacegroupCell.create(header.spacegroup.number || 'P 1', Vec3.ofArray(header.spacegroup.size), Vec3.scale(Vec3.zero(), Vec3.ofArray(header.spacegroup.angles), Math.PI / 180)),
dataBox: { a: origin, b: Coords.add(origin, dimensions) },
sampling: header.sampling.map((s, i) => createSampling(header, i, dataOffset))
};