diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a47280049c7837d1c9f2c412ad5c732b2d385d..330b3f25d1e56fa55b31a27fcb3cafeef3be814c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +- [empty] + +## [v2.0.5] - 2021-04-26 + - Ability to pass ``Canvas3DContext`` to ``PluginContext.fromCanvas``. - Relative frame support for ``Canvas3D`` viewport. - Fix bug in screenshot copy UI. @@ -13,6 +17,10 @@ Note that since we don't clearly distinguish between a public and private interf - Support for full pausing (no draw) rendering: ``Canvas3D.pause(true)``. - Add `MeshBuilder.addMesh`. - Add `Torus` primitive. +- Lazy volume loading support. +- [Breaking] ``Viewer.loadVolumeFromUrl`` signature change. + - ``loadVolumeFromUrl(url, format, isBinary, isovalues, entryId)`` => ``loadVolumeFromUrl({ url, format, isBinary }, isovalues, { entryId, isLazy })`` +- Add ``TextureMesh`` support to ``geo-export`` extension. ## [v2.0.4] - 2021-04-20 diff --git a/package-lock.json b/package-lock.json index 2a44265afce892d1a93b5fb11fdf71abac389f26..d422a4144bec0d0211ae38b339fb8817cf5bf7de 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index b3ab20ef622319d80d1b55f4ec98b5b9318143d1..44873159b6d87421c98232dd842dd583214ad22b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "molstar", - "version": "2.0.4", + "version": "2.0.5", "description": "A comprehensive macromolecular library.", "homepage": "https://github.com/molstar/molstar#readme", "repository": { diff --git a/src/apps/viewer/embedded.html b/src/apps/viewer/embedded.html index 9e7238aa5b7fbe9c7238b561e29f67008f8160bd..e5cc89d86fd7773412720607ff82a35448c11c23 100644 --- a/src/apps/viewer/embedded.html +++ b/src/apps/viewer/embedded.html @@ -21,7 +21,7 @@ <script type="text/javascript" src="./molstar.js"></script> <script type="text/javascript"> var viewer = new molstar.Viewer('app', { - layoutIsExpanded: false, + layoutIsExpanded: true, layoutShowControls: false, layoutShowRemoteState: false, layoutShowSequence: true, @@ -37,6 +37,20 @@ }); viewer.loadPdb('7bv2'); viewer.loadEmdb('EMD-30210', { detail: 6 }); + + // viewer.loadVolumeFromUrl({ + // url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6', + // format: 'dscif', + // isBinary: true + // }, [{ + // type: 'relative', + // value: 1, + // color: 0x3377aa + // }], { + // entryId: 'EMD-30210', + // isLazy: true + // }); + // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } }) </script> </body> diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 283296edb7b4293fde2d8001ab4ced91f86db262..fada81b318d4e601152506661d95f2413569d253 100644 --- a/src/apps/viewer/index.ts +++ b/src/apps/viewer/index.ts @@ -243,17 +243,29 @@ export class Viewer { })); } - async loadVolumeFromUrl(url: string, format: BuildInVolumeFormat, isBinary: boolean, isovalues: VolumeIsovalueInfo[], entryId?: string) { + async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string, isLazy?: boolean }) { const plugin = this.plugin; if (!plugin.dataFormats.get(format)) { throw new Error(`Unknown density format: ${format}`); } + if (options?.isLazy) { + const update = this.plugin.build(); + update.toRoot().apply(StateTransforms.Data.LazyVolume, { + url, + format, + entryId: options?.entryId, + isBinary, + isovalues: isovalues.map(v => ({ alpha: 1, ...v })) + }); + return update.commit(); + } + return plugin.dataTransaction(async () => { - const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } }); + const data = await plugin.builders.data.download({ url, isBinary, label: options?.entryId }, { state: { isGhost: true } }); - const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId }); + const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId }); const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>; if (!volume?.isOk) throw new Error('Failed to parse any volume.'); @@ -261,7 +273,7 @@ export class Viewer { for (const iso of isovalues) { repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, { type: 'isosurface', - typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } }, + typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } }, color: 'uniform', colorParams: { value: iso.color } })); diff --git a/src/extensions/geo-export/controls.ts b/src/extensions/geo-export/controls.ts index 2307dbb64c4812dfa89b5eef7f0b32bf154b7db8..d309e13f5fcb6083e7bd086cbf2c24e4a4ff5189 100644 --- a/src/extensions/geo-export/controls.ts +++ b/src/extensions/geo-export/controls.ts @@ -31,7 +31,7 @@ export class GeometryControls extends PluginComponent { const objExporter = new ObjExporter(filename); for (let i = 0, il = renderObjects.length; i < il; ++i) { await ctx.update({ message: `Exporting object ${i}/${il}` }); - await objExporter.add(renderObjects[i], ctx); + await objExporter.add(renderObjects[i], this.plugin.canvas3d?.webgl!, ctx); } const { obj, mtl } = objExporter.getData(); diff --git a/src/extensions/geo-export/export.ts b/src/extensions/geo-export/export.ts index 3fd88ee81b6c8ce2f732a3b4049b1524b760b2da..8a8a5fc295db11c604bcadad8975df375d410e30 100644 --- a/src/extensions/geo-export/export.ts +++ b/src/extensions/geo-export/export.ts @@ -10,8 +10,10 @@ import { LinesValues } from '../../mol-gl/renderable/lines'; import { PointsValues } from '../../mol-gl/renderable/points'; import { SpheresValues } from '../../mol-gl/renderable/spheres'; import { CylindersValues } from '../../mol-gl/renderable/cylinders'; +import { TextureMeshValues } from '../../mol-gl/renderable/texture-mesh'; import { BaseValues, SizeValues } from '../../mol-gl/renderable/schema'; import { TextureImage } from '../../mol-gl/renderable/util'; +import { WebGLContext } from '../../mol-gl/webgl/context'; import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder'; import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere'; import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder'; @@ -32,7 +34,7 @@ type RenderObjectExportData = { } interface RenderObjectExporter<D extends RenderObjectExportData> { - add(renderObject: GraphicsRenderObject, ctx: RuntimeContext): Promise<void> | undefined + add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext): Promise<void> | undefined getData(): D } @@ -80,6 +82,17 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { return size * values.uSizeFactor.ref.value; } + private static getGroup(groups: Float32Array | Uint8Array, i: number): number { + const i4 = i * 4; + const r = groups[i4]; + const g = groups[i4 + 1]; + const b = groups[i4 + 2]; + if (groups instanceof Float32Array) { + return decodeFloatRGB(r * 255 + 0.5, g * 255 + 0.5, b * 255 + 0.5); + } + return decodeFloatRGB(r, g, b); + } + private updateMaterial(color: Color, alpha: number) { if (this.currentColor === color && this.currentAlpha === alpha) return; @@ -111,11 +124,12 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { } } - private async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array, groups: Float32Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, ctx: RuntimeContext) { + private async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, groups: Float32Array | Uint8Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, geoTexture: boolean, ctx: RuntimeContext) { const obj = this.obj; const t = Mat4(); const n = Mat3(); const tmpV = Vec3(); + const stride = geoTexture ? 4 : 3; const colorType = values.dColorType.ref.value; const tColor = values.tColor.ref.value.array; @@ -131,7 +145,7 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { // position for (let i = 0; i < vertexCount; ++i) { if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + i }); - v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * 3), t); + v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t); StringBuilder.writeSafe(obj, 'v '); StringBuilder.writeFloat(obj, tmpV[0], 1000); StringBuilder.whitespace1(obj); @@ -144,7 +158,7 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { // normal for (let i = 0; i < vertexCount; ++i) { if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount + i }); - v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * 3), n); + v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n); StringBuilder.writeSafe(obj, 'vn '); StringBuilder.writeFloat(obj, tmpV[0], 100); StringBuilder.whitespace1(obj); @@ -165,14 +179,17 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { case 'instance': color = Color.fromArray(tColor, instanceIndex * 3); break; - case 'group': - color = Color.fromArray(tColor, groups[indices[i]] * 3); + case 'group': { + const group = geoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]]; + color = Color.fromArray(tColor, group * 3); break; - case 'groupInstance': + } + case 'groupInstance': { const groupCount = values.uGroupCount.ref.value; - const group = groups[indices[i]]; + const group = geoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]]; color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3); break; + } case 'vertex': color = Color.fromArray(tColor, i * 3); break; @@ -183,9 +200,9 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { } this.updateMaterial(color, uAlpha); - const v1 = this.vertexOffset + indices[i] + 1; - const v2 = this.vertexOffset + indices[i + 1] + 1; - const v3 = this.vertexOffset + indices[i + 2] + 1; + const v1 = this.vertexOffset + (geoTexture ? i : indices![i]) + 1; + const v2 = this.vertexOffset + (geoTexture ? i + 1 : indices![i + 1]) + 1; + const v3 = this.vertexOffset + (geoTexture ? i + 2 : indices![i + 2]) + 1; StringBuilder.writeSafe(obj, 'f '); StringBuilder.writeInteger(obj, v1); StringBuilder.writeSafe(obj, '//'); @@ -212,7 +229,7 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { const drawCount = values.drawCount.ref.value; for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) { - await this.addMeshWithColors(aPosition, aNormal, elements, aGroup, vertexCount, drawCount, values, instanceIndex, ctx); + await this.addMeshWithColors(aPosition, aNormal, elements, aGroup, vertexCount, drawCount, values, instanceIndex, false, ctx); } } @@ -249,7 +266,7 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { const normals = mesh.normalBuffer.ref.value; const indices = mesh.indexBuffer.ref.value; const groups = mesh.groupBuffer.ref.value; - await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx); + await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, false, ctx); } } @@ -287,11 +304,40 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { const normals = mesh.normalBuffer.ref.value; const indices = mesh.indexBuffer.ref.value; const groups = mesh.groupBuffer.ref.value; - await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, ctx); + await this.addMeshWithColors(vertices, normals, indices, groups, vertices.length / 3, indices.length, values, instanceIndex, false, ctx); + } + } + + private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) { + const GeoExportName = 'geo-export'; + if (!webgl.namedFramebuffers[GeoExportName]) { + webgl.namedFramebuffers[GeoExportName] = webgl.resources.framebuffer(); + } + const framebuffer = webgl.namedFramebuffers[GeoExportName]; + + const [ width, height ] = values.uGeoTexDim.ref.value; + const vertices = new Float32Array(width * height * 4); + const normals = new Float32Array(width * height * 4); + const groups = webgl.isWebGL2 ? new Uint8Array(width * height * 4) : new Float32Array(width * height * 4); + + framebuffer.bind(); + values.tPosition.ref.value.attachFramebuffer(framebuffer, 0); + webgl.readPixels(0, 0, width, height, vertices); + values.tNormal.ref.value.attachFramebuffer(framebuffer, 0); + webgl.readPixels(0, 0, width, height, normals); + values.tGroup.ref.value.attachFramebuffer(framebuffer, 0); + webgl.readPixels(0, 0, width, height, groups); + + const vertexCount = values.uVertexCount.ref.value; + const instanceCount = values.instanceCount.ref.value; + const drawCount = values.drawCount.ref.value; + + for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) { + await this.addMeshWithColors(vertices, normals, undefined, groups, vertexCount, drawCount, values, instanceIndex, true, ctx); } } - add(renderObject: GraphicsRenderObject, ctx: RuntimeContext) { + add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext) { if (!renderObject.state.visible) return; switch (renderObject.type) { @@ -305,6 +351,8 @@ export class ObjExporter implements RenderObjectExporter<ObjData> { return this.addSpheres(renderObject.values as SpheresValues, ctx); case 'cylinders': return this.addCylinders(renderObject.values as CylindersValues, ctx); + case 'texture-mesh': + return this.addTextureMesh(renderObject.values as TextureMeshValues, webgl, ctx); } } diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index 628ef8252e956e4e4f10f4bdfca6b93ae7cc3940..100b00c9b12ee8eb22787c38b17c0f131c2cae71 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -234,7 +234,7 @@ namespace Camera { up: Vec3.create(0, 1, 0), target: Vec3.create(0, 0, 0), - radius: 10, + radius: 0, radiusMax: 10, fog: 50, clipFar: true @@ -272,6 +272,18 @@ namespace Camera { return out; } + + export function areSnapshotsEqual(a: Snapshot, b: Snapshot) { + return a.mode === b.mode + && a.fov === b.fov + && a.radius === b.radius + && a.radiusMax === b.radiusMax + && a.fog === b.fog + && a.clipFar === b.clipFar + && Vec3.exactEquals(a.position, b.position) + && Vec3.exactEquals(a.up, b.up) + && Vec3.exactEquals(a.target, b.target); + } } function updateOrtho(camera: Camera) { diff --git a/src/mol-plugin-state/manager/volume/hierarchy-state.ts b/src/mol-plugin-state/manager/volume/hierarchy-state.ts index 07238690e85d98c52eef1808667fac7c6ae9ed2e..5c22163b384486bcddf57b0f96b00115dcadba01 100644 --- a/src/mol-plugin-state/manager/volume/hierarchy-state.ts +++ b/src/mol-plugin-state/manager/volume/hierarchy-state.ts @@ -17,13 +17,14 @@ export function buildVolumeHierarchy(state: State, previous?: VolumeHierarchy) { export interface VolumeHierarchy { volumes: VolumeRef[], + lazyVolumes: LazyVolumeRef[], refs: Map<StateTransform.Ref, VolumeHierarchyRef> // TODO: might be needed in the future // decorators: Map<StateTransform.Ref, StateTransform>, } export function VolumeHierarchy(): VolumeHierarchy { - return { volumes: [], refs: new Map() }; + return { volumes: [], lazyVolumes: [], refs: new Map() }; } interface RefBase<K extends string = string, O extends StateObject = StateObject, T extends StateTransformer = StateTransformer> { @@ -32,7 +33,7 @@ interface RefBase<K extends string = string, O extends StateObject = StateObject version: StateTransform['version'] } -export type VolumeHierarchyRef = VolumeRef | VolumeRepresentationRef +export type VolumeHierarchyRef = VolumeRef | LazyVolumeRef | VolumeRepresentationRef export interface VolumeRef extends RefBase<'volume', SO.Volume.Data> { representations: VolumeRepresentationRef[] @@ -42,6 +43,13 @@ function VolumeRef(cell: StateObjectCell<SO.Volume.Data>): VolumeRef { return { kind: 'volume', cell, version: cell.transform.version, representations: [] }; } +export interface LazyVolumeRef extends RefBase<'lazy-volume', SO.Volume.Lazy> { +} + +function LazyVolumeRef(cell: StateObjectCell<SO.Volume.Lazy>): LazyVolumeRef { + return { kind: 'lazy-volume', cell, version: cell.transform.version }; +} + export interface VolumeRepresentationRef extends RefBase<'volume-representation', SO.Volume.Representation3D, StateTransforms['Representation']['VolumeRepresentation3D']> { volume: VolumeRef } @@ -95,6 +103,10 @@ const Mapping: [TestCell, ApplyRef, LeaveRef][] = [ state.currentVolume = createOrUpdateRefList(state, cell, state.hierarchy.volumes, VolumeRef, cell); }, state => state.currentVolume = void 0], + [cell => SO.Volume.Lazy.is(cell.obj), (state, cell) => { + createOrUpdateRefList(state, cell, state.hierarchy.lazyVolumes, LazyVolumeRef, cell); + }, noop], + [(cell, state) => { return !cell.state.isGhost && !!state.currentVolume && SO.Volume.Representation3D.is(cell.obj); }, (state, cell) => { diff --git a/src/mol-plugin-state/objects.ts b/src/mol-plugin-state/objects.ts index 8106bca4fdd6b2064e014ca564a19208437a9b6e..28c04c6577e22fa9db64981e1e58b10c8b670c03 100644 --- a/src/mol-plugin-state/objects.ts +++ b/src/mol-plugin-state/objects.ts @@ -22,6 +22,8 @@ import { VolumeRepresentation } from '../mol-repr/volume/representation'; import { StateObject, StateTransformer } from '../mol-state'; import { CubeFile } from '../mol-io/reader/cube/parser'; import { DxFile } from '../mol-io/reader/dx/parser'; +import { Color } from '../mol-util/color/color'; +import { Asset } from '../mol-util/assets'; export type TypeClass = 'root' | 'data' | 'prop' @@ -119,7 +121,21 @@ export namespace PluginStateObject { } export namespace Volume { + export interface LazyInfo { + url: string | Asset.Url, + isBinary: boolean, + format: string, + entryId?: string, + isovalues: { + type: 'absolute' | 'relative', + value: number, + color: Color, + alpha?: number + }[] + } + export class Data extends Create<_Volume>({ name: 'Volume', typeClass: 'Object' }) { } + export class Lazy extends Create<LazyInfo>({ name: 'Lazy Volume', typeClass: 'Object' }) { } export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>, _Volume>({ name: 'Volume 3D' }) { } } diff --git a/src/mol-plugin-state/transforms/data.ts b/src/mol-plugin-state/transforms/data.ts index fe25f66c9fe0a9e111f73ef19bc613cd69f3e452..ddd57fd112f2ecbe9c75feed3c15f392541c1785 100644 --- a/src/mol-plugin-state/transforms/data.ts +++ b/src/mol-plugin-state/transforms/data.ts @@ -19,6 +19,7 @@ import { PluginStateObject as SO, PluginStateTransform } from '../objects'; import { Asset } from '../../mol-util/assets'; import { parseCube } from '../../mol-io/reader/cube/parser'; import { parseDx } from '../../mol-io/reader/dx/parser'; +import { ColorNames } from '../../mol-util/color/names'; export { Download }; export { DownloadBlob }; @@ -35,6 +36,7 @@ export { ParseDx }; export { ImportString }; export { ImportJson }; export { ParseJson }; +export { LazyVolume }; type Download = typeof Download const Download = PluginStateTransform.BuiltIn({ @@ -441,4 +443,31 @@ const ParseJson = PluginStateTransform.BuiltIn({ return new SO.Format.Json(json); }); } -}); \ No newline at end of file +}); + +type LazyVolume = typeof LazyVolume +const LazyVolume = PluginStateTransform.BuiltIn({ + name: 'lazy-volume', + display: { name: 'Lazy Volume', description: 'A placeholder for lazy loaded volume representation' }, + from: SO.Root, + to: SO.Volume.Lazy, + params: { + url: PD.Url(''), + isBinary: PD.Boolean(false), + format: PD.Text('ccp4'), // TODO: use Select based on available formats + entryId: PD.Text(''), + isovalues: PD.ObjectList({ + type: PD.Text<'absolute' | 'relative'>('relative'), // TODO: Select + value: PD.Numeric(0), + color: PD.Color(ColorNames.black), + alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }) + }, e => `${e.type} ${e.value}`) + } +})({ + apply({ a, params }) { + return Task.create('Lazy Volume', async ctx => { + return new SO.Volume.Lazy(params, { label: `${params.entryId || params.url}`, description: 'Lazy Volume' }); + }); + } +}); + diff --git a/src/mol-plugin-ui/structure/volume.tsx b/src/mol-plugin-ui/structure/volume.tsx index 140ec6458f617ebbd398d1a83ffa86a2b9a42802..3ee109fb1ee9ab1614771078782a48aad2ea41f3 100644 --- a/src/mol-plugin-ui/structure/volume.tsx +++ b/src/mol-plugin-ui/structure/volume.tsx @@ -8,11 +8,11 @@ import * as React from 'react'; import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy'; import { VolumeHierarchyManager } from '../../mol-plugin-state/manager/volume/hierarchy'; -import { VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state'; +import { LazyVolumeRef, VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state'; import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation'; import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior'; import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers'; -import { State, StateSelection, StateTransform } from '../../mol-state'; +import { State, StateObjectCell, StateObjectSelector, StateSelection, StateTransform } from '../../mol-state'; import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base'; import { ActionMenu } from '../controls/action-menu'; import { Button, ExpandGroup, IconButton } from '../controls/common'; @@ -21,6 +21,9 @@ import { UpdateTransformControl } from '../state/update-transform'; import { BindingsHelp } from '../viewport/help'; import { PluginCommands } from '../../mol-plugin/commands'; import { BlurOnSvg, ErrorSvg, CheckSvg, AddSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg } from '../controls/icons'; +import { PluginStateObject } from '../../mol-plugin-state/objects'; +import { StateTransforms } from '../../mol-plugin-state/transforms'; +import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params'; interface VolumeStreamingControlState extends CollapsableState { isBusy: boolean @@ -104,6 +107,7 @@ export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStrea interface VolumeSourceControlState extends CollapsableState { isBusy: boolean, + loadingLabel?: string, show?: 'hierarchy' | 'add-repr' } @@ -120,18 +124,23 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo componentDidMount() { this.subscribe(this.plugin.managers.volume.hierarchy.behaviors.selection, sel => { - this.setState({ isHidden: sel.hierarchy.volumes.length === 0 }); + this.setState({ isHidden: sel.hierarchy.volumes.length === 0 && sel.hierarchy.lazyVolumes.length === 0 }); }); this.subscribe(this.plugin.behaviors.state.isBusy, v => { this.setState({ isBusy: v }); }); } - private item = (ref: VolumeRef) => { + private item = (ref: VolumeRef | LazyVolumeRef) => { const selected = this.plugin.managers.volume.hierarchy.selection; const label = ref.cell.obj?.label || 'Volume'; - const item: ActionMenu.Item = { kind: 'item', label: label || ref.kind, selected: selected === ref, value: ref }; + const item: ActionMenu.Item = { + kind: 'item', + label: (ref.kind === 'lazy-volume' ? 'Load ' : '') + (label || ref.kind), + selected: selected === ref, + value: ref + }; return item; } @@ -139,9 +148,15 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo const mng = this.plugin.managers.volume.hierarchy; const { current } = mng; const ret: ActionMenu.Items = []; - for (let ref of current.volumes) { + + for (const ref of current.volumes) { + ret.push(this.item(ref)); + } + + for (const ref of current.lazyVolumes) { ret.push(this.item(ref)); } + return ret; } @@ -158,11 +173,13 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo } get isEmpty() { - const { volumes } = this.plugin.managers.volume.hierarchy.current; - return volumes.length === 0; + const { volumes, lazyVolumes } = this.plugin.managers.volume.hierarchy.current; + return volumes.length === 0 && lazyVolumes.length === 0; } get label() { + if (this.state.loadingLabel) return `Loading ${this.state.loadingLabel}...`; + const selected = this.plugin.managers.volume.hierarchy.selection; if (!selected) return 'Nothing Selected'; return selected?.cell.obj?.label || 'Volume'; @@ -171,7 +188,45 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo selectCurrent: ActionMenu.OnSelect = (item) => { this.toggleHierarchy(); if (!item) return; - this.plugin.managers.volume.hierarchy.setCurrent(item.value as VolumeRef); + + const current = item.value as VolumeRef | LazyVolumeRef; + if (current.kind === 'volume') { + this.plugin.managers.volume.hierarchy.setCurrent(current); + } else { + this.lazyLoad(current.cell); + } + } + + private async lazyLoad(cell: StateObjectCell<PluginStateObject.Volume.Lazy>) { + const { url, isBinary, format, entryId, isovalues } = cell.obj!.data; + + this.setState({ isBusy: true, loadingLabel: cell.obj!.label }); + + try { + const plugin = this.plugin; + await plugin.dataTransaction(async () => { + const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } }); + const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId }); + const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>; + if (!volume?.isOk) throw new Error('Failed to parse any volume.'); + + const repr = plugin.build().to(volume); + for (const iso of isovalues) { + repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, { + type: 'isosurface', + typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } }, + color: 'uniform', + colorParams: { value: iso.color } + })); + } + + await repr.commit(); + + await plugin.build().delete(cell).commit(); + }); + } finally { + this.setState({ isBusy: false, loadingLabel: void 0 }); + } } selectAdd: ActionMenu.OnSelect = (item) => { @@ -186,13 +241,12 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo renderControls() { const disabled = this.state.isBusy || this.isEmpty; const label = this.label; - const selected = this.plugin.managers.volume.hierarchy.selection; return <> <div className='msp-flex-row' style={{ marginTop: '1px' }}> <Button noOverflow flex onClick={this.toggleHierarchy} disabled={disabled} title={label}>{label}</Button> - {!this.isEmpty && <IconButton svg={AddSvg} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />} + {!this.isEmpty && selected && <IconButton svg={AddSvg} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />} </div> {this.state.show === 'hierarchy' && <ActionMenu items={this.hierarchyItems} onSelect={this.selectCurrent} />} {this.state.show === 'add-repr' && <ActionMenu items={this.addActions} onSelect={this.selectAdd} />}