mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 14:04:36 +08:00
Compare commits
19 Commits
safari-deb
...
v3.16.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c8d2cfa21 | ||
|
|
d67c0eb757 | ||
|
|
7094f8f265 | ||
|
|
19a9ed3e19 | ||
|
|
0dac1b93ae | ||
|
|
e824863de1 | ||
|
|
9ff8becd62 | ||
|
|
fcaa1bcfa8 | ||
|
|
1b904ee2c9 | ||
|
|
d9b2b99c86 | ||
|
|
bdf23a7c4e | ||
|
|
f6ed650ef6 | ||
|
|
df9ce6add9 | ||
|
|
28ee47d501 | ||
|
|
df2da479ad | ||
|
|
46eb9d8baf | ||
|
|
b6be871a21 | ||
|
|
ce2367fc0a | ||
|
|
ef176efed8 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -6,6 +6,18 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v3.16.0] - 2022-08-25
|
||||
|
||||
- Support ``globalColorParams`` and ``globalSymmetryParams`` in common representation params
|
||||
- Support ``label`` parameter in ``Viewer.loadStructureFromUrl``
|
||||
- Fix ``ViewportHelpContent`` Mouse Controls section
|
||||
|
||||
## [v3.15.0] - 2022-08-23
|
||||
|
||||
- Fix wboit in Safari >=15 (add missing depth renderbuffer to wboit pass)
|
||||
- Add 'Around Camera' option to Volume streaming
|
||||
- Avoid queuing more than one update in Volume streaming
|
||||
|
||||
## [v3.14.0] - 2022-08-20
|
||||
|
||||
- Expose inter-bonds compute params in structure
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "3.14.0",
|
||||
"version": "3.16.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "molstar",
|
||||
"version": "3.14.0",
|
||||
"version": "3.16.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "3.14.0",
|
||||
"version": "3.16.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -89,7 +89,8 @@
|
||||
"Ludovic Autin <autin@scripps.edu>",
|
||||
"Michal Malý <michal.maly@ibt.cas.cz>",
|
||||
"Jiří Černý <jiri.cerny@ibt.cas.cz>",
|
||||
"Panagiotis Tourlas <panagiot_tourlov@hotmail.com>"
|
||||
"Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
|
||||
"Adam Midlik <midlik@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -199,7 +199,7 @@ export class Viewer {
|
||||
return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
|
||||
}
|
||||
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions & { label?: string }) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
@@ -208,6 +208,7 @@ export class Viewer {
|
||||
url: Asset.Url(url),
|
||||
format: format as any,
|
||||
isBinary,
|
||||
label: options?.label,
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
viewer.loadPdb('7bv2');
|
||||
viewer.loadEmdb('EMD-30210', { detail: 6 });
|
||||
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
|
||||
// viewer.loadStructureFromUrl('my url', 'pdb', false, {
|
||||
// representationParams: {
|
||||
// theme: {
|
||||
// globalName: 'uniform',
|
||||
// globalColorParams: { value: 0xff0000 }
|
||||
// }
|
||||
// },
|
||||
// label: 'my structure'
|
||||
// });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -18,6 +18,8 @@ import { evaluateWboit_frag } from '../../mol-gl/shader/evaluate-wboit.frag';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
import { isWebGL2 } from '../../mol-gl/webgl/compat';
|
||||
import { Renderbuffer } from '../../mol-gl/webgl/renderbuffer';
|
||||
|
||||
const EvaluateWboitSchema = {
|
||||
...QuadSchema,
|
||||
@@ -50,6 +52,7 @@ export class WboitPass {
|
||||
private readonly framebuffer: Framebuffer;
|
||||
private readonly textureA: Texture;
|
||||
private readonly textureB: Texture;
|
||||
private readonly depthRenderbuffer: Renderbuffer;
|
||||
|
||||
private _supported = false;
|
||||
get supported() {
|
||||
@@ -87,6 +90,7 @@ export class WboitPass {
|
||||
if (width !== w || height !== h) {
|
||||
this.textureA.define(width, height);
|
||||
this.textureB.define(width, height);
|
||||
this.depthRenderbuffer.setSize(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
}
|
||||
@@ -106,6 +110,8 @@ export class WboitPass {
|
||||
|
||||
this.textureA.attachFramebuffer(this.framebuffer, 'color0');
|
||||
this.textureB.attachFramebuffer(this.framebuffer, 'color1');
|
||||
|
||||
this.depthRenderbuffer.attachFramebuffer(this.framebuffer);
|
||||
}
|
||||
|
||||
static isSupported(webgl: WebGLContext) {
|
||||
@@ -128,7 +134,7 @@ export class WboitPass {
|
||||
constructor(private webgl: WebGLContext, width: number, height: number) {
|
||||
if (!WboitPass.isSupported(webgl)) return;
|
||||
|
||||
const { resources } = webgl;
|
||||
const { resources, gl } = webgl;
|
||||
|
||||
this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.textureA.define(width, height);
|
||||
@@ -136,6 +142,10 @@ export class WboitPass {
|
||||
this.textureB = resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.textureB.define(width, height);
|
||||
|
||||
this.depthRenderbuffer = isWebGL2(gl)
|
||||
? resources.renderbuffer('depth32f', 'depth', width, height)
|
||||
: resources.renderbuffer('depth16', 'depth', width, height);
|
||||
|
||||
this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
|
||||
this.framebuffer = resources.framebuffer();
|
||||
|
||||
|
||||
@@ -109,14 +109,14 @@ void main() {
|
||||
|
||||
vec3 vViewPosition = vModelPosition + intersection.x * rayDir;
|
||||
vViewPosition = (uView * vec4(vViewPosition, 1.0)).xyz;
|
||||
gl_FragDepthEXT = calcDepth(vViewPosition);
|
||||
float fragmentDepth = calcDepth(vViewPosition);
|
||||
|
||||
if (fragmentDepth < 0.0) discard;
|
||||
if (fragmentDepth > 1.0) discard;
|
||||
|
||||
gl_FragDepthEXT = fragmentDepth;
|
||||
|
||||
vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
|
||||
|
||||
if (gl_FragDepthEXT < 0.0) discard;
|
||||
if (gl_FragDepthEXT > 1.0) discard;
|
||||
|
||||
float fragmentDepth = gl_FragDepthEXT;
|
||||
#include assign_material_color
|
||||
|
||||
#if defined(dRenderVariant_pick)
|
||||
|
||||
@@ -70,17 +70,17 @@ void main(void){
|
||||
}
|
||||
|
||||
vec3 vViewPosition = cameraPos;
|
||||
gl_FragDepthEXT = calcDepth(vViewPosition);
|
||||
if (!flag && gl_FragDepthEXT >= 0.0) {
|
||||
gl_FragDepthEXT = 0.0 + (0.0000001 / vRadius);
|
||||
float fragmentDepth = calcDepth(vViewPosition);
|
||||
if (!flag && fragmentDepth >= 0.0) {
|
||||
fragmentDepth = 0.0 + (0.0000001 / vRadius);
|
||||
}
|
||||
|
||||
if (fragmentDepth < 0.0) discard;
|
||||
if (fragmentDepth > 1.0) discard;
|
||||
|
||||
gl_FragDepthEXT = fragmentDepth;
|
||||
|
||||
vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
|
||||
|
||||
if (gl_FragDepthEXT < 0.0) discard;
|
||||
if (gl_FragDepthEXT > 1.0) discard;
|
||||
|
||||
float fragmentDepth = gl_FragDepthEXT;
|
||||
#include assign_material_color
|
||||
|
||||
#if defined(dRenderVariant_pick)
|
||||
|
||||
@@ -35,7 +35,7 @@ function Vec3() {
|
||||
|
||||
namespace Vec3 {
|
||||
export function zero(): Vec3 {
|
||||
const out = [0.1, 0.0, 0.0];
|
||||
const out = [0.1, 0.0, 0.0]; // ensure backing array of type double
|
||||
out[0] = 0;
|
||||
return out as any;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ const DownloadStructure = StateAction.build({
|
||||
url: PD.Url(''),
|
||||
format: PD.Select<BuiltInTrajectoryFormat>('mmcif', PD.arrayToOptions(BuiltInTrajectoryFormats.map(f => f[0]), f => f)),
|
||||
isBinary: PD.Boolean(false),
|
||||
label: PD.Optional(PD.Text('')),
|
||||
options
|
||||
}, { isFlat: true, label: 'URL' })
|
||||
})
|
||||
@@ -104,7 +105,7 @@ const DownloadStructure = StateAction.build({
|
||||
|
||||
switch (src.name) {
|
||||
case 'url':
|
||||
downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary }];
|
||||
downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary, label: src.params.label || undefined }];
|
||||
format = src.params.format;
|
||||
break;
|
||||
case 'pdb':
|
||||
|
||||
@@ -41,8 +41,10 @@ export namespace StructureRepresentationPresetProvider {
|
||||
quality: PD.Optional(PD.Select<VisualQuality>('auto', VisualQualityOptions)),
|
||||
theme: PD.Optional(PD.Group({
|
||||
globalName: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
|
||||
globalColorParams: PD.Optional(PD.Value<any>({}, { isHidden: true })),
|
||||
carbonColor: PD.Optional(PD.Select('chain-id', PD.arrayToOptions(['chain-id', 'operator-name', 'element-symbol'] as const))),
|
||||
symmetryColor: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
|
||||
symmetryColorParams: PD.Optional(PD.Value<any>({}, { isHidden: true })),
|
||||
focus: PD.Optional(PD.Group({
|
||||
name: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
|
||||
params: PD.Optional(PD.Value<ColorTheme.BuiltInParams<ColorTheme.BuiltIn>>({} as any))
|
||||
@@ -76,13 +78,15 @@ export namespace StructureRepresentationPresetProvider {
|
||||
if (params.ignoreLight !== void 0) typeParams.ignoreLight = !!params.ignoreLight;
|
||||
const color: ColorTheme.BuiltIn | undefined = params.theme?.globalName ? params.theme?.globalName : void 0;
|
||||
const ballAndStickColor: ColorTheme.BuiltInParams<'element-symbol'> = params.theme?.carbonColor !== undefined
|
||||
? { carbonColor: getCarbonColorParams(params.theme?.carbonColor) }
|
||||
: { };
|
||||
? { carbonColor: getCarbonColorParams(params.theme?.carbonColor), ...params.theme?.globalColorParams }
|
||||
: { ...params.theme?.globalColorParams };
|
||||
const symmetryColor: ColorTheme.BuiltIn | undefined = structure && params.theme?.symmetryColor
|
||||
? isSymmetry(structure) ? params.theme?.symmetryColor : color
|
||||
: color;
|
||||
const symmetryColorParams = params.theme?.symmetryColorParams ? { ...params.theme?.globalColorParams, ...params.theme?.symmetryColorParams } : { ...params.theme?.globalColorParams };
|
||||
const globalColorParams = params.theme?.globalColorParams ? { ...params.theme?.globalColorParams } : undefined;
|
||||
|
||||
return { update, builder, color, symmetryColor, typeParams, ballAndStickColor };
|
||||
return { update, builder, color, symmetryColor, symmetryColorParams, globalColorParams, typeParams, ballAndStickColor };
|
||||
}
|
||||
|
||||
export function updateFocusRepr<T extends ColorTheme.BuiltIn>(plugin: PluginContext, structure: Structure, themeName: T | undefined, themeParams: ColorTheme.BuiltInParams<T> | undefined) {
|
||||
@@ -177,18 +181,18 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
|
||||
const waterType = (components.water?.obj?.data?.elementCount || 0) > 50_000 ? 'line' : 'ball-and-stick';
|
||||
const lipidType = (components.lipid?.obj?.data?.elementCount || 0) > 20_000 ? 'line' : 'ball-and-stick';
|
||||
|
||||
const { update, builder, typeParams, color, symmetryColor, ballAndStickColor } = reprBuilder(plugin, params, structure);
|
||||
const { update, builder, typeParams, color, symmetryColor, symmetryColorParams, globalColorParams, ballAndStickColor } = reprBuilder(plugin, params, structure);
|
||||
|
||||
const representations = {
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' }),
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'ligand' }),
|
||||
nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'non-standard' }),
|
||||
branchedBallAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.3 }, color, colorParams: ballAndStickColor }, { tag: 'branched-ball-and-stick' }),
|
||||
branchedSnfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color }, { tag: 'branched-snfg-3d' }),
|
||||
water: builder.buildRepresentation(update, components.water, { type: waterType, typeParams: { ...typeParams, alpha: 0.6, visuals: waterType === 'line' ? ['intra-bond', 'element-point'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'water' }),
|
||||
ion: builder.buildRepresentation(update, components.ion, { type: 'ball-and-stick', typeParams, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ion' }),
|
||||
lipid: builder.buildRepresentation(update, components.lipid, { type: lipidType, typeParams: { ...typeParams, alpha: 0.6, visuals: lipidType === 'line' ? ['intra-bond'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'lipid' }),
|
||||
coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'chain-id' }, { tag: 'coarse' })
|
||||
branchedSnfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color, colorParams: globalColorParams }, { tag: 'branched-snfg-3d' }),
|
||||
water: builder.buildRepresentation(update, components.water, { type: waterType, typeParams: { ...typeParams, alpha: 0.6, visuals: waterType === 'line' ? ['intra-bond', 'element-point'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'water' }),
|
||||
ion: builder.buildRepresentation(update, components.ion, { type: 'ball-and-stick', typeParams, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'ion' }),
|
||||
lipid: builder.buildRepresentation(update, components.lipid, { type: lipidType, typeParams: { ...typeParams, alpha: 0.6, visuals: lipidType === 'line' ? ['intra-bond'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'lipid' }),
|
||||
coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'chain-id', colorParams: globalColorParams }, { tag: 'coarse' })
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: false });
|
||||
@@ -223,11 +227,11 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
|
||||
smoothness: structure.isCoarseGrained ? 1.0 : 1.5,
|
||||
};
|
||||
|
||||
const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
|
||||
const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
|
||||
|
||||
const representations = {
|
||||
protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'protein' }),
|
||||
nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'nucleic' })
|
||||
protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'protein' }),
|
||||
nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'nucleic' })
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
@@ -275,11 +279,11 @@ const coarseSurface = StructureRepresentationPresetProvider({
|
||||
});
|
||||
}
|
||||
|
||||
const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
|
||||
const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
|
||||
|
||||
const representations = {
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'polymer' }),
|
||||
lipid: builder.buildRepresentation(update, components.lipid, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'lipid' })
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' }),
|
||||
lipid: builder.buildRepresentation(update, components.lipid, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'lipid' })
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
@@ -309,10 +313,10 @@ const polymerCartoon = StructureRepresentationPresetProvider({
|
||||
sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2
|
||||
};
|
||||
|
||||
const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
|
||||
const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
|
||||
|
||||
const representations = {
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' })
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' })
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
@@ -367,9 +371,9 @@ const atomicDetail = StructureRepresentationPresetProvider({
|
||||
});
|
||||
}
|
||||
|
||||
const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params, structure);
|
||||
const { update, builder, typeParams, color, ballAndStickColor, globalColorParams } = reprBuilder(plugin, params, structure);
|
||||
const colorParams = lowResidueElementRatio && !bondsGiven
|
||||
? { carbonColor: { name: 'element-symbol', params: {} } }
|
||||
? { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams }
|
||||
: ballAndStickColor;
|
||||
|
||||
const representations = {
|
||||
@@ -377,7 +381,7 @@ const atomicDetail = StructureRepresentationPresetProvider({
|
||||
};
|
||||
if (showCarbohydrateSymbol) {
|
||||
Object.assign(representations, {
|
||||
snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams: { ...typeParams, alpha: 0.4, visuals: ['carbohydrate-symbol'] }, color }, { tag: 'snfg-3d' }),
|
||||
snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams: { ...typeParams, alpha: 0.4, visuals: ['carbohydrate-symbol'] }, color, colorParams: globalColorParams }, { tag: 'snfg-3d' }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginUIComponent } from '../base';
|
||||
@@ -199,6 +200,9 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
|
||||
const viewParams = { ...oldView };
|
||||
if (value.name === 'selection-box') {
|
||||
viewParams.radius = value.params.radius;
|
||||
} else if (value.name === 'camera-target') {
|
||||
viewParams.radius = value.params.radius;
|
||||
viewParams.dynamicDetailLevel = value.params.dynamicDetailLevel;
|
||||
} else if (value.name === 'box') {
|
||||
viewParams.bottomLeft = value.params.bottomLeft;
|
||||
viewParams.topRight = value.params.topRight;
|
||||
@@ -240,13 +244,23 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
|
||||
const pivot = isEM ? 'em' : '2fo-fc';
|
||||
|
||||
const params = this.props.params as VolumeStreaming.Params;
|
||||
const entry = ((this.props.info.params as VolumeStreaming.ParamDefinition)
|
||||
.entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>);
|
||||
const entry = (this.props.info.params as VolumeStreaming.ParamDefinition)
|
||||
.entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>;
|
||||
const detailLevel = entry.params.detailLevel;
|
||||
const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
|
||||
const dynamicDetailLevel = {
|
||||
...detailLevel,
|
||||
label: 'Dynamic Detail',
|
||||
defaultValue: (entry.params.view as any).map('camera-target').params.dynamicDetailLevel.defaultValue,
|
||||
};
|
||||
const selectionDetailLevel = {
|
||||
...detailLevel,
|
||||
label: 'Selection Detail',
|
||||
defaultValue: (entry.params.view as any).map('auto').params.selectionDetailLevel.defaultValue,
|
||||
};
|
||||
|
||||
const sampling = b.info.header.sampling[0];
|
||||
|
||||
const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
|
||||
const isRelativeParam = PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' });
|
||||
|
||||
const isUnbounded = !!(params.entry.params.view.params as any).isUnbounded;
|
||||
@@ -274,6 +288,13 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
|
||||
isRelative: isRelativeParam,
|
||||
isUnbounded: isUnboundedParam,
|
||||
}, { description: 'Box around focused element.' }),
|
||||
'camera-target': PD.Group({
|
||||
radius: PD.Numeric(0.5, { min: 0, max: 1, step: 0.05 }, { description: 'Radius within which the volume is shown (relative to the field of view).' }),
|
||||
detailLevel: { ...detailLevel, isHidden: true },
|
||||
dynamicDetailLevel: dynamicDetailLevel,
|
||||
isRelative: isRelativeParam,
|
||||
isUnbounded: isUnboundedParam,
|
||||
}, { description: 'Box around camera target.' }),
|
||||
'cell': PD.Group({
|
||||
detailLevel,
|
||||
isRelative: isRelativeParam,
|
||||
@@ -282,12 +303,11 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
|
||||
'auto': PD.Group({
|
||||
radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
|
||||
detailLevel,
|
||||
selectionDetailLevel: { ...detailLevel, label: 'Selection Detail' },
|
||||
selectionDetailLevel: selectionDetailLevel,
|
||||
isRelative: isRelativeParam,
|
||||
isUnbounded: isUnboundedParam,
|
||||
}, { description: 'Box around focused element.' }),
|
||||
// 'auto': PD.Group({ }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
|
||||
}, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Whole Structure" shows the volume for the whole structure.' })
|
||||
}, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Around Camera" shows the volume around the point the camera is targeting. "Whole Structure" shows the volume for the whole structure.' })
|
||||
};
|
||||
const options = {
|
||||
entry: params.entry.name,
|
||||
@@ -299,6 +319,7 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
|
||||
bottomLeft: (params.entry.params.view.params as any).bottomLeft,
|
||||
topRight: (params.entry.params.view.params as any).topRight,
|
||||
selectionDetailLevel: (params.entry.params.view.params as any).selectionDetailLevel,
|
||||
dynamicDetailLevel: (params.entry.params.view.params as any).dynamicDetailLevel,
|
||||
isRelative,
|
||||
isUnbounded
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
import * as React from 'react';
|
||||
import { Binding } from '../../mol-util/binding';
|
||||
import { PluginUIComponent } from '../base';
|
||||
import { StateTransformer, StateSelection } from '../../mol-state';
|
||||
import { StateTransformer, StateSelection, State } from '../../mol-state';
|
||||
import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
|
||||
import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
|
||||
import { Icon, ArrowDropDownSvg, ArrowRightSvg, CameraSvg } from '../controls/icons';
|
||||
import { Button } from '../controls/common';
|
||||
import { memoizeLatest } from '../../mol-util/memoize';
|
||||
|
||||
function getBindingsList(bindings: { [k: string]: Binding }) {
|
||||
return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]);
|
||||
return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]).filter(b => Binding.isBinding(b[1]));
|
||||
}
|
||||
|
||||
export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]: Binding } }> {
|
||||
@@ -77,19 +78,30 @@ export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolea
|
||||
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
|
||||
}
|
||||
|
||||
render() {
|
||||
const interactionBindings: { [k: string]: Binding } = {};
|
||||
this.plugin.spec.behaviors.forEach(b => {
|
||||
const { bindings } = b.defaultParams;
|
||||
if (bindings) Object.assign(interactionBindings, bindings);
|
||||
getInteractionBindings = memoizeLatest((cells: State.Cells) => {
|
||||
let interactionBindings: { [k: string]: Binding } | undefined = void 0;
|
||||
|
||||
cells.forEach(c => {
|
||||
const params = c.params?.values;
|
||||
if (params?.bindings && Object.keys(params.bindings).length > 0) {
|
||||
if (!interactionBindings) interactionBindings = { };
|
||||
Object.assign(interactionBindings, params.bindings);
|
||||
}
|
||||
});
|
||||
|
||||
return interactionBindings;
|
||||
});
|
||||
|
||||
render() {
|
||||
const interactionBindings = this.getInteractionBindings(this.plugin.state.behaviors.cells);
|
||||
|
||||
return <>
|
||||
{(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
|
||||
<BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
|
||||
</HelpGroup>}
|
||||
<HelpGroup key='interactions' header='Mouse Controls'>
|
||||
{!!interactionBindings && <HelpGroup key='interactions' header='Mouse Controls'>
|
||||
<BindingsHelp bindings={interactionBindings} />
|
||||
</HelpGroup>
|
||||
</HelpGroup>}
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateTransform, PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
@@ -144,8 +145,18 @@ namespace PluginBehavior {
|
||||
protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
|
||||
this.subs.push(cmd.subscribe(this.plugin, action));
|
||||
}
|
||||
protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void) {
|
||||
this.subs.push(o.subscribe(action));
|
||||
protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void): PluginCommand.Subscription {
|
||||
const sub = o.subscribe(action);
|
||||
this.subs.push(sub);
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
const idx = this.subs.indexOf(sub);
|
||||
if (idx >= 0) {
|
||||
this.subs.splice(idx, 1);
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
dispose(): void {
|
||||
for (const s of this.subs) s.unsubscribe();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2022 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>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
@@ -24,6 +25,10 @@ import { PluginContext } from '../../../context';
|
||||
import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
|
||||
import { Asset } from '../../../../mol-util/assets';
|
||||
import { GlobalModelTransformInfo } from '../../../../mol-model/structure/model/properties/global-transform';
|
||||
import { distinctUntilChanged, filter, map, Observable, throttleTime } from 'rxjs';
|
||||
import { Camera } from '../../../../mol-canvas3d/camera';
|
||||
import { PluginCommand } from '../../../command';
|
||||
import { SingleAsyncQueue } from '../../../../mol-util/single-async-queue';
|
||||
|
||||
export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
|
||||
|
||||
@@ -53,7 +58,7 @@ export namespace VolumeStreaming {
|
||||
valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
|
||||
};
|
||||
|
||||
export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = { }) {
|
||||
export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = {}) {
|
||||
const { data, defaultView, channelParams } = options;
|
||||
const map = new Map<string, VolumeServerInfo.EntryData>();
|
||||
if (data) data.entries.forEach(d => map.set(d.dataId, d));
|
||||
@@ -68,7 +73,7 @@ export namespace VolumeStreaming {
|
||||
export type EntryParams = PD.Values<EntryParamDefinition>
|
||||
|
||||
export function createEntryParams(options: { entryData?: VolumeServerInfo.EntryData, defaultView?: ViewTypes, structure?: Structure, channelParams?: DefaultChannelParams }) {
|
||||
const { entryData, defaultView, structure, channelParams = { } } = options;
|
||||
const { entryData, defaultView, structure, channelParams = {} } = options;
|
||||
|
||||
// fake the info
|
||||
const info = entryData || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: Volume.IsoValue.relative(0) };
|
||||
@@ -86,19 +91,24 @@ export namespace VolumeStreaming {
|
||||
bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
|
||||
topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
|
||||
}, { description: 'Box around focused element.', isFlat: true }),
|
||||
'camera-target': PD.Group({
|
||||
radius: PD.Numeric(0.5, { min: 0, max: 1, step: 0.05 }, { description: 'Radius within which the volume is shown (relative to the field of view).' }),
|
||||
// Minimal detail level for the inside of the zoomed region (real detail can be higher, depending on the region size)
|
||||
dynamicDetailLevel: createDetailParams(info.header.availablePrecisions, 0, { label: 'Dynamic Detail' }),
|
||||
bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
|
||||
topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
|
||||
}, { description: 'Box around camera target.', isFlat: true }),
|
||||
'cell': PD.Group<{}>({}),
|
||||
// Show selection-box if available and cell otherwise.
|
||||
'auto': PD.Group({
|
||||
radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
|
||||
selectionDetailLevel: PD.Select<number>(Math.min(6, info.header.availablePrecisions.length - 1),
|
||||
info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { label: 'Selection Detail', description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
|
||||
selectionDetailLevel: createDetailParams(info.header.availablePrecisions, 6, { label: 'Selection Detail' }),
|
||||
isSelection: PD.Boolean(false, { isHidden: true }),
|
||||
bottomLeft: PD.Vec3(box.min, {}, { isHidden: true }),
|
||||
topRight: PD.Vec3(box.max, {}, { isHidden: true }),
|
||||
}, { description: 'Box around focused element.', isFlat: true })
|
||||
}, { options: ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Interaction" shows the volume around the focused element/atom. "Whole Structure" shows the volume for the whole structure.' }),
|
||||
detailLevel: PD.Select<number>(Math.min(3, info.header.availablePrecisions.length - 1),
|
||||
info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
|
||||
detailLevel: createDetailParams(info.header.availablePrecisions, 3),
|
||||
channels: info.kind === 'em'
|
||||
? PD.Group({
|
||||
'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || Volume.IsoValue.relative(1), info.header.sampling[0].valuesInfo[0], channelParams['em'])
|
||||
@@ -111,13 +121,40 @@ export namespace VolumeStreaming {
|
||||
};
|
||||
}
|
||||
|
||||
export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
|
||||
function createDetailParams(availablePrecisions: VolumeServerHeader.DetailLevel[], preferredPrecision: number, info?: PD.Info) {
|
||||
return PD.Select<number>(Math.min(preferredPrecision, availablePrecisions.length - 1),
|
||||
availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]),
|
||||
{
|
||||
description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 1 (0.52M voxels) to 7 (25.17M voxels).',
|
||||
...info
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export type ViewTypes = 'off' | 'box' | 'selection-box' | 'cell' | 'auto'
|
||||
export function copyParams(origParams: Params): Params {
|
||||
return {
|
||||
entry: {
|
||||
name: origParams.entry.name,
|
||||
params: {
|
||||
detailLevel: origParams.entry.params.detailLevel,
|
||||
channels: origParams.entry.params.channels,
|
||||
view: {
|
||||
name: origParams.entry.params.view.name,
|
||||
params: { ...origParams.entry.params.view.params } as any,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['camera-target', 'Around Camera'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
|
||||
|
||||
export type ViewTypes = 'off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'
|
||||
|
||||
export type ParamDefinition = ReturnType<typeof createParams>
|
||||
export type Params = PD.Values<ParamDefinition>
|
||||
|
||||
|
||||
type ChannelsInfo = { [name in ChannelType]?: { isoValue: Volume.IsoValue, color: Color, wireframe: boolean, opacity: number } }
|
||||
type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: Volume }
|
||||
|
||||
@@ -140,6 +177,14 @@ export namespace VolumeStreaming {
|
||||
private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
|
||||
private ref: string = '';
|
||||
public infoMap: Map<string, VolumeServerInfo.EntryData>;
|
||||
private updateQueue: SingleAsyncQueue;
|
||||
private cameraTargetObservable = this.plugin.canvas3d!.didDraw!.pipe(
|
||||
throttleTime(500, undefined, { 'leading': true, 'trailing': true }),
|
||||
map(() => this.plugin.canvas3d?.camera.getSnapshot()),
|
||||
distinctUntilChanged((a, b) => this.isCameraTargetSame(a, b)),
|
||||
filter(a => a !== undefined),
|
||||
) as Observable<Camera.Snapshot>;
|
||||
private cameraTargetSubscription?: PluginCommand.Subscription = undefined;
|
||||
|
||||
channels: Channels = {};
|
||||
|
||||
@@ -163,6 +208,9 @@ export namespace VolumeStreaming {
|
||||
if (this.params.entry.params.view.name === 'auto' && this.params.entry.params.view.params.isSelection) {
|
||||
detail = this.params.entry.params.view.params.selectionDetailLevel;
|
||||
}
|
||||
if (this.params.entry.params.view.name === 'camera-target' && box) {
|
||||
detail = this.decideDetail(box, this.params.entry.params.view.params.dynamicDetailLevel);
|
||||
}
|
||||
|
||||
url += `?detail=${detail}`;
|
||||
|
||||
@@ -201,58 +249,21 @@ export namespace VolumeStreaming {
|
||||
return ret;
|
||||
}
|
||||
|
||||
private updateSelectionBoxParams(box: Box3D) {
|
||||
if (this.params.entry.params.view.name !== 'selection-box') return;
|
||||
private async updateParams(box: Box3D | undefined, autoIsSelection: boolean = false) {
|
||||
const newParams = copyParams(this.params);
|
||||
const viewType = newParams.entry.params.view.name;
|
||||
if (viewType !== 'off' && viewType !== 'cell') {
|
||||
newParams.entry.params.view.params.bottomLeft = box?.min || Vec3.zero();
|
||||
newParams.entry.params.view.params.topRight = box?.max || Vec3.zero();
|
||||
}
|
||||
if (viewType === 'auto') {
|
||||
newParams.entry.params.view.params.isSelection = autoIsSelection;
|
||||
}
|
||||
|
||||
const state = this.plugin.state.data;
|
||||
const newParams: Params = {
|
||||
...this.params,
|
||||
entry: {
|
||||
name: this.params.entry.name,
|
||||
params: {
|
||||
...this.params.entry.params,
|
||||
view: {
|
||||
name: 'selection-box' as const,
|
||||
params: {
|
||||
radius: this.params.entry.params.view.params.radius,
|
||||
bottomLeft: box.min,
|
||||
topRight: box.max
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const update = state.build().to(this.ref).update(newParams);
|
||||
|
||||
PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
|
||||
private updateAutoParams(box: Box3D | undefined, isSelection: boolean) {
|
||||
if (this.params.entry.params.view.name !== 'auto') return;
|
||||
|
||||
const state = this.plugin.state.data;
|
||||
const newParams: Params = {
|
||||
...this.params,
|
||||
entry: {
|
||||
name: this.params.entry.name,
|
||||
params: {
|
||||
...this.params.entry.params,
|
||||
view: {
|
||||
name: 'auto' as const,
|
||||
params: {
|
||||
radius: this.params.entry.params.view.params.radius,
|
||||
selectionDetailLevel: this.params.entry.params.view.params.selectionDetailLevel,
|
||||
isSelection,
|
||||
bottomLeft: box?.min || Vec3.zero(),
|
||||
topRight: box?.max || Vec3.zero()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const update = state.build().to(this.ref).update(newParams);
|
||||
|
||||
PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
|
||||
private getStructureRoot() {
|
||||
@@ -303,6 +314,18 @@ export namespace VolumeStreaming {
|
||||
}
|
||||
}
|
||||
|
||||
private isCameraTargetSame(a?: Camera.Snapshot, b?: Camera.Snapshot): boolean {
|
||||
if (!a || !b) return false;
|
||||
const targetSame = Vec3.equals(a.target, b.target);
|
||||
const sqDistA = Vec3.squaredDistance(a.target, a.position);
|
||||
const sqDistB = Vec3.squaredDistance(b.target, b.position);
|
||||
const distanceSame = Math.abs(sqDistA - sqDistB) / sqDistA < 1e-3;
|
||||
return targetSame && distanceSame;
|
||||
}
|
||||
private cameraTargetDistance(snapshot: Camera.Snapshot): number {
|
||||
return Vec3.distance(snapshot.target, snapshot.position);
|
||||
}
|
||||
|
||||
private _invTransform: Mat4 = Mat4();
|
||||
private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
|
||||
if (Loci.isEmpty(loci) || isEmptyLoci(loci)) {
|
||||
@@ -328,39 +351,82 @@ export namespace VolumeStreaming {
|
||||
}
|
||||
|
||||
private updateAuto(loci: StructureElement.Loci | EmptyLoci) {
|
||||
// if (Loci.areEqual(this.lastLoci, loci)) {
|
||||
// this.lastLoci = EmptyLoci;
|
||||
// this.updateSelectionBoxParams(Box3D.empty());
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.lastLoci = loci;
|
||||
|
||||
if (isEmptyLoci(loci)) {
|
||||
this.updateAutoParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const box = this.getBoxFromLoci(loci);
|
||||
this.updateAutoParams(box, true);
|
||||
this.updateQueue.enqueue(async () => {
|
||||
this.lastLoci = loci;
|
||||
if (isEmptyLoci(loci)) {
|
||||
await this.updateParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
|
||||
} else {
|
||||
await this.updateParams(this.getBoxFromLoci(loci), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateSelectionBox(loci: StructureElement.Loci | EmptyLoci) {
|
||||
if (Loci.areEqual(this.lastLoci, loci)) {
|
||||
this.lastLoci = EmptyLoci;
|
||||
this.updateSelectionBoxParams(Box3D());
|
||||
return;
|
||||
this.updateQueue.enqueue(async () => {
|
||||
if (Loci.areEqual(this.lastLoci, loci)) {
|
||||
this.lastLoci = EmptyLoci;
|
||||
} else {
|
||||
this.lastLoci = loci;
|
||||
}
|
||||
const box = this.getBoxFromLoci(this.lastLoci);
|
||||
await this.updateParams(box);
|
||||
});
|
||||
}
|
||||
|
||||
private updateCameraTarget(snapshot: Camera.Snapshot) {
|
||||
this.updateQueue.enqueue(async () => {
|
||||
const origManualReset = this.plugin.canvas3d?.props.camera.manualReset;
|
||||
try {
|
||||
if (!origManualReset) this.plugin.canvas3d?.setProps({ camera: { manualReset: true } });
|
||||
const box = this.boxFromCameraTarget(snapshot, true);
|
||||
await this.updateParams(box);
|
||||
} finally {
|
||||
if (!origManualReset) this.plugin.canvas3d?.setProps({ camera: { manualReset: origManualReset } });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boxFromCameraTarget(snapshot: Camera.Snapshot, boundByBoundarySize: boolean): Box3D {
|
||||
const target = snapshot.target;
|
||||
const distance = this.cameraTargetDistance(snapshot);
|
||||
const top = Math.tan(0.5 * snapshot.fov) * distance;
|
||||
let radius = top;
|
||||
const viewport = this.plugin.canvas3d?.camera.viewport;
|
||||
if (viewport && viewport.width > viewport.height) {
|
||||
radius *= viewport.width / viewport.height;
|
||||
}
|
||||
|
||||
this.lastLoci = loci;
|
||||
|
||||
if (isEmptyLoci(loci)) {
|
||||
this.updateSelectionBoxParams(Box3D());
|
||||
return;
|
||||
const relativeRadius = this.params.entry.params.view.name === 'camera-target' ? this.params.entry.params.view.params.radius : 0.5;
|
||||
radius *= relativeRadius;
|
||||
let radiusX, radiusY, radiusZ;
|
||||
if (boundByBoundarySize) {
|
||||
const bBoxSize = Vec3.zero();
|
||||
Box3D.size(bBoxSize, this.data.structure.boundary.box);
|
||||
radiusX = Math.min(radius, 0.5 * bBoxSize[0]);
|
||||
radiusY = Math.min(radius, 0.5 * bBoxSize[1]);
|
||||
radiusZ = Math.min(radius, 0.5 * bBoxSize[2]);
|
||||
} else {
|
||||
radiusX = radiusY = radiusZ = radius;
|
||||
}
|
||||
return Box3D.create(
|
||||
Vec3.create(target[0] - radiusX, target[1] - radiusY, target[2] - radiusZ),
|
||||
Vec3.create(target[0] + radiusX, target[1] + radiusY, target[2] + radiusZ)
|
||||
);
|
||||
}
|
||||
|
||||
const box = this.getBoxFromLoci(loci);
|
||||
this.updateSelectionBoxParams(box);
|
||||
private decideDetail(box: Box3D, baseDetail: number): number {
|
||||
const cellVolume = this.info.kind === 'x-ray'
|
||||
? Box3D.volume(this.data.structure.boundary.box)
|
||||
: this.info.header.spacegroup.size.reduce((a, b) => a * b, 1);
|
||||
const boxVolume = Box3D.volume(box);
|
||||
let ratio = boxVolume / cellVolume;
|
||||
const maxDetail = this.info.header.availablePrecisions.length - 1;
|
||||
let detail = baseDetail;
|
||||
while (ratio <= 0.5 && detail < maxDetail) {
|
||||
ratio *= 2;
|
||||
detail += 1;
|
||||
}
|
||||
// console.log(`Decided dynamic detail: ${detail}, (base detail: ${baseDetail}, box/cell volume ratio: ${boxVolume / cellVolume})`);
|
||||
return detail;
|
||||
}
|
||||
|
||||
async update(params: Params) {
|
||||
@@ -369,6 +435,11 @@ export namespace VolumeStreaming {
|
||||
this.params = params;
|
||||
let box: Box3D | undefined = void 0, emptyData = false;
|
||||
|
||||
if (params.entry.params.view.name !== 'camera-target' && this.cameraTargetSubscription) {
|
||||
this.cameraTargetSubscription.unsubscribe();
|
||||
this.cameraTargetSubscription = undefined;
|
||||
}
|
||||
|
||||
switch (params.entry.params.view.name) {
|
||||
case 'off':
|
||||
emptyData = true;
|
||||
@@ -388,6 +459,12 @@ export namespace VolumeStreaming {
|
||||
Box3D.expand(box, box, Vec3.create(r, r, r));
|
||||
break;
|
||||
}
|
||||
case 'camera-target':
|
||||
if (!this.cameraTargetSubscription) {
|
||||
this.cameraTargetSubscription = this.subscribeObservable(this.cameraTargetObservable, (e) => this.updateCameraTarget(e));
|
||||
}
|
||||
box = this.boxFromCameraTarget(this.plugin.canvas3d!.camera.getSnapshot(), true);
|
||||
break;
|
||||
case 'cell':
|
||||
box = this.info.kind === 'x-ray'
|
||||
? this.data.structure.boundary.box
|
||||
@@ -439,6 +516,7 @@ export namespace VolumeStreaming {
|
||||
|
||||
getDescription() {
|
||||
if (this.params.entry.params.view.name === 'selection-box') return 'Selection';
|
||||
if (this.params.entry.params.view.name === 'camera-target') return 'Camera';
|
||||
if (this.params.entry.params.view.name === 'box') return 'Static Box';
|
||||
if (this.params.entry.params.view.name === 'cell') return 'Cell';
|
||||
return '';
|
||||
@@ -449,6 +527,7 @@ export namespace VolumeStreaming {
|
||||
|
||||
this.infoMap = new Map<string, VolumeServerInfo.EntryData>();
|
||||
this.data.entries.forEach(info => this.infoMap.set(info.dataId, info));
|
||||
this.updateQueue = new SingleAsyncQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2022 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>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject as SO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
|
||||
@@ -219,6 +220,7 @@ const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({
|
||||
canAutoUpdate: ({ oldParams, newParams }) => {
|
||||
return oldParams.entry.params.view === newParams.entry.params.view
|
||||
|| newParams.entry.params.view.name === 'selection-box'
|
||||
|| newParams.entry.params.view.name === 'camera-target'
|
||||
|| newParams.entry.params.view.name === 'off';
|
||||
},
|
||||
apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const PluginConfig = {
|
||||
PixelScale: item('plugin-config.pixel-scale', 1),
|
||||
PickScale: item('plugin-config.pick-scale', 0.25),
|
||||
PickPadding: item('plugin-config.pick-padding', 3),
|
||||
EnableWboit: item('plugin-config.enable-wboit', PluginFeatureDetection.wboit),
|
||||
EnableWboit: item('plugin-config.enable-wboit', true),
|
||||
// as of Oct 1 2021, WebGL 2 doesn't work on iOS 15.
|
||||
// TODO: check back in a few weeks to see if it was fixed
|
||||
PreferWebGl1: item('plugin-config.prefer-webgl1', PluginFeatureDetection.preferWebGl1),
|
||||
|
||||
@@ -29,10 +29,4 @@ export const PluginFeatureDetection = {
|
||||
const isTouchScreen = navigator.maxTouchPoints >= 4; // true for iOS 13 (and hopefully beyond)
|
||||
return !(window as any).MSStream && (isIOS || (isAppleDevice && isTouchScreen));
|
||||
},
|
||||
get wboit() {
|
||||
if (typeof navigator === 'undefined' || typeof window === 'undefined') return true;
|
||||
|
||||
// disable Wboit in Safari 15
|
||||
return !/Version\/15.\d Safari/.test(navigator.userAgent);
|
||||
}
|
||||
};
|
||||
@@ -24,6 +24,10 @@ namespace Binding {
|
||||
return { triggers, action, description };
|
||||
}
|
||||
|
||||
export function isBinding(x: any): x is Binding {
|
||||
return !!x && Array.isArray(x.triggers) && typeof x.action === 'string';
|
||||
}
|
||||
|
||||
export const Empty: Binding = { triggers: [], action: '', description: '' };
|
||||
export function isEmpty(binding: Binding) {
|
||||
return binding.triggers.length === 0 ||
|
||||
|
||||
41
src/mol-util/single-async-queue.ts
Normal file
41
src/mol-util/single-async-queue.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
/** Job queue that allows at most one running and one pending job.
|
||||
* A newly enqueued job will cancel any other pending jobs. */
|
||||
export class SingleAsyncQueue {
|
||||
private isRunning: boolean;
|
||||
private queue: { id: number, func: () => any }[];
|
||||
private counter: number;
|
||||
private log: boolean;
|
||||
constructor(log: boolean = false) {
|
||||
this.isRunning = false;
|
||||
this.queue = [];
|
||||
this.counter = 0;
|
||||
this.log = log;
|
||||
}
|
||||
enqueue(job: () => any) {
|
||||
if (this.log) console.log('SingleAsyncQueue enqueue', this.counter);
|
||||
this.queue[0] = { id: this.counter, func: job };
|
||||
this.counter++;
|
||||
this.run(); // do not await
|
||||
}
|
||||
private async run() {
|
||||
if (this.isRunning) return;
|
||||
const job = this.queue.pop();
|
||||
if (!job) return;
|
||||
this.isRunning = true;
|
||||
try {
|
||||
if (this.log) console.log('SingleAsyncQueue run', job.id);
|
||||
await job.func();
|
||||
if (this.log) console.log('SingleAsyncQueue complete', job.id);
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
this.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user