Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Rose
1db0ada684 debugging 2022-08-20 16:50:39 -07:00
21 changed files with 160 additions and 360 deletions

View File

@@ -6,18 +6,6 @@ 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
View File

@@ -1,12 +1,12 @@
{
"name": "molstar",
"version": "3.16.0",
"version": "3.14.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "molstar",
"version": "3.16.0",
"version": "3.14.0",
"license": "MIT",
"dependencies": {
"@types/argparse": "^2.0.10",

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "3.16.0",
"version": "3.14.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -89,8 +89,7 @@
"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>",
"Adam Midlik <midlik@gmail.com>"
"Panagiotis Tourlas <panagiot_tourlov@hotmail.com>"
],
"license": "MIT",
"devDependencies": {

View File

@@ -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 & { label?: string }) {
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
source: {
@@ -208,7 +208,6 @@ 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 },
}
}

View File

@@ -38,15 +38,6 @@
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>

View File

@@ -18,8 +18,6 @@ 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,
@@ -52,7 +50,6 @@ export class WboitPass {
private readonly framebuffer: Framebuffer;
private readonly textureA: Texture;
private readonly textureB: Texture;
private readonly depthRenderbuffer: Renderbuffer;
private _supported = false;
get supported() {
@@ -90,7 +87,6 @@ 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));
}
}
@@ -110,8 +106,6 @@ export class WboitPass {
this.textureA.attachFramebuffer(this.framebuffer, 'color0');
this.textureB.attachFramebuffer(this.framebuffer, 'color1');
this.depthRenderbuffer.attachFramebuffer(this.framebuffer);
}
static isSupported(webgl: WebGLContext) {
@@ -134,7 +128,7 @@ export class WboitPass {
constructor(private webgl: WebGLContext, width: number, height: number) {
if (!WboitPass.isSupported(webgl)) return;
const { resources, gl } = webgl;
const { resources } = webgl;
this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
this.textureA.define(width, height);
@@ -142,10 +136,6 @@ 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();

View File

@@ -44,9 +44,10 @@ varying vec3 vModelPosition;
varying vec3 vViewPosition;
#if defined(noNonInstancedActiveAttribs)
// int() is needed for some Safari versions
// see https://bugs.webkit.org/show_bug.cgi?id=244152
#define VertexID int(gl_VertexID)
#define VertexID gl_VertexID // for testing
// // int() is needed for some Safari versions
// // see https://bugs.webkit.org/show_bug.cgi?id=244152
// #define VertexID int(gl_VertexID)
#else
attribute float aVertex;
#define VertexID int(aVertex)

View File

@@ -109,14 +109,14 @@ void main() {
vec3 vViewPosition = vModelPosition + intersection.x * rayDir;
vViewPosition = (uView * vec4(vViewPosition, 1.0)).xyz;
float fragmentDepth = calcDepth(vViewPosition);
if (fragmentDepth < 0.0) discard;
if (fragmentDepth > 1.0) discard;
gl_FragDepthEXT = fragmentDepth;
gl_FragDepthEXT = calcDepth(vViewPosition);
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)

View File

@@ -70,17 +70,17 @@ void main(void){
}
vec3 vViewPosition = cameraPos;
float fragmentDepth = calcDepth(vViewPosition);
if (!flag && fragmentDepth >= 0.0) {
fragmentDepth = 0.0 + (0.0000001 / vRadius);
gl_FragDepthEXT = calcDepth(vViewPosition);
if (!flag && gl_FragDepthEXT >= 0.0) {
gl_FragDepthEXT = 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)

View File

@@ -35,7 +35,7 @@ function Vec3() {
namespace Vec3 {
export function zero(): Vec3 {
const out = [0.1, 0.0, 0.0]; // ensure backing array of type double
const out = [0.1, 0.0, 0.0];
out[0] = 0;
return out as any;
}

View File

@@ -90,7 +90,6 @@ 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' })
})
@@ -105,7 +104,7 @@ const DownloadStructure = StateAction.build({
switch (src.name) {
case 'url':
downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary, label: src.params.label || undefined }];
downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary }];
format = src.params.format;
break;
case 'pdb':

View File

@@ -41,10 +41,8 @@ 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))
@@ -78,15 +76,13 @@ 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), ...params.theme?.globalColorParams }
: { ...params.theme?.globalColorParams };
? { carbonColor: getCarbonColorParams(params.theme?.carbonColor) }
: { };
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, symmetryColorParams, globalColorParams, typeParams, ballAndStickColor };
return { update, builder, color, symmetryColor, typeParams, ballAndStickColor };
}
export function updateFocusRepr<T extends ColorTheme.BuiltIn>(plugin: PluginContext, structure: Structure, themeName: T | undefined, themeParams: ColorTheme.BuiltInParams<T> | undefined) {
@@ -181,18 +177,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, symmetryColorParams, globalColorParams, ballAndStickColor } = reprBuilder(plugin, params, structure);
const { update, builder, typeParams, color, symmetryColor, ballAndStickColor } = reprBuilder(plugin, params, structure);
const representations = {
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' }),
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { 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, 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' })
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' })
};
await update.commit({ revertOnError: false });
@@ -227,11 +223,11 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
smoothness: structure.isCoarseGrained ? 1.0 : 1.5,
};
const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
const representations = {
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' })
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' })
};
await update.commit({ revertOnError: true });
@@ -279,11 +275,11 @@ const coarseSurface = StructureRepresentationPresetProvider({
});
}
const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
const representations = {
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' })
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' })
};
await update.commit({ revertOnError: true });
@@ -313,10 +309,10 @@ const polymerCartoon = StructureRepresentationPresetProvider({
sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2
};
const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
const representations = {
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' })
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' })
};
await update.commit({ revertOnError: true });
@@ -371,9 +367,9 @@ const atomicDetail = StructureRepresentationPresetProvider({
});
}
const { update, builder, typeParams, color, ballAndStickColor, globalColorParams } = reprBuilder(plugin, params, structure);
const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params, structure);
const colorParams = lowResidueElementRatio && !bondsGiven
? { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams }
? { carbonColor: { name: 'element-symbol', params: {} } }
: ballAndStickColor;
const representations = {
@@ -381,7 +377,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, colorParams: globalColorParams }, { tag: 'snfg-3d' }),
snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams: { ...typeParams, alpha: 0.4, visuals: ['carbohydrate-symbol'] }, color }, { tag: 'snfg-3d' }),
});
}

View File

@@ -1,8 +1,7 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019 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';
@@ -200,9 +199,6 @@ 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;
@@ -244,23 +240,13 @@ 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 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 isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
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;
@@ -288,13 +274,6 @@ 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,
@@ -303,11 +282,12 @@ 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: selectionDetailLevel,
selectionDetailLevel: { ...detailLevel, label: 'Selection Detail' },
isRelative: isRelativeParam,
isUnbounded: isUnboundedParam,
}, { description: 'Box around focused element.' }),
}, { 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.' })
// '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.' })
};
const options = {
entry: params.entry.name,
@@ -319,7 +299,6 @@ 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
}

View File

@@ -7,15 +7,14 @@
import * as React from 'react';
import { Binding } from '../../mol-util/binding';
import { PluginUIComponent } from '../base';
import { StateTransformer, StateSelection, State } from '../../mol-state';
import { StateTransformer, StateSelection } 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]).filter(b => Binding.isBinding(b[1]));
return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]);
}
export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]: Binding } }> {
@@ -78,30 +77,19 @@ export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolea
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
}
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);
const interactionBindings: { [k: string]: Binding } = {};
this.plugin.spec.behaviors.forEach(b => {
const { bindings } = b.defaultParams;
if (bindings) Object.assign(interactionBindings, bindings);
});
return <>
{(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
<BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
</HelpGroup>}
{!!interactionBindings && <HelpGroup key='interactions' header='Mouse Controls'>
<HelpGroup key='interactions' header='Mouse Controls'>
<BindingsHelp bindings={interactionBindings} />
</HelpGroup>}
</HelpGroup>
</>;
}
}

View File

@@ -1,8 +1,7 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018 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';
@@ -145,18 +144,8 @@ 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): 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();
}
}
};
protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void) {
this.subs.push(o.subscribe(action));
}
dispose(): void {
for (const s of this.subs) s.unsubscribe();

View File

@@ -1,9 +1,8 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 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';
@@ -25,10 +24,6 @@ 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' }) { }
@@ -58,7 +53,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));
@@ -73,7 +68,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) };
@@ -91,24 +86,19 @@ 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: createDetailParams(info.header.availablePrecisions, 6, { label: 'Selection Detail' }),
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).' }),
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: createDetailParams(info.header.availablePrecisions, 3),
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).' }),
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'])
@@ -121,40 +111,13 @@ export namespace VolumeStreaming {
};
}
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 const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
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 ViewTypes = 'off' | 'box' | 'selection-box' | '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 }
@@ -177,14 +140,6 @@ 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 = {};
@@ -208,9 +163,6 @@ 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}`;
@@ -249,21 +201,58 @@ export namespace VolumeStreaming {
return ret;
}
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;
}
private updateSelectionBoxParams(box: Box3D) {
if (this.params.entry.params.view.name !== 'selection-box') 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: '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);
await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
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 } });
}
private getStructureRoot() {
@@ -314,18 +303,6 @@ 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)) {
@@ -351,82 +328,39 @@ export namespace VolumeStreaming {
}
private updateAuto(loci: StructureElement.Loci | EmptyLoci) {
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);
}
});
// 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);
}
private updateSelectionBox(loci: StructureElement.Loci | EmptyLoci) {
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;
if (Loci.areEqual(this.lastLoci, loci)) {
this.lastLoci = EmptyLoci;
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)
);
}
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;
this.lastLoci = loci;
if (isEmptyLoci(loci)) {
this.updateSelectionBoxParams(Box3D());
return;
}
// console.log(`Decided dynamic detail: ${detail}, (base detail: ${baseDetail}, box/cell volume ratio: ${boxVolume / cellVolume})`);
return detail;
const box = this.getBoxFromLoci(loci);
this.updateSelectionBoxParams(box);
}
async update(params: Params) {
@@ -435,11 +369,6 @@ 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;
@@ -459,12 +388,6 @@ 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
@@ -516,7 +439,6 @@ 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 '';
@@ -527,7 +449,6 @@ 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();
}
}
}
}

View File

@@ -1,9 +1,8 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 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';
@@ -220,7 +219,6 @@ 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 _ => {

View File

@@ -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', true),
EnableWboit: item('plugin-config.enable-wboit', PluginFeatureDetection.wboit),
// 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),

View File

@@ -29,4 +29,11 @@ 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() {
return true; // for testing
// if (typeof navigator === 'undefined' || typeof window === 'undefined') return true;
// // disable Wboit in Safari 15
// return !/Version\/15.\d Safari/.test(navigator.userAgent);
}
};

View File

@@ -24,10 +24,6 @@ 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 ||

View File

@@ -1,41 +0,0 @@
/**
* 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();
}
}
}