Browse Source

Tensor tweaks, marching cubes

David Sehnal 7 years ago
parent
commit
fe1850012f

+ 1 - 1
src/mol-data/db/column.ts

@@ -34,7 +34,7 @@ namespace Column {
         export type Float = { '@type': 'float', T: number } & Base<'float'>
         export type Coordinate = { '@type': 'coord', T: number } & Base<'float'>
 
-        export type Tensor = { '@type': 'tensor', T: Tensors, space: Tensors.Space } & Base<'tensor'>
+        export type Tensor = { '@type': 'tensor', T: Tensors.Data, space: Tensors.Space } & Base<'tensor'>
         export type Aliased<T> = { '@type': 'aliased', T: T } & Base<'str' | 'int'>
         export type List<T extends number|string> = { '@type': 'list', T: T[], separator: string, itemParse: (x: string) => T } & Base<'list'>
 

+ 5 - 4
src/mol-data/util/chunked-array.ts

@@ -74,13 +74,14 @@ namespace ChunkedArray {
         return array.elementCount++;
     }
 
-
-    export function compact<T>(array: ChunkedArray<T>): ArrayLike<T> {
+    export function compact<T>(array: ChunkedArray<T>, doNotResizeSingleton = false): ArrayLike<T> {
         const { ctor, chunks, currentIndex } = array;
 
         if (!chunks.length) return ctor(0);
-        if (chunks.length === 1 && currentIndex === array.allocatedSize) {
-            return chunks[0];
+        if (chunks.length === 1) {
+            if (doNotResizeSingleton || currentIndex === array.allocatedSize) {
+                return chunks[0];
+            }
         }
 
         let size = 0;

+ 200 - 0
src/mol-geo/shape/surface.ts

@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export interface Surface {
+    vertexCount: number,
+    triangleCount: number,
+    vertexBuffer: Float32Array,
+    indexBuffer: Uint32Array,
+    normalBuffer?: Float32Array,
+    normalsComputed: boolean,
+
+    vertexAnnotation?: ArrayLike<number>[]
+    //boundingSphere?: { center: Geometry.LinearAlgebra.Vector3, radius: number };
+}
+
+// export namespace Surface {
+//     export function computeNormalsImmediate(surface: Surface) {
+//         if (surface.normals) return;
+
+//         const normals = new Float32Array(surface.vertices.length),
+//             v = surface.vertices, triangles = surface.triangleIndices;
+//         for (let i = 0; i < triangles.length; i += 3) {
+//             const a = 3 * triangles[i],
+//                 b = 3 * triangles[i + 1],
+//                 c = 3 * triangles[i + 2];
+
+//             const nx = v[a + 2] * (v[b + 1] - v[c + 1]) + v[b + 2] * v[c + 1] - v[b + 1] * v[c + 2] + v[a + 1] * (-v[b + 2] + v[c + 2]),
+//                 ny = -(v[b + 2] * v[c]) + v[a + 2] * (-v[b] + v[c]) + v[a] * (v[b + 2] - v[c + 2]) + v[b] * v[c + 2],
+//                 nz = v[a + 1] * (v[b] - v[c]) + v[b + 1] * v[c] - v[b] * v[c + 1] + v[a] * (-v[b + 1] + v[b + 1]);
+
+//             normals[a] += nx; normals[a + 1] += ny; normals[a + 2] += nz;
+//             normals[b] += nx; normals[b + 1] += ny; normals[b + 2] += nz;
+//             normals[c] += nx; normals[c + 1] += ny; normals[c + 2] += nz;
+//         }
+
+//         for (let i = 0; i < normals.length; i += 3) {
+//             const nx = normals[i];
+//             const ny = normals[i + 1];
+//             const nz = normals[i + 2];
+//             const f = 1.0 / Math.sqrt(nx * nx + ny * ny + nz * nz);
+//             normals[i] *= f; normals[i + 1] *= f; normals[i + 2] *= f;
+//         }
+//         surface.normals = normals;
+//     }
+
+//     export function computeNormals(surface: Surface): Computation<Surface> {
+//         return computation<Surface>(async ctx => {
+//             if (surface.normals) {
+//                 return surface;
+//             };
+
+//             await ctx.updateProgress('Computing normals...');
+//             computeNormalsImmediate(surface);
+//             return surface;
+//         });
+//     }
+
+//     function addVertex(src: Float32Array, i: number, dst: Float32Array, j: number) {
+//         dst[3 * j] += src[3 * i];
+//         dst[3 * j + 1] += src[3 * i + 1];
+//         dst[3 * j + 2] += src[3 * i + 2];
+//     }
+
+//     function laplacianSmoothIter(surface: Surface, vertexCounts: Int32Array, vs: Float32Array, vertexWeight: number) {
+//         const triCount = surface.triangleIndices.length,
+//             src = surface.vertices;
+
+//         const triangleIndices = surface.triangleIndices;
+
+//         for (let i = 0; i < triCount; i += 3) {
+//             const a = triangleIndices[i],
+//                 b = triangleIndices[i + 1],
+//                 c = triangleIndices[i + 2];
+
+//             addVertex(src, b, vs, a);
+//             addVertex(src, c, vs, a);
+
+//             addVertex(src, a, vs, b);
+//             addVertex(src, c, vs, b);
+
+//             addVertex(src, a, vs, c);
+//             addVertex(src, b, vs, c);
+//         }
+
+//         const vw = 2 * vertexWeight;
+//         for (let i = 0, _b = surface.vertexCount; i < _b; i++) {
+//             const n = vertexCounts[i] + vw;
+//             vs[3 * i] = (vs[3 * i] + vw * src[3 * i]) / n;
+//             vs[3 * i + 1] = (vs[3 * i + 1] + vw * src[3 * i + 1]) / n;
+//             vs[3 * i + 2] = (vs[3 * i + 2] + vw * src[3 * i + 2]) / n;
+//         }
+//     }
+
+//     async function laplacianSmoothComputation(ctx: Computation.Context, surface: Surface, iterCount: number, vertexWeight: number) {
+//         await ctx.updateProgress('Smoothing surface...', true);
+
+//         const vertexCounts = new Int32Array(surface.vertexCount),
+//             triCount = surface.triangleIndices.length;
+
+//         const tris = surface.triangleIndices;
+//         for (let i = 0; i < triCount; i++) {
+//             // in a triangle 2 edges touch each vertex, hence the constant.
+//             vertexCounts[tris[i]] += 2;
+//         }
+
+//         let vs = new Float32Array(surface.vertices.length);
+//         let started = Utils.PerformanceMonitor.currentTime();
+//         await ctx.updateProgress('Smoothing surface...', true);
+//         for (let i = 0; i < iterCount; i++) {
+//             if (i > 0) {
+//                 for (let j = 0, _b = vs.length; j < _b; j++) vs[j] = 0;
+//             }
+//             surface.normals = void 0;
+//             laplacianSmoothIter(surface, vertexCounts, vs, vertexWeight);
+//             const t = surface.vertices;
+//             surface.vertices = <any>vs;
+//             vs = <any>t;
+
+//             const time = Utils.PerformanceMonitor.currentTime();
+//             if (time - started > Computation.UpdateProgressDelta) {
+//                 started = time;
+//                 await ctx.updateProgress('Smoothing surface...', true, i + 1, iterCount);
+//             }
+//         }
+//         return surface;
+//     }
+
+//     /*
+//      * Smooths the vertices by averaging the neighborhood.
+//      *
+//      * Resets normals. Might replace vertex array.
+//      */
+//     export function laplacianSmooth(surface: Surface, iterCount: number = 1, vertexWeight: number = 1): Computation<Surface> {
+
+//         if (iterCount < 1) iterCount = 0;
+//         if (iterCount === 0) return Computation.resolve(surface);
+
+//         return computation(async ctx => await laplacianSmoothComputation(ctx, surface, iterCount, (1.1 * vertexWeight) / 1.1));
+//     }
+
+//     export function computeBoundingSphere(surface: Surface): Computation<Surface> {
+//         return computation<Surface>(async ctx => {
+//             if (surface.boundingSphere) {
+//                 return surface;
+//             }
+//             await ctx.updateProgress('Computing bounding sphere...');
+
+//             const vertices = surface.vertices;
+//             let x = 0, y = 0, z = 0;
+//             for (let i = 0, _c = surface.vertices.length; i < _c; i += 3) {
+//                 x += vertices[i];
+//                 y += vertices[i + 1];
+//                 z += vertices[i + 2];
+//             }
+//             x /= surface.vertexCount;
+//             y /= surface.vertexCount;
+//             z /= surface.vertexCount;
+//             let r = 0;
+//             for (let i = 0, _c = vertices.length; i < _c; i += 3) {
+//                 const dx = x - vertices[i];
+//                 const dy = y - vertices[i + 1];
+//                 const dz = z - vertices[i + 2];
+//                 r = Math.max(r, dx * dx + dy * dy + dz * dz);
+//             }
+//             surface.boundingSphere = {
+//                 center: LinearAlgebra.Vector3.fromValues(x, y, z),
+//                 radius: Math.sqrt(r)
+//             }
+//             return surface;
+//         });
+//     }
+
+//     export function transformImmediate(surface: Surface, t: number[]) {
+//         const p = LinearAlgebra.Vector3.zero();
+//         const m = LinearAlgebra.Vector3.transformMat4;
+//         const vertices = surface.vertices;
+//         for (let i = 0, _c = surface.vertices.length; i < _c; i += 3) {
+//             p[0] = vertices[i];
+//             p[1] = vertices[i + 1];
+//             p[2] = vertices[i + 2];
+//             m(p, p, t);
+//             vertices[i] = p[0];
+//             vertices[i + 1] = p[1];
+//             vertices[i + 2] = p[2];
+//         }
+//         surface.normals = void 0;
+//         surface.boundingSphere = void 0;
+//     }
+
+//     export function transform(surface: Surface, t: number[]): Computation<Surface> {
+//         return computation<Surface>(async ctx => {
+//             ctx.updateProgress('Updating surface...');
+//             transformImmediate(surface, t);
+//             return surface;
+//         });
+//     }
+// }

+ 242 - 0
src/mol-geo/util/marching-cubes/algorithm.ts

@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task, RuntimeContext } from 'mol-task'
+import { ChunkedArray } from 'mol-data/util'
+import { Tensor } from 'mol-math/linear-algebra'
+import { Surface } from '../../shape/surface'
+import { Index, EdgeIdInfo, CubeEdges, EdgeTable, TriTable } from './tables'
+
+/**
+ * The parameters required by the algorithm.
+ */
+export interface MarchingCubesParameters {
+    isoLevel: number,
+    scalarField: Tensor,
+
+    bottomLeft?: ArrayLike<number>,
+    topRight?: ArrayLike<number>,
+
+    annotationField?: Tensor,
+
+    buffers?: {
+        vertex?: Float32Array,
+        index?: Uint32Array,
+        normal?: Float32Array,
+        annotation?: ArrayLike<number>
+    }
+}
+
+export function compute(parameters: MarchingCubesParameters) {
+    return Task.create('Marching Cubes', async ctx => {
+        let comp = new MarchingCubesComputation(parameters, ctx);
+        return await comp.run();
+    });
+}
+
+class MarchingCubesComputation {
+    private size: number;
+    private sliceSize: number;
+
+    private minX = 0; private minY = 0; private minZ = 0;
+    private maxX = 0; private maxY = 0; private maxZ = 0;
+    private state: MarchingCubesState;
+
+    private async doSlices() {
+        let done = 0;
+
+        for (let k = this.minZ; k < this.maxZ; k++) {
+            this.slice(k);
+
+            done += this.sliceSize;
+            if (this.ctx.shouldUpdate) {
+                await this.ctx.update({ message: 'Computing surface...', current: done, max: this.size });
+            }
+        }
+    }
+
+    private slice(k: number) {
+        for (let j = this.minY; j < this.maxY; j++) {
+            for (let i = this.minX; i < this.maxX; i++) {
+                this.state.processCell(i, j, k);
+            }
+        }
+        this.state.clearEdgeVertexIndexSlice(k);
+    }
+
+    private finish() {
+        const vertexBuffer = ChunkedArray.compact(this.state.vertexBuffer) as Float32Array;
+        const indexBuffer = ChunkedArray.compact(this.state.triangleBuffer) as Uint32Array;
+
+        this.state.vertexBuffer = <any>void 0;
+        this.state.verticesOnEdges = <any>void 0;
+
+        let ret: Surface = {
+            triangleCount: 0,
+            vertexCount: 0,
+            vertexBuffer,
+            indexBuffer,
+            // vertexAnnotation: this.state.annotate ? ChunkedArray.compact(this.state.annotationBuffer) : void 0,
+            normalsComputed: false
+        }
+
+        return ret;
+    }
+
+    async run() {
+        await this.ctx.update({ message: 'Computing surface...', current: 0, max: this.size });
+        await this.doSlices();
+        await this.ctx.update('Finalizing...');
+        return this.finish();
+    }
+
+    constructor(
+        parameters: MarchingCubesParameters,
+        private ctx: RuntimeContext) {
+
+        let params = { ...parameters };
+
+        if (!params.bottomLeft) params.bottomLeft = [0, 0, 0];
+        if (!params.topRight) params.topRight = params.scalarField.space.dimensions;
+
+        this.state = new MarchingCubesState(params),
+            this.minX = params.bottomLeft[0]; this.minY = params.bottomLeft[1]; this.minZ = params.bottomLeft[2];
+        this.maxX = params.topRight[0] - 1; this.maxY = params.topRight[1] - 1; this.maxZ = params.topRight[2] - 1;
+
+        this.size = (this.maxX - this.minX) * (this.maxY - this.minY) * (this.maxZ - this.minZ);
+        this.sliceSize = (this.maxX - this.minX) * (this.maxY - this.minY);
+    }
+}
+
+class MarchingCubesState {
+    nX: number; nY: number; nZ: number;
+    isoLevel: number;
+    scalarFieldGet: Tensor.Space['get'];
+    scalarField: Tensor.Data;
+    annotationFieldGet?: Tensor.Space['get'];
+    annotationField?: Tensor.Data;
+    annotate: boolean;
+
+    // two layers of vertex indices. Each vertex has 3 edges associated.
+    verticesOnEdges: Int32Array;
+    vertList: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+    i: number = 0; j: number = 0; k: number = 0;
+
+    vertexBuffer: ChunkedArray<number>;
+    annotationBuffer: ChunkedArray<number>;
+    triangleBuffer: ChunkedArray<number>;
+
+    private get3dOffsetFromEdgeInfo(index: Index) {
+        return (this.nX * (((this.k + index.k) % 2) * this.nY + this.j + index.j) + this.i + index.i);
+    }
+
+    /**
+     * This clears the "vertex index buffer" for the slice that will not be accessed anymore.
+     */
+    clearEdgeVertexIndexSlice(k: number) {
+        // clear either the top or bottom half of the buffer...
+        const start = k % 2 === 0 ? 0 : 3 * this.nX * this.nY;
+        const end = k % 2 === 0 ? 3 * this.nX * this.nY : this.verticesOnEdges.length;
+        for (let i = start; i < end; i++) this.verticesOnEdges[i] = 0;
+    }
+
+    private interpolate(edgeNum: number) {
+        const info = EdgeIdInfo[edgeNum],
+            edgeId = 3 * this.get3dOffsetFromEdgeInfo(info) + info.e;
+
+        const ret = this.verticesOnEdges[edgeId];
+        if (ret > 0) return (ret - 1) | 0;
+
+        const edge = CubeEdges[edgeNum];
+        const a = edge.a, b = edge.b;
+        const li = a.i + this.i, lj = a.j + this.j, lk = a.k + this.k;
+        const hi = b.i + this.i, hj = b.j + this.j, hk = b.k + this.k;
+        const v0 = this.scalarFieldGet(this.scalarField, li, lj, lk), v1 = this.scalarFieldGet(this.scalarField, hi, hj, hk);
+        const t = (this.isoLevel - v0) / (v0 - v1);
+
+        const id = ChunkedArray.add3(
+            this.vertexBuffer,
+            li + t * (li - hi),
+            lj + t * (lj - hj),
+            lk + t * (lk - hk)) | 0;
+
+        this.verticesOnEdges[edgeId] = id + 1;
+
+        if (this.annotate) {
+            const u = this.annotationFieldGet!(this.annotationField!, li, lj, lk);
+            const v = this.annotationFieldGet!(this.annotationField!, hi, hj, hk)
+            let a = t < 0.5 ? u : v;
+            if (a < 0) a = t < 0.5 ? v : u;
+            ChunkedArray.add(this.annotationBuffer, a);
+        }
+
+        return id;
+    }
+
+    constructor(params: MarchingCubesParameters) {
+        const dims = params.scalarField.space.dimensions;
+        this.nX = dims[0]; this.nY = dims[1]; this.nZ = dims[2];
+        this.isoLevel = params.isoLevel;
+        this.scalarFieldGet = params.scalarField.space.get;
+        this.scalarField = params.scalarField.data;
+        if (params.annotationField) {
+            this.annotationField = params.annotationField.data;
+            this.annotationFieldGet = params.annotationField.space.get;
+        }
+
+        let dX = params.topRight![0] - params.bottomLeft![0], dY = params.topRight![1] - params.bottomLeft![1], dZ = params.topRight![2] - params.bottomLeft![2],
+            vertexBufferSize = Math.min(262144, Math.max(dX * dY * dZ / 16, 1024) | 0),
+            triangleBufferSize = Math.min(1 << 16, vertexBufferSize * 4);
+
+        this.vertexBuffer = ChunkedArray.create<number>(s => new Float32Array(s), 3, vertexBufferSize);
+        this.triangleBuffer = ChunkedArray.create<number>(s => new Uint32Array(s), 3, triangleBufferSize);
+
+        this.annotate = !!params.annotationField;
+        if (this.annotate) this.annotationBuffer = ChunkedArray.create(s => new Int32Array(s), 1, vertexBufferSize);
+
+        // two layers of vertex indices. Each vertex has 3 edges associated.
+        this.verticesOnEdges = new Int32Array(3 * this.nX * this.nY * 2);
+    }
+
+    get(i: number, j: number, k: number) {
+        return this.scalarFieldGet(this.scalarField, i, j, k);
+    } 
+
+    processCell(i: number, j: number, k: number) {
+        let tableIndex = 0;
+
+        if (this.get(i, j, k) < this.isoLevel) tableIndex |= 1;
+        if (this.get(i + 1, j, k) < this.isoLevel) tableIndex |= 2;
+        if (this.get(i + 1, j + 1, k) < this.isoLevel) tableIndex |= 4;
+        if (this.get(i, j + 1, k) < this.isoLevel) tableIndex |= 8;
+        if (this.get(i, j, k + 1) < this.isoLevel) tableIndex |= 16;
+        if (this.get(i + 1, j, k + 1) < this.isoLevel) tableIndex |= 32;
+        if (this.get(i + 1, j + 1, k + 1) < this.isoLevel) tableIndex |= 64;
+        if (this.get(i, j + 1, k + 1) < this.isoLevel) tableIndex |= 128;
+
+        if (tableIndex === 0 || tableIndex === 255) return;
+
+        this.i = i; this.j = j; this.k = k;
+        let edgeInfo = EdgeTable[tableIndex];
+        if ((edgeInfo & 1) > 0) this.vertList[0] = this.interpolate(0); // 0 1
+        if ((edgeInfo & 2) > 0) this.vertList[1] = this.interpolate(1); // 1 2
+        if ((edgeInfo & 4) > 0) this.vertList[2] = this.interpolate(2); // 2 3
+        if ((edgeInfo & 8) > 0) this.vertList[3] = this.interpolate(3); // 0 3
+        if ((edgeInfo & 16) > 0) this.vertList[4] = this.interpolate(4); // 4 5
+        if ((edgeInfo & 32) > 0) this.vertList[5] = this.interpolate(5); // 5 6
+        if ((edgeInfo & 64) > 0) this.vertList[6] = this.interpolate(6); // 6 7
+        if ((edgeInfo & 128) > 0) this.vertList[7] = this.interpolate(7); // 4 7
+        if ((edgeInfo & 256) > 0) this.vertList[8] = this.interpolate(8); // 0 4
+        if ((edgeInfo & 512) > 0) this.vertList[9] = this.interpolate(9); // 1 5
+        if ((edgeInfo & 1024) > 0) this.vertList[10] = this.interpolate(10); // 2 6
+        if ((edgeInfo & 2048) > 0) this.vertList[11] = this.interpolate(11); // 3 7
+
+        let triInfo = TriTable[tableIndex];
+        for (let t = 0; t < triInfo.length; t += 3) {
+            ChunkedArray.add3(this.triangleBuffer, this.vertList[triInfo[t]], this.vertList[triInfo[t + 1]], this.vertList[triInfo[t + 2]]);
+        }
+    }
+}

+ 1 - 1
src/mol-io/reader/cif/data-model.ts

@@ -78,7 +78,7 @@ export interface Field {
     toFloatArray(params?: Column.ToArrayParams<number>): ReadonlyArray<number>
 }
 
-export function getTensor(category: Category, field: string, space: Tensor.Space, row: number): Tensor {
+export function getTensor(category: Category, field: string, space: Tensor.Space, row: number): Tensor.Data {
     const ret = space.create();
     if (space.rank === 1) {
         const rows = space.dimensions[0];

+ 2 - 2
src/mol-io/reader/cif/schema.ts

@@ -71,7 +71,7 @@ function createListColumn<T extends number|string>(schema: Column.Schema.List<T>
     };
 }
 
-function createTensorColumn(schema: Column.Schema.Tensor, category: Data.Category, key: string): Column<Tensor> {
+function createTensorColumn(schema: Column.Schema.Tensor, category: Data.Category, key: string): Column<Tensor.Data> {
     const space = schema.space;
     let firstFieldName: string;
     switch (space.rank) {
@@ -82,7 +82,7 @@ function createTensorColumn(schema: Column.Schema.Tensor, category: Data.Categor
     }
     const first = category.getField(firstFieldName) || Column.Undefined(category.rowCount, schema);
     const value = (row: number) => Data.getTensor(category, key, space, row);
-    const toArray: Column<Tensor>['toArray'] = params => ColumnHelpers.createAndFillArray(category.rowCount, value, params)
+    const toArray: Column<Tensor.Data>['toArray'] = params => ColumnHelpers.createAndFillArray(category.rowCount, value, params)
 
     return {
         schema,

+ 11 - 9
src/mol-math/linear-algebra/tensor.ts

@@ -6,18 +6,20 @@
 
 import { Mat4, Vec3, Vec4 } from './3d'
 
-export interface Tensor extends Array<number> { '@type': 'tensor' }
+export interface Tensor { data: Tensor.Data, space: Tensor.Space }
 
 export namespace Tensor {
     export type ArrayCtor = { new (size: number): ArrayLike<number> }
 
+    export interface Data extends Array<number> { '@type': 'tensor' }
+
     export interface Space {
         readonly rank: number,
         readonly dimensions: ReadonlyArray<number>,
         readonly axisOrderSlowToFast: ReadonlyArray<number>,
-        create(array?: ArrayCtor): Tensor,
-        get(data: Tensor, ...coords: number[]): number
-        set(data: Tensor, ...coordsAndValue: number[]): number
+        create(array?: ArrayCtor): Tensor.Data,
+        get(data: Tensor.Data, ...coords: number[]): number
+        set(data: Tensor.Data, ...coordsAndValue: number[]): number
     }
 
     interface Layout {
@@ -49,7 +51,7 @@ export namespace Tensor {
     export function ColumnMajorMatrix(rows: number, cols: number, ctor?: ArrayCtor) { return Space([rows, cols], [1, 0], ctor); }
     export function RowMajorMatrix(rows: number, cols: number, ctor?: ArrayCtor) { return Space([rows, cols], [0, 1], ctor); }
 
-    export function toMat4(space: Space, data: Tensor): Mat4 {
+    export function toMat4(space: Space, data: Tensor.Data): Mat4 {
         if (space.rank !== 2) throw new Error('Invalid tensor rank');
         const mat = Mat4.zero();
         const d0 = Math.min(4, space.dimensions[0]), d1 = Math.min(4, space.dimensions[1]);
@@ -59,7 +61,7 @@ export namespace Tensor {
         return mat;
     }
 
-    export function toVec3(space: Space, data: Tensor): Vec3 {
+    export function toVec3(space: Space, data: Tensor.Data): Vec3 {
         if (space.rank !== 1) throw new Error('Invalid tensor rank');
         const vec = Vec3.zero();
         const d0 = Math.min(3, space.dimensions[0]);
@@ -67,7 +69,7 @@ export namespace Tensor {
         return vec;
     }
 
-    export function toVec4(space: Space, data: Tensor): Vec4 {
+    export function toVec4(space: Space, data: Tensor.Data): Vec4 {
         if (space.rank !== 1) throw new Error('Invalid tensor rank');
         const vec = Vec4.zero();
         const d0 = Math.min(4, space.dimensions[0]);
@@ -75,7 +77,7 @@ export namespace Tensor {
         return vec;
     }
 
-    export function areEqualExact(a: Tensor, b: Tensor) {
+    export function areEqualExact(a: Tensor.Data, b: Tensor.Data) {
         const len = a.length;
         if (len !== b.length) return false;
         for (let i = 0; i < len; i++) if (a[i] !== b[i]) return false;
@@ -136,7 +138,7 @@ export namespace Tensor {
         const { dimensions: ds } = layout;
         let size = 1;
         for (let i = 0, _i = ds.length; i < _i; i++) size *= ds[i];
-        return ctor => new (ctor || layout.defaultCtor)(size) as Tensor;
+        return ctor => new (ctor || layout.defaultCtor)(size) as Tensor.Data;
     }
 
     function dataOffset(layout: Layout, coord: number[]) {

+ 2 - 1
src/mol-task/execution/observable.ts

@@ -159,10 +159,11 @@ class ObservableRuntimeContext implements RuntimeContext {
             progress.message = update;
         } else {
             if (typeof update.canAbort !== 'undefined') progress.canAbort = update.canAbort;
+            if (typeof update.message !== 'undefined') progress.message = update.message;
             if (typeof update.current !== 'undefined') progress.current = update.current;
             if (typeof update.max !== 'undefined') progress.max = update.max;
-            if (typeof update.message !== 'undefined') progress.message = update.message;
             progress.isIndeterminate = typeof progress.current === 'undefined' || typeof progress.max === 'undefined';
+            if (typeof update.isIndeterminate !== 'undefined') progress.isIndeterminate = update.isIndeterminate;
         }
     }