Browse Source

volume loci, picking and repr improvements

- Volume.Loci
- Volume.Isosurface.Loci
- Volume.Cell.Loci
- picking
- wip: slice
Alexander Rose 5 years ago
parent
commit
f707cb19a4

+ 1 - 1
src/mol-geo/geometry/image/image.ts

@@ -116,7 +116,7 @@ namespace Image {
 
     export const Params = {
         ...BaseGeometry.Params,
-        interpolation: PD.Select('bspline', PD.objectToOptions(InterpolationTypes)),
+        interpolation: PD.Select('bspline', PD.objectToOptions(InterpolationTypes), { isEssential: true }),
     };
     export type Params = typeof Params
 

+ 8 - 1
src/mol-gl/shader/image.frag.ts

@@ -8,12 +8,16 @@ precision highp float;
 precision highp int;
 
 #include common
+#include read_from_texture
 #include common_frag_params
 
 uniform vec2 uImageTexDim;
 uniform sampler2D tImageTex;
 uniform sampler2D tGroupTex;
 
+uniform vec2 uMarkerTexDim;
+uniform sampler2D tMarker;
+
 varying vec2 vUv;
 varying float vInstance;
 
@@ -99,7 +103,7 @@ void main() {
             #elif defined(dRenderVariant_pickInstance)
                 gl_FragColor = vec4(encodeFloatRGB(vInstance), 1.0);
             #elif defined(dRenderVariant_pickGroup)
-                float group = texture2D(tGroupTex, vUv).a;
+                float group = texture2D(tGroupTex, vUv).r;
                 gl_FragColor = vec4(encodeFloatRGB(group), 1.0);
             #endif
         } else {
@@ -121,6 +125,9 @@ void main() {
         gl_FragColor = imageData;
         gl_FragColor.a *= uAlpha;
 
+        float group = texture2D(tGroupTex, vUv).r;
+        float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+        #include apply_marker_color
         #include apply_fog
     #endif
 }

+ 31 - 1
src/mol-model/loci.ts

@@ -16,6 +16,8 @@ import { shallowEqual } from '../mol-util';
 import { FiniteArray } from '../mol-util/type-helpers';
 import { BoundaryHelper } from '../mol-math/geometry/boundary-helper';
 import { stringToWords } from '../mol-util/string';
+import { Volume } from './volume/volume';
+import { VolumeData } from './volume';
 
 /** A Loci that includes every loci */
 export const EveryLoci = { kind: 'every-loci' as 'every-loci' };
@@ -62,7 +64,7 @@ export function DataLoci<T = unknown, E = unknown>(tag: string, data: T, element
 
 export { Loci };
 
-type Loci = StructureElement.Loci | Structure.Loci | Bond.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci | ShapeGroup.Loci
+type Loci = StructureElement.Loci | Structure.Loci | Bond.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci | ShapeGroup.Loci | Volume.Loci | Volume.Isosurface.Loci | Volume.Cell.Loci
 
 namespace Loci {
     export interface Bundle<L extends number> { loci: FiniteArray<Loci, L> }
@@ -98,6 +100,15 @@ namespace Loci {
         if (ShapeGroup.isLoci(lociA) && ShapeGroup.isLoci(lociB)) {
             return ShapeGroup.areLociEqual(lociA, lociB);
         }
+        if (Volume.isLoci(lociA) && Volume.isLoci(lociB)) {
+            return Volume.areLociEqual(lociA, lociB);
+        }
+        if (Volume.Isosurface.isLoci(lociA) && Volume.Isosurface.isLoci(lociB)) {
+            return Volume.Isosurface.areLociEqual(lociA, lociB);
+        }
+        if (Volume.Cell.isLoci(lociA) && Volume.Cell.isLoci(lociB)) {
+            return Volume.Cell.areLociEqual(lociA, lociB);
+        }
         return false;
     }
 
@@ -114,6 +125,9 @@ namespace Loci {
         if (Bond.isLoci(loci)) return Bond.isLociEmpty(loci);
         if (Shape.isLoci(loci)) return Shape.isLociEmpty(loci);
         if (ShapeGroup.isLoci(loci)) return ShapeGroup.isLociEmpty(loci);
+        if (Volume.isLoci(loci)) return Volume.isLociEmpty(loci);
+        if (Volume.Isosurface.isLoci(loci)) return Volume.Isosurface.isLociEmpty(loci);
+        if (Volume.Cell.isLoci(loci)) return Volume.Cell.isLociEmpty(loci);
         return false;
     }
 
@@ -147,6 +161,13 @@ namespace Loci {
             return ShapeGroup.getBoundingSphere(loci, boundingSphere);
         } else if (loci.kind === 'data-loci') {
             return loci.getBoundingSphere(boundingSphere);
+        } else if (loci.kind === 'volume-loci') {
+            return VolumeData.getBoundingSphere(loci.volume, boundingSphere);
+        } else if (loci.kind === 'isosurface-loci') {
+            return VolumeData.getBoundingSphere(loci.volume, boundingSphere);
+        } else if (loci.kind === 'cell-loci') {
+            // TODO
+            return VolumeData.getBoundingSphere(loci.volume, boundingSphere);
         }
     }
 
@@ -175,6 +196,15 @@ namespace Loci {
         } else if (loci.kind === 'data-loci') {
             // TODO maybe add loci.getPrincipalAxes()???
             return void 0;
+        } else if (loci.kind === 'volume-loci') {
+            // TODO
+            return void 0;
+        } else if (loci.kind === 'isosurface-loci') {
+            // TODO
+            return void 0;
+        } else if (loci.kind === 'cell-loci') {
+            // TODO
+            return void 0;
         }
     }
 

+ 1 - 1
src/mol-model/shape/shape.ts

@@ -100,7 +100,7 @@ export namespace Shape {
     export function Loci(shape: Shape): Loci { return { kind: 'shape-loci', shape }; }
     export function isLoci(x: any): x is Loci { return !!x && x.kind === 'shape-loci'; }
     export function areLociEqual(a: Loci, b: Loci) { return a.shape === b.shape; }
-    export function isLociEmpty(loci: Loci) { return loci.shape.groupCount === 0 ? true : false; }
+    export function isLociEmpty(loci: Loci) { return loci.shape.groupCount === 0; }
 }
 
 export namespace ShapeGroup {

+ 7 - 1
src/mol-model/volume/data.ts

@@ -5,7 +5,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { SpacegroupCell, Box3D } from '../../mol-math/geometry';
+import { SpacegroupCell, Box3D, Sphere3D } from '../../mol-math/geometry';
 import { Tensor, Mat4, Vec3 } from '../../mol-math/linear-algebra';
 import { equalEps } from '../../mol-math/linear-algebra/3d/common';
 
@@ -42,6 +42,12 @@ namespace VolumeData {
     export function areEquivalent(volA: VolumeData, volB: VolumeData) {
         return volA === volB;
     }
+
+    export function getBoundingSphere(volume: VolumeData, boundingSphere?: Sphere3D) {
+        if (!boundingSphere) boundingSphere = Sphere3D();
+        // TODO
+        return boundingSphere;
+    }
 }
 
 type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative

+ 34 - 0
src/mol-model/volume/volume.ts

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { VolumeData, VolumeIsoValue } from './data';
+import { OrderedSet } from '../../mol-data/int';
+
+export namespace Volume {
+    export type CellIndex = { readonly '@type': 'cell-index' } & number
+
+    export interface Loci { readonly kind: 'volume-loci', readonly volume: VolumeData }
+    export function Loci(volume: VolumeData): Loci { return { kind: 'volume-loci', volume }; }
+    export function isLoci(x: any): x is Loci { return !!x && x.kind === 'volume-loci'; }
+    export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume; }
+    export function isLociEmpty(loci: Loci) { return loci.volume.data.data.length === 0; }
+
+    export namespace Isosurface {
+        export interface Loci { readonly kind: 'isosurface-loci', readonly volume: VolumeData, readonly isoValue: VolumeIsoValue }
+        export function Loci(volume: VolumeData, isoValue: VolumeIsoValue): Loci { return { kind: 'isosurface-loci', volume, isoValue }; }
+        export function isLoci(x: any): x is Loci { return !!x && x.kind === 'isosurface-loci'; }
+        export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume && VolumeIsoValue.areSame(a.isoValue, b.isoValue, a.volume.dataStats); }
+        export function isLociEmpty(loci: Loci) { return loci.volume.data.data.length === 0; }
+    }
+
+    export namespace Cell {
+        export interface Loci { readonly kind: 'cell-loci', readonly volume: VolumeData, readonly indices: OrderedSet<CellIndex> }
+        export function Loci(volume: VolumeData, indices: OrderedSet<CellIndex>): Loci { return { kind: 'cell-loci', volume, indices }; }
+        export function isLoci(x: any): x is Loci { return !!x && x.kind === 'cell-loci'; }
+        export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume && OrderedSet.areEqual(a.indices, b.indices); }
+        export function isLociEmpty(loci: Loci) { return OrderedSet.size(loci.indices) === 0; }
+    }
+}

