mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 15:14:22 +08:00
Compare commits
5 Commits
v5.0.0-dev
...
v5.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c04580377b | ||
|
|
a492b38368 | ||
|
|
518f21531e | ||
|
|
36fd40ee09 | ||
|
|
0e968ae59c |
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -47,7 +47,7 @@ export class ChainSequenceWrapper extends SequenceWrapper<StructureUnit> {
|
||||
return Interval.Empty;
|
||||
}
|
||||
|
||||
getLoci(seqIdx: number) {
|
||||
override getLoci(seqIdx: number) {
|
||||
return this.loci;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user