diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index f3fae6e80fe4442f4df5c2c7d799d8222a29cdf0..5aa091a68f86ba3b9e726f810fb8d7b1962aae32 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/src/mol-plugin/behavior/behavior.ts @@ -136,4 +136,24 @@ namespace PluginBehavior { constructor(protected ctx: PluginContext, protected params: P) { } } + + export abstract class WithSubscribers<P = { }> implements PluginBehavior<P> { + abstract register(ref: string): void; + + private subs: PluginCommand.Subscription[] = []; + 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)); + } + + unregister() { + for (const s of this.subs) s.unsubscribe(); + this.subs = []; + } + + constructor(protected plugin: PluginContext) { + } + } } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 3f9925eb128862d5b5819a61f2d47b3d86dc1b5e..7990e753dcdbaa98dbb98ba40a34e52e03460a30 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts @@ -18,6 +18,9 @@ import CIF from 'mol-io/reader/cif'; import { Box3D } from 'mol-math/geometry'; import { urlCombine } from 'mol-util/url'; import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server'; +import { StructureElement } from 'mol-model/structure'; +import { Loci } from 'mol-model/loci'; +import { CreateVolumeStreamingBehavior } from './transformers'; export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { } @@ -42,15 +45,19 @@ export namespace VolumeStreaming { const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) }; return { - view: PD.MappedStatic('box', { + view: PD.MappedStatic('selection-box', { 'box': PD.Group({ bottomLeft: PD.Vec3(Vec3.create(-22.4, -33.4, -21.6)), topRight: PD.Vec3(Vec3.create(-7.1, -10, -0.9)), - autoUpdate: PD.Boolean(true, { description: 'Update the box when user clicks an element.' }) }, { description: 'Static box defined by cartesian coords.', isFlat: true }), + 'selection-box': PD.Group({ + radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }), + bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }), + topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }), + }, { description: 'Box around last-interacted element.', isFlat: true }), 'cell': PD.Group({}), // 'auto': PD.Group({ }), // based on camera distance/active selection/whatever, show whole structure or slice. - }, { options: [['box', 'Bounded Box'], ['cell', 'Whole Structure']] }), + }, { options: [['box', 'Bounded Box'], ['selection-box', 'Selection'], ['cell', 'Whole Structure']] }), detailLevel: PD.Select<number>(Math.min(1, info.header.availablePrecisions.length - 1), info.header.availablePrecisions.map((p, i) => [i, `${i + 1} (${Math.pow(p.maxVoxels, 1 / 3) | 0}^3)`] as [number, string])), channels: info.kind === 'em' @@ -81,10 +88,10 @@ export namespace VolumeStreaming { } export type Channels = { [name in ChannelType]?: ChannelInfo } - export class Behavior implements PluginBehavior<{}> { + export class Behavior extends PluginBehavior.WithSubscribers<Params> { private cache = LRUCache.create<ChannelsData>(25); private params: Params = {} as any; - private ref: string = ''; + // private ref: string = ''; channels: Channels = {} @@ -139,18 +146,50 @@ export namespace VolumeStreaming { } register(ref: string): void { - this.ref = ref; + // this.ref = ref; + + this.subscribeObservable(this.plugin.events.canvas3d.click, ({ current }) => { + if (this.params.view.name !== 'selection-box') return; + if (!StructureElement.isLoci(current.loci)) return; + + // TODO: check if it's the related structure + + const eR = this.params.view.params.radius; + + const sphere = Loci.getBoundingSphere(current.loci)!; + const r = Vec3.create(sphere.radius + eR, sphere.radius + eR, sphere.radius + eR); + const box = Box3D.create(Vec3.sub(Vec3.zero(), sphere.center, r), Vec3.add(Vec3.zero(), sphere.center, r)); + + const update = this.plugin.state.dataState.build().to(ref) + .update(CreateVolumeStreamingBehavior, old => ({ + ...old, + view: { + name: 'selection-box' as 'selection-box', + params: { + radius: eR, + bottomLeft: box.min, + topRight: box.max + } + } + })); + + this.plugin.runTask(this.plugin.state.dataState.updateTree(update)); + }); } async update(params: Params) { this.params = params; - let box: Box3D | undefined = void 0; + let box: Box3D | undefined = void 0, emptyData = false; switch (params.view.name) { case 'box': box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight); break; + case 'selection-box': + box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight); + emptyData = Box3D.volume(box) < 0.0001; + break; case 'cell': box = this.info.kind === 'x-ray' ? this.info.structure.boundary.box @@ -158,18 +197,18 @@ export namespace VolumeStreaming { break; } - const data = await this.queryData(box); + const data = emptyData ? { } : await this.queryData(box); if (!data) return false; const info = params.channels as ChannelsInfo; if (this.info.kind === 'x-ray') { - this.channels['2fo-fc'] = this.createChannel(data['2FO-FC']!, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]); - this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC']!, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]); - this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC']!, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]); + this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || VolumeData.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]); + this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]); + this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]); } else { - this.channels['em'] = this.createChannel(data['EM']!, info['em'], this.info.header.sampling[0].valuesInfo[0]); + this.channels['em'] = this.createChannel(data['EM'] || VolumeData.One, info['em'], this.info.header.sampling[0].valuesInfo[0]); } return true; @@ -185,12 +224,8 @@ export namespace VolumeStreaming { }; } - unregister(): void { - // throw new Error('Method not implemented.'); - } - constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) { - + super(plugin); } } } \ 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 2ed0f9eff7a8a50936563a56e8b89dff5b866771..d7c6f12f27e27a0b1e302c471d80b0481a610e32 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts @@ -33,10 +33,10 @@ export const InitVolumeStreaming = StateAction.build({ })(({ ref, state, params }, plugin: PluginContext) => Task.create('Volume Streaming', async taskCtx => { // TODO: custom react view for this and the VolumeStreamingBehavior transformer - let dataId = params.id, emDefaultContourLevel: number | undefined; + let dataId = params.id.toLowerCase(), emDefaultContourLevel: number | undefined; if (params.method === 'em') { await taskCtx.update('Getting EMDB info...'); - const emInfo = await getEmdbIdAndContourLevel(plugin, taskCtx, params.id); + const emInfo = await getEmdbIdAndContourLevel(plugin, taskCtx, dataId); dataId = emInfo.emdbId; emDefaultContourLevel = emInfo.contour; } @@ -54,11 +54,11 @@ export const InitVolumeStreaming = StateAction.build({ const behTree = state.build().to(infoTree.ref).apply(CreateVolumeStreamingBehavior, PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data))) if (params.method === 'em') { - behTree.apply(VolumeStreamingVisual, { channel: 'em' }); + behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { props: { isGhost: true } }); } else { - behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }); - behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }); - behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }); + behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { props: { isGhost: true } }); + behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { props: { isGhost: true } }); + behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { props: { isGhost: true } }); } await state.updateTree(behTree).runInContext(taskCtx); })); @@ -112,7 +112,8 @@ const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({ } })({ canAutoUpdate: ({ oldParams, newParams }) => { - return oldParams.view === newParams.view; + return oldParams.view === newParams.view + || (oldParams.view.name === newParams.view.name && oldParams.view.name === 'selection-box'); }, apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => { const behavior = new VolumeStreaming.Behavior(plugin, a.data);