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} />}