+ 8 - 3
src/mol-repr/volume/direct-volume.ts

@@ -20,6 +20,7 @@ import { NullLocation } from '../../mol-model/location';
 import { EmptyLoci } from '../../mol-model/loci';
 import { VisualUpdateState } from '../util';
 import { RepresentationContext, RepresentationParamsGetter } from '../representation';
+import { Volume } from '../../mol-model/volume/volume';
 
 function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
     const bbox = Box3D.empty();
@@ -147,8 +148,12 @@ export async function createDirectVolume(ctx: VisualContext, volume: VolumeData,
     if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in props');
 
     return webgl.isWebGL2 ?
-        await createDirectVolume3d(runtime, webgl, volume, directVolume) :
-        await createDirectVolume2d(runtime, webgl, volume, directVolume);
+        createDirectVolume3d(runtime, webgl, volume, directVolume) :
+        createDirectVolume2d(runtime, webgl, volume, directVolume);
+}
+
+function getLoci(volume: VolumeData, props: PD.Values<DirectVolumeParams>) {
+    return Volume.Loci(volume);
 }
 
 //
@@ -176,7 +181,7 @@ export function DirectVolumeVisual(materialId: number): VolumeVisual<DirectVolum
 }
 
 export function DirectVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, DirectVolumeParams>): VolumeRepresentation<DirectVolumeParams> {
-    return VolumeRepresentation('Direct Volume', ctx, getParams, DirectVolumeVisual);
+    return VolumeRepresentation('Direct Volume', ctx, getParams, DirectVolumeVisual, getLoci);
 }
 
 export const DirectVolumeRepresentationProvider = VolumeRepresentationProvider({

+ 57 - 12
src/mol-repr/volume/isosurface.ts

@@ -14,11 +14,16 @@ import { computeMarchingCubesMesh, computeMarchingCubesLines } from '../../mol-g
 import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
 import { LocationIterator } from '../../mol-geo/util/location-iterator';
 import { NullLocation } from '../../mol-model/location';
-import { EmptyLoci } from '../../mol-model/loci';
 import { VisualUpdateState } from '../util';
 import { Lines } from '../../mol-geo/geometry/lines/lines';
 import { RepresentationContext, RepresentationParamsGetter, Representation } from '../representation';
 import { toPrecision } from '../../mol-util/number';
+import { Volume } from '../../mol-model/volume/volume';
+import { PickingId } from '../../mol-geo/geometry/picking';
+import { EmptyLoci, Loci } from '../../mol-model/loci';
+import { Interval, OrderedSet } from '../../mol-data/int';
+import { Tensor } from '../../mol-math/linear-algebra';
+import { fillSerial } from '../../mol-util/array';
 
 const defaultStats: VolumeData['dataStats'] = { min: -1, max: 1, mean: 0, sigma: 0.1  };
 export function createIsoValueParam(defaultValue: VolumeIsoValue, stats?: VolumeData['dataStats']) {
@@ -67,20 +72,57 @@ export const VolumeIsosurfaceParams = {
 export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
 export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
 
+function getLoci(volume: VolumeData, props: VolumeIsosurfaceProps) {
+    return Volume.Isosurface.Loci(volume, props.isoValue);
+}
+
+function getIsosurfaceLoci(pickingId: PickingId, volume: VolumeData, props: VolumeIsosurfaceProps, id: number) {
+    const { objectId, groupId } = pickingId;
+    if (id === objectId) {
+        return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
+    }
+    return EmptyLoci;
+}
+
+function eachIsosurface(loci: Loci, volume: VolumeData, props: VolumeIsosurfaceProps, apply: (interval: Interval) => boolean) {
+    let changed = false;
+    if (Volume.isLoci(loci)) {
+        if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
+        if (apply(Interval.ofLength(volume.data.data.length))) changed = true;
+    } else if (Volume.Isosurface.isLoci(loci)) {
+        if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
+        if (!VolumeIsoValue.areSame(loci.isoValue, props.isoValue, volume.dataStats)) return false;
+        if (apply(Interval.ofLength(volume.data.data.length))) changed = true;
+    } else if (Volume.Cell.isLoci(loci)) {
+        if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
+        if (Interval.is(loci.indices)) {
+            if (apply(loci.indices)) changed = true;
+        } else {
+            OrderedSet.forEach(loci.indices, v => {
+                if (apply(Interval.ofSingleton(v))) changed = true;
+            });
+        }
+    }
+    return changed;
+}
+
 //
 
 export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: VolumeData, theme: Theme, props: VolumeIsosurfaceProps, mesh?: Mesh) {
     ctx.runtime.update({ message: 'Marching cubes...' });
 
+    const ids = fillSerial(new Int32Array(volume.data.data.length));
+
     const surface = await computeMarchingCubesMesh({
         isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
-        scalarField: volume.data
+        scalarField: volume.data,
+        idField: Tensor.create(volume.data.space, Tensor.Data1(ids))
     }, mesh).runAsChild(ctx.runtime);
 
     const transform = VolumeData.getGridToCartesianTransform(volume);
     ctx.runtime.update({ message: 'Transforming mesh...' });
     Mesh.transform(surface, transform);
-
+    console.log(surface, Tensor.create(volume.data.space, Tensor.Data1(ids)));
     return surface;
 }
 
@@ -94,9 +136,9 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
     return VolumeVisual<Mesh, IsosurfaceMeshParams>({
         defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
         createGeometry: createVolumeIsosurfaceMesh,
-        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
-        getLoci: () => EmptyLoci,
-        eachLocation: () => false,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(volume.data.data.length, 1, () => NullLocation),
+        getLoci: getIsosurfaceLoci,
+        eachLocation: eachIsosurface,
         setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
             if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true;
         },
@@ -109,9 +151,12 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
 export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume: VolumeData, theme: Theme, props: VolumeIsosurfaceProps, lines?: Lines) {
     ctx.runtime.update({ message: 'Marching cubes...' });
 
+    const ids = fillSerial(new Int32Array(volume.data.data.length));
+
     const wireframe = await computeMarchingCubesLines({
         isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
-        scalarField: volume.data
+        scalarField: volume.data,
+        idField: Tensor.create(volume.data.space, Tensor.Data1(ids))
     }, lines).runAsChild(ctx.runtime);
 
     const transform = VolumeData.getGridToCartesianTransform(volume);
@@ -131,9 +176,9 @@ export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<Isos
     return VolumeVisual<Lines, IsosurfaceWireframeParams>({
         defaultProps: PD.getDefaultValues(IsosurfaceWireframeParams),
         createGeometry: createVolumeIsosurfaceWireframe,
-        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
-        getLoci: () => EmptyLoci,
-        eachLocation: () => false,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(volume.data.data.length, 1, () => NullLocation),
+        getLoci: getIsosurfaceLoci,
+        eachLocation: eachIsosurface,
         setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<IsosurfaceWireframeParams>, currentProps: PD.Values<IsosurfaceWireframeParams>) => {
             if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true;
         },
@@ -144,8 +189,8 @@ export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<Isos
 //
 
 const IsosurfaceVisuals = {
-    'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceMeshVisual),
-    'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual),
+    'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceMeshVisual, getLoci),
+    'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual, getLoci),
 };
 
 export const IsosurfaceParams = {

+ 10 - 12
src/mol-repr/volume/representation.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -44,8 +44,8 @@ interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
     defaultProps: PD.Values<P>
     createGeometry(ctx: VisualContext, volume: VolumeData, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
     createLocationIterator(volume: VolumeData): LocationIterator
-    getLoci(pickingId: PickingId, id: number): Loci
-    eachLocation(loci: Loci, apply: (interval: Interval) => boolean): boolean
+    getLoci(pickingId: PickingId, volume: VolumeData, props: PD.Values<P>, id: number): Loci
+    eachLocation(loci: Loci, volume: VolumeData, props: PD.Values<P>, apply: (interval: Interval) => boolean): boolean
     setUpdateState(state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
 }
 
@@ -151,7 +151,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
         if (isEveryLoci(loci)) {
             return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
         } else {
-            return eachLocation(loci, apply);
+            return eachLocation(loci, currentVolume, currentProps, apply);
         }
     }
 
@@ -168,7 +168,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
             }
         },
         getLoci(pickingId: PickingId) {
-            return renderObject ? getLoci(pickingId, renderObject.id) : EmptyLoci;
+            return renderObject ? getLoci(pickingId, currentVolume, currentProps, renderObject.id) : EmptyLoci;
         },
         mark(loci: Loci, action: MarkerAction) {
             return Visual.mark(renderObject, loci, action, lociApply);
@@ -210,7 +210,7 @@ export const VolumeParams = {
 };
 export type VolumeParams = typeof VolumeParams
 
-export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (materialId: number) => VolumeVisual<P>): VolumeRepresentation<P> {
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (materialId: number) => VolumeVisual<P>, getLoci: (volume: VolumeData, props: PD.Values<P>) => Loci): VolumeRepresentation<P> {
     let version = 0;
     const updated = new Subject<number>();
     const materialId = getNextMaterialId();
@@ -243,11 +243,6 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         });
     }
 
-    function getLoci(pickingId?: PickingId) {
-        if (pickingId === undefined) return EmptyLoci; // TODO add Volume.Loci when available
-        return visual ? visual.getLoci(pickingId) : EmptyLoci;
-    }
-
     function mark(loci: Loci, action: MarkerAction) {
         return visual ? visual.mark(loci, action) : false;
     }
@@ -285,7 +280,10 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         createOrUpdate,
         setState,
         setTheme,
-        getLoci,
+        getLoci: (pickingId?: PickingId): Loci => {
+            if (pickingId === undefined) return getLoci(_volume, _props);
+            return visual ? visual.getLoci(pickingId) : EmptyLoci;
+        },
         mark,
         destroy
     };

+ 83 - 11
src/mol-repr/volume/slice.ts

@@ -12,26 +12,48 @@ import { VolumeData } from '../../mol-model/volume';
 import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
 import { LocationIterator } from '../../mol-geo/util/location-iterator';
 import { VisualUpdateState } from '../util';
-import { EmptyLoci } from '../../mol-model/loci';
 import { NullLocation } from '../../mol-model/location';
 import { RepresentationContext, RepresentationParamsGetter } from '../representation';
 import { VisualContext } from '../visual';
+import { Volume } from '../../mol-model/volume/volume';
+import { PickingId } from '../../mol-geo/geometry/picking';
+import { EmptyLoci, Loci } from '../../mol-model/loci';
+import { Interval, OrderedSet } from '../../mol-data/int';
+import { fillSerial } from '../../mol-util/array';
 
 export async function createImage(ctx: VisualContext, volume: VolumeData, theme: Theme, props: PD.Values<SliceParams>, image?: Image) {
+    const dim = parseInt(props.dimension.name.toString());
+    // const index = props.dimension.params;
+
     const { space } = volume.data;
 
-    const width = space.dimensions[0], height = space.dimensions[1];
+    let width: number, height: number;
+    if (dim === 0) {
+        width = space.dimensions[1];
+        height = space.dimensions[2];
+    } else if (dim === 1) {
+        width = space.dimensions[0];
+        height = space.dimensions[2];
+    } else {
+        width = space.dimensions[0];
+        height = space.dimensions[1];
+    }
+
     const n = width * height;
 
     // TODO fill with volume data values
     const imageTexture = { width, height, array: new Float32Array(n * 4) };
-    for (let i = 0, il = n * 4; i < il; ++i) {
-        imageTexture.array[i] = Math.random();
+
+    for (let i = 0, il = n * 4; i < il; i += 4) {
+        imageTexture.array[i] = 0;
+        imageTexture.array[i + 1] = Math.random();
+        imageTexture.array[i + 2] = Math.random();
+        imageTexture.array[i + 3] = 1;
     }
 
     // TODO fill with linearized index into volume
     //      (to be used for picking which needs a volume location/loci)
-    const groupTexture = { width, height, array: new Float32Array(n * 1) };
+    const groupTexture = { width, height, array: fillSerial(new Float32Array(n)) };
 
     // TODO four corners of a plane
     const corners = new Float32Array([
@@ -44,32 +66,82 @@ export async function createImage(ctx: VisualContext, volume: VolumeData, theme:
     return Image.create(imageTexture, corners, groupTexture, image);
 }
 
+function getLoci(volume: VolumeData, props: PD.Values<SliceParams>) {
+    // TODO only slice indices
+    return Volume.Loci(volume);
+}
+
+function getSliceLoci(pickingId: PickingId, volume: VolumeData, props: PD.Values<SliceParams>, id: number) {
+    const { objectId, groupId } = pickingId;
+    if (id === objectId) {
+        return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
+    }
+    return EmptyLoci;
+}
+
+function eachSlice(loci: Loci, volume: VolumeData, props: PD.Values<SliceParams>, apply: (interval: Interval) => boolean) {
+    let changed = false;
+    if (Volume.isLoci(loci)) {
+        if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
+        if (apply(Interval.ofLength(volume.data.data.length))) changed = true;
+    } else if (Volume.Isosurface.isLoci(loci)) {
+        if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
+        // TODO check isoValue
+        if (apply(Interval.ofLength(volume.data.data.length))) changed = true;
+    } else if (Volume.Cell.isLoci(loci)) {
+        if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
+        if (Interval.is(loci.indices)) {
+            if (apply(loci.indices)) changed = true;
+        } else {
+            OrderedSet.forEach(loci.indices, v => {
+                if (apply(Interval.ofSingleton(v))) changed = true;
+            });
+        }
+    }
+    return changed;
+}
+
 //
 
 export const SliceParams = {
     ...BaseGeometry.Params,
-    ...Image.Params
+    ...Image.Params,
+    dimension: PD.MappedStatic(0, {
+        0: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
+        1: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
+        2: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
+    }, { isEssential: true }),
 };
 export type SliceParams = typeof SliceParams
 export function getSliceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
-    return PD.clone(SliceParams);
+    const p = PD.clone(SliceParams);
+    p.dimension = PD.MappedStatic(0, {
+        0: PD.Numeric(0, { min: 0, max: volume.data.space.dimensions[0], step: 1 }),
+        1: PD.Numeric(0, { min: 0, max: volume.data.space.dimensions[1], step: 1 }),
+        2: PD.Numeric(0, { min: 0, max: volume.data.space.dimensions[2], step: 1 }),
+    }, { isEssential: true });
+    return p;
 }
 
 export function SliceVisual(materialId: number): VolumeVisual<SliceParams> {
     return VolumeVisual<Image, SliceParams>({
         defaultProps: PD.getDefaultValues(SliceParams),
         createGeometry: createImage,
-        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
-        getLoci: () => EmptyLoci,
-        eachLocation: () => false,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(volume.data.data.length, 1, () => NullLocation),
+        getLoci: getSliceLoci,
+        eachLocation: eachSlice,
         setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<SliceParams>, currentProps: PD.Values<SliceParams>) => {
+            state.createGeometry = (
+                newProps.dimension.name !== currentProps.dimension.name ||
+                newProps.dimension.params !== currentProps.dimension.params
+            );
         },
         geometryUtils: Image.Utils
     }, materialId);
 }
 
 export function SliceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, SliceParams>): VolumeRepresentation<SliceParams> {
-    return VolumeRepresentation('Slice', ctx, getParams, SliceVisual);
+    return VolumeRepresentation('Slice', ctx, getParams, SliceVisual, getLoci);
 }
 
 export const SliceRepresentationProvider = VolumeRepresentationProvider({

+ 15 - 0
src/mol-theme/label.ts

@@ -12,6 +12,7 @@ import { capitalize, stripTags } from '../mol-util/string';
 import { Column } from '../mol-data/db';
 import { Vec3 } from '../mol-math/linear-algebra';
 import { radToDeg } from '../mol-math/misc';
+import { VolumeIsoValue } from '../mol-model/volume';
 
 export type LabelGranularity = 'element' | 'conformation' | 'residue' | 'chain' | 'structure'
 
@@ -45,6 +46,20 @@ export function lociLabel(loci: Loci, options: Partial<LabelOptions> = {}): stri
             return 'Nothing';
         case 'data-loci':
             return loci.getLabel();
+        case 'volume-loci':
+            return loci.volume.label || 'Volume';
+        case 'isosurface-loci':
+            return [
+                `${loci.volume.label || 'Volume'}`,
+                `Isosurface at ${VolumeIsoValue.toString(loci.isoValue)}`
+            ].join(' | ');
+        case 'cell-loci':
+            const size = OrderedSet.size(loci.indices);
+            const start = OrderedSet.start(loci.indices);
+            return [
+                `${loci.volume.label || 'Volume'}`,
+                `${size === 1 ? `Cell #${start}` : `${size} Cells`}`
+            ].join(' | ');
     }
 }