|
@@ -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) {
|
|
|
+ let 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();
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+}
|