diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e626135db116d926fb541df67b641ed485a55ad..79ec858e85d7aaadbcacc6c7f5bc7db7e64214b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +## [v3.15.0] - 2022-08-23 + - Integration of Dual depth peeling - OIT method - 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 diff --git a/package-lock.json b/package-lock.json index a72fbe0fcc2a7b5c080d31538b3576a78bb47ffa..790dffbec532074be7c354995dc9ee44609c165f 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index ef924d7f8f7262735c7a8bb8e6103cb689ae5acb..c52403ed64524c15fef0f8d1bf7ee21ce96f8598 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "molstar", - "version": "3.14.0", + "version": "3.15.0", "description": "A comprehensive macromolecular library.", "homepage": "https://github.com/molstar/molstar#readme", "repository": { @@ -90,6 +90,7 @@ "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>", "Gianluca Tomasello <giagitom@gmail.com>" ], "license": "MIT", diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts index 5c5e39f21ed821b16da7cbd614dcf38943468358..d55b4ee8a79c6abccef76f75969fffe2813ffe83 100644 --- a/src/mol-math/linear-algebra/3d/vec3.ts +++ b/src/mol-math/linear-algebra/3d/vec3.ts @@ -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; } diff --git a/src/mol-plugin-ui/custom/volume.tsx b/src/mol-plugin-ui/custom/volume.tsx index 8a6d9b1b167e3146f45ca7fcdc442a7840f412a1..0a13a436e087795313b51dd9ac5999822b4df229 100644 --- a/src/mol-plugin-ui/custom/volume.tsx +++ b/src/mol-plugin-ui/custom/volume.tsx @@ -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 } diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index b65203e9c88a52d7399c6c10a6f473153fad7b62..ddbb603c351f26f63978ca998ea3d5a5bab7475b 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/src/mol-plugin/behavior/behavior.ts @@ -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(); diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 071e4544ac1f234044caa0d222ad3953025c44bf..4cec92de8bb504f853a67911c156743f13c22453 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts @@ -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; - - 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; + 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: '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); + }); + } - this.lastLoci = loci; + 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 } }); + } + }); + } - if (isEmptyLoci(loci)) { - this.updateSelectionBoxParams(Box3D()); - return; + 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; + } + 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(); } } -} \ No newline at end of file +} diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts index af1c49d98d59c0e2adf0d4ffd9c035f441866688..d4285669051f60b09d99650d6ab4cd991ad0725f 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts @@ -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 _ => { diff --git a/src/mol-util/single-async-queue.ts b/src/mol-util/single-async-queue.ts new file mode 100644 index 0000000000000000000000000000000000000000..be62698d1af38e9c140324e9784b21530f051749 --- /dev/null +++ b/src/mol-util/single-async-queue.ts @@ -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(); + } + } +}