Browse Source

Merge branch 'master' of https://github.com/molstar/molstar

David Sehnal 4 years ago
parent
commit
be07c1668f
38 changed files with 697 additions and 514 deletions
  1. 17 10
      src/mol-data/util/chunked-array.ts
  2. 14 9
      src/mol-geo/geometry/lines/lines-builder.ts
  3. 109 97
      src/mol-geo/geometry/mesh/builder/sheet.ts
  4. 65 52
      src/mol-geo/geometry/mesh/builder/tube.ts
  5. 50 40
      src/mol-geo/geometry/mesh/mesh-builder.ts
  6. 6 2
      src/mol-geo/geometry/points/points-builder.ts
  7. 10 5
      src/mol-geo/geometry/spheres/spheres-builder.ts
  8. 32 27
      src/mol-geo/geometry/text/text-builder.ts
  9. 1 1
      src/mol-geo/util/marching-cubes/algorithm.ts
  10. 22 0
      src/mol-math/approx.ts
  11. 1 6
      src/mol-math/geometry/boundary-helper.ts
  12. 34 4
      src/mol-math/geometry/boundary.ts
  13. 3 2
      src/mol-math/geometry/gaussian-density/cpu.ts
  14. 11 0
      src/mol-math/geometry/primitives/box3d.ts
  15. 18 10
      src/mol-math/geometry/symmetry-operator.ts
  16. 3 0
      src/mol-model/structure/model/properties/atomic/hierarchy.ts
  17. 19 10
      src/mol-model/structure/model/properties/utils/atomic-derived.ts
  18. 4 0
      src/mol-model/structure/model/types.ts
  19. 18 7
      src/mol-model/structure/structure/unit.ts
  20. 1 1
      src/mol-plugin-state/animation/built-in.ts
  21. 1 1
      src/mol-plugin-state/manager/camera.ts
  22. 34 12
      src/mol-plugin-state/manager/structure/focus.ts
  23. 0 47
      src/mol-plugin-state/transforms/model.ts
  24. 10 20
      src/mol-plugin-state/transforms/representation.ts
  25. 2 1
      src/mol-plugin/behavior/dynamic/representation.ts
  26. 3 14
      src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts
  27. 3 14
      src/mol-repr/structure/visual/bond-inter-unit-line.ts
  28. 11 17
      src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts
  29. 11 17
      src/mol-repr/structure/visual/bond-intra-unit-line.ts
  30. 15 10
      src/mol-repr/structure/visual/element-point.ts
  31. 1 1
      src/mol-repr/structure/visual/polymer-trace-mesh.ts
  32. 1 1
      src/mol-repr/structure/visual/polymer-tube-mesh.ts
  33. 51 0
      src/mol-repr/structure/visual/util/bond.ts
  34. 5 3
      src/mol-repr/structure/visual/util/common.ts
  35. 41 22
      src/mol-repr/structure/visual/util/element.ts
  36. 28 22
      src/mol-repr/structure/visual/util/link.ts
  37. 37 25
      src/mol-repr/structure/visual/util/polymer/curve-segment.ts
  38. 5 4
      src/mol-state/state.ts

+ 17 - 10
src/mol-data/util/chunked-array.ts

@@ -47,33 +47,40 @@ namespace ChunkedArray {
     export function add4<T>(array: ChunkedArray<T, 4>, x: T, y: T, z: T, w: T) {
         if (array.currentIndex >= array.currentSize) allocateNext(array);
         const c = array.currentChunk;
-        c[array.currentIndex++] = x;
-        c[array.currentIndex++] = y;
-        c[array.currentIndex++] = z;
-        c[array.currentIndex++] = w;
+        const i = array.currentIndex;
+        c[i] = x;
+        c[i + 1] = y;
+        c[i + 2] = z;
+        c[i + 3] = w;
+        array.currentIndex += 4;
         return array.elementCount++;
     }
 
     export function add3<T>(array: ChunkedArray<T, 3>, x: T, y: T, z: T) {
         if (array.currentIndex >= array.currentSize) allocateNext(array);
         const c = array.currentChunk;
-        c[array.currentIndex++] = x;
-        c[array.currentIndex++] = y;
-        c[array.currentIndex++] = z;
+        const i = array.currentIndex;
+        c[i] = x;
+        c[i + 1] = y;
+        c[i + 2] = z;
+        array.currentIndex += 3;
         return array.elementCount++;
     }
 
     export function add2<T>(array: ChunkedArray<T, 2>, x: T, y: T) {
         if (array.currentIndex >= array.currentSize) allocateNext(array);
         const c = array.currentChunk;
-        c[array.currentIndex++] = x;
-        c[array.currentIndex++] = y;
+        const i = array.currentIndex;
+        c[i] = x;
+        c[i + 1] = y;
+        array.currentIndex += 2;
         return array.elementCount++;
     }
 
     export function add<T>(array: ChunkedArray<T, 1>, x: T) {
         if (array.currentIndex >= array.currentSize) allocateNext(array);
-        array.currentChunk[array.currentIndex++] = x;
+        array.currentChunk[array.currentIndex] = x;
+        array.currentIndex += 1;
         return array.elementCount++;
     }
 

+ 14 - 9
src/mol-geo/geometry/lines/lines-builder.ts

@@ -21,6 +21,11 @@ const tmpVecA = Vec3();
 const tmpVecB = Vec3();
 const tmpDir = Vec3();
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const caAdd = ChunkedArray.add;
+const caAdd2 = ChunkedArray.add2;
+const caAdd3 = ChunkedArray.add3;
+
 export namespace LinesBuilder {
     export function create(initialCount = 2048, chunkSize = 1024, lines?: Lines): LinesBuilder {
         const mappings = ChunkedArray.create(Float32Array, 2, chunkSize, lines ? lines.mappingBuffer.ref.value : initialCount);
@@ -32,16 +37,16 @@ export namespace LinesBuilder {
         const add = (startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number) => {
             const offset = mappings.elementCount;
             for (let i = 0; i < 4; ++i) {
-                ChunkedArray.add3(starts, startX, startY, startZ);
-                ChunkedArray.add3(ends, endX, endY, endZ);
-                ChunkedArray.add(groups, group);
+                caAdd3(starts, startX, startY, startZ);
+                caAdd3(ends, endX, endY, endZ);
+                caAdd(groups, group);
             }
-            ChunkedArray.add2(mappings, -1, 1);
-            ChunkedArray.add2(mappings, -1, -1);
-            ChunkedArray.add2(mappings, 1, 1);
-            ChunkedArray.add2(mappings, 1, -1);
-            ChunkedArray.add3(indices, offset, offset + 1, offset + 2);
-            ChunkedArray.add3(indices, offset + 1, offset + 3, offset + 2);
+            caAdd2(mappings, -1, 1);
+            caAdd2(mappings, -1, -1);
+            caAdd2(mappings, 1, 1);
+            caAdd2(mappings, 1, -1);
+            caAdd3(indices, offset, offset + 1, offset + 2);
+            caAdd3(indices, offset + 1, offset + 3, offset + 2);
         };
 
         const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, group: number) => {

+ 109 - 97
src/mol-geo/geometry/mesh/builder/sheet.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>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -9,65 +9,77 @@ import { Vec3 } from '../../../../mol-math/linear-algebra';
 import { ChunkedArray } from '../../../../mol-data/util';
 import { MeshBuilder } from '../mesh-builder';
 
-const tA = Vec3.zero();
-const tB = Vec3.zero();
-const tV = Vec3.zero();
-
-const horizontalVector = Vec3.zero();
-const verticalVector = Vec3.zero();
-const verticalRightVector = Vec3.zero();
-const verticalLeftVector = Vec3.zero();
-const normalOffset = Vec3.zero();
-const positionVector = Vec3.zero();
-const normalVector = Vec3.zero();
-const torsionVector = Vec3.zero();
-
-const p1 = Vec3.zero();
-const p2 = Vec3.zero();
-const p3 = Vec3.zero();
-const p4 = Vec3.zero();
+const tA = Vec3();
+const tB = Vec3();
+const tV = Vec3();
+
+const horizontalVector = Vec3();
+const verticalVector = Vec3();
+const verticalRightVector = Vec3();
+const verticalLeftVector = Vec3();
+const normalOffset = Vec3();
+const positionVector = Vec3();
+const normalVector = Vec3();
+const torsionVector = Vec3();
+
+const p1 = Vec3();
+const p2 = Vec3();
+const p3 = Vec3();
+const p4 = Vec3();
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3fromArray = Vec3.fromArray;
+const v3scale = Vec3.scale;
+const v3add = Vec3.add;
+const v3sub = Vec3.sub;
+const v3magnitude = Vec3.magnitude;
+const v3negate = Vec3.negate;
+const v3copy = Vec3.copy;
+const v3cross = Vec3.cross;
+const caAdd3 = ChunkedArray.add3;
+const caAdd = ChunkedArray.add;
 
 function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
     const { vertices, normals, indices } = state;
     const vertexCount = vertices.elementCount;
 
-    Vec3.fromArray(verticalLeftVector, normalVectors, offset);
-    Vec3.scale(verticalLeftVector, verticalLeftVector, leftHeight);
+    v3fromArray(verticalLeftVector, normalVectors, offset);
+    v3scale(verticalLeftVector, verticalLeftVector, leftHeight);
 
-    Vec3.fromArray(verticalRightVector, normalVectors, offset);
-    Vec3.scale(verticalRightVector, verticalRightVector, rightHeight);
+    v3fromArray(verticalRightVector, normalVectors, offset);
+    v3scale(verticalRightVector, verticalRightVector, rightHeight);
 
-    Vec3.fromArray(horizontalVector, binormalVectors, offset);
-    Vec3.scale(horizontalVector, horizontalVector, width);
+    v3fromArray(horizontalVector, binormalVectors, offset);
+    v3scale(horizontalVector, horizontalVector, width);
 
-    Vec3.fromArray(positionVector, controlPoints, offset);
+    v3fromArray(positionVector, controlPoints, offset);
 
-    Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalRightVector);
-    Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalLeftVector);
-    Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalLeftVector);
-    Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalRightVector);
+    v3add(p1, v3add(p1, positionVector, horizontalVector), verticalRightVector);
+    v3sub(p2, v3add(p2, positionVector, horizontalVector), verticalLeftVector);
+    v3sub(p3, v3sub(p3, positionVector, horizontalVector), verticalLeftVector);
+    v3add(p4, v3sub(p4, positionVector, horizontalVector), verticalRightVector);
 
     if (leftHeight < rightHeight) {
-        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]);
-        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]);
-        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]);
-        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]);
-        Vec3.copy(verticalVector, verticalRightVector);
+        caAdd3(vertices, p4[0], p4[1], p4[2]);
+        caAdd3(vertices, p3[0], p3[1], p3[2]);
+        caAdd3(vertices, p2[0], p2[1], p2[2]);
+        caAdd3(vertices, p1[0], p1[1], p1[2]);
+        v3copy(verticalVector, verticalRightVector);
     } else {
-        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]);
-        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]);
-        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]);
-        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]);
-        Vec3.copy(verticalVector, verticalLeftVector);
+        caAdd3(vertices, p1[0], p1[1], p1[2]);
+        caAdd3(vertices, p2[0], p2[1], p2[2]);
+        caAdd3(vertices, p3[0], p3[1], p3[2]);
+        caAdd3(vertices, p4[0], p4[1], p4[2]);
+        v3copy(verticalVector, verticalLeftVector);
     }
 
-    Vec3.cross(normalVector, horizontalVector, verticalVector);
+    v3cross(normalVector, horizontalVector, verticalVector);
 
     for (let i = 0; i < 4; ++i) {
-        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+        caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
     }
-    ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
-    ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
+    caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+    caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
 }
 
 /** set arrowHeight = 0 for no arrow */
@@ -78,9 +90,9 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
     let offsetLength = 0;
 
     if (arrowHeight > 0) {
-        Vec3.fromArray(tA, controlPoints, 0);
-        Vec3.fromArray(tB, controlPoints, linearSegments * 3);
-        offsetLength = arrowHeight / Vec3.magnitude(Vec3.sub(tV, tB, tA));
+        v3fromArray(tA, controlPoints, 0);
+        v3fromArray(tB, controlPoints, linearSegments * 3);
+        offsetLength = arrowHeight / v3magnitude(v3sub(tV, tB, tA));
     }
 
     for (let i = 0; i <= linearSegments; ++i) {
@@ -90,70 +102,70 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
         const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments);
         const i3 = i * 3;
 
-        Vec3.fromArray(verticalVector, normalVectors, i3);
-        Vec3.scale(verticalVector, verticalVector, actualHeight);
+        v3fromArray(verticalVector, normalVectors, i3);
+        v3scale(verticalVector, verticalVector, actualHeight);
 
-        Vec3.fromArray(horizontalVector, binormalVectors, i3);
-        Vec3.scale(horizontalVector, horizontalVector, width);
+        v3fromArray(horizontalVector, binormalVectors, i3);
+        v3scale(horizontalVector, horizontalVector, width);
 
         if (arrowHeight > 0) {
-            Vec3.fromArray(tA, normalVectors, i3);
-            Vec3.fromArray(tB, binormalVectors, i3);
-            Vec3.scale(normalOffset, Vec3.cross(normalOffset, tA, tB), offsetLength);
+            v3fromArray(tA, normalVectors, i3);
+            v3fromArray(tB, binormalVectors, i3);
+            v3scale(normalOffset, v3cross(normalOffset, tA, tB), offsetLength);
         }
 
-        Vec3.fromArray(positionVector, controlPoints, i3);
-        Vec3.fromArray(normalVector, normalVectors, i3);
-        Vec3.fromArray(torsionVector, binormalVectors, i3);
-
-        Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector);
-        Vec3.copy(tB, normalVector);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
-
-        Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
-
-        // Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
-        Vec3.add(tB, Vec3.negate(tB, torsionVector), normalOffset);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
-
-        Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
-
-        // Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
-        Vec3.negate(tB, normalVector);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
-
-        Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
-
-        // Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) // reuse tA
-        Vec3.add(tB, torsionVector, normalOffset);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
-
-        Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector);
-        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
-        ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
+        v3fromArray(positionVector, controlPoints, i3);
+        v3fromArray(normalVector, normalVectors, i3);
+        v3fromArray(torsionVector, binormalVectors, i3);
+
+        v3add(tA, v3add(tA, positionVector, horizontalVector), verticalVector);
+        v3copy(tB, normalVector);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
+
+        v3add(tA, v3sub(tA, positionVector, horizontalVector), verticalVector);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
+
+        // v3add(tA, v3sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        v3add(tB, v3negate(tB, torsionVector), normalOffset);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
+
+        v3sub(tA, v3sub(tA, positionVector, horizontalVector), verticalVector);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
+
+        // v3sub(tA, v3sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        v3negate(tB, normalVector);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
+
+        v3sub(tA, v3add(tA, positionVector, horizontalVector), verticalVector);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
+
+        // v3sub(tA, v3add(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        v3add(tB, torsionVector, normalOffset);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
+
+        v3add(tA, v3add(tA, positionVector, horizontalVector), verticalVector);
+        caAdd3(vertices, tA[0], tA[1], tA[2]);
+        caAdd3(normals, tB[0], tB[1], tB[2]);
     }
 
     for (let i = 0; i < linearSegments; ++i) {
         // the triangles are arranged such that opposing triangles of the sheet align
         // which prevents triangle intersection within tight curves
         for (let j = 0; j < 2; j++) {
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + i * 8 + 2 * j, // a
                 vertexCount + (i + 1) * 8 + 2 * j + 1, // c
                 vertexCount + i * 8 + 2 * j + 1 // b
             );
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + i * 8 + 2 * j, // a
                 vertexCount + (i + 1) * 8 + 2 * j, // d
@@ -161,13 +173,13 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
             );
         }
         for (let j = 2; j < 4; j++) {
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + i * 8 + 2 * j, // a
                 vertexCount + (i + 1) * 8 + 2 * j, // d
                 vertexCount + i * 8 + 2 * j + 1, // b
             );
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + (i + 1) * 8 + 2 * j, // d
                 vertexCount + (i + 1) * 8 + 2 * j + 1, // c
@@ -198,5 +210,5 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
     const addedVertexCount = (linearSegments + 1) * 8 +
         (startCap ? 4 : (arrowHeight > 0 ? 8 : 0)) +
         (endCap && arrowHeight === 0 ? 4 : 0);
-    for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(groups, currentGroup);
+    for (let i = 0, il = addedVertexCount; i < il; ++i) caAdd(groups, currentGroup);
 }

+ 65 - 52
src/mol-geo/geometry/mesh/builder/tube.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>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -9,11 +9,11 @@ import { Vec3 } from '../../../../mol-math/linear-algebra';
 import { ChunkedArray } from '../../../../mol-data/util';
 import { MeshBuilder } from '../mesh-builder';
 
-const normalVector = Vec3.zero();
-const surfacePoint = Vec3.zero();
-const controlPoint = Vec3.zero();
-const u = Vec3.zero();
-const v = Vec3.zero();
+const normalVector = Vec3();
+const surfacePoint = Vec3();
+const controlPoint = Vec3();
+const u = Vec3();
+const v = Vec3();
 
 function add2AndScale2(out: Vec3, a: Vec3, b: Vec3, sa: number, sb: number) {
     out[0] = (a[0] * sa) + (b[0] * sb);
@@ -27,53 +27,70 @@ function add3AndScale2(out: Vec3, a: Vec3, b: Vec3, c: Vec3, sa: number, sb: num
     out[2] = (a[2] * sa) + (b[2] * sb) + c[2];
 }
 
-export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, waveFactor: number, startCap: boolean, endCap: boolean) {
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3fromArray = Vec3.fromArray;
+const v3normalize = Vec3.normalize;
+const v3negate = Vec3.negate;
+const v3copy = Vec3.copy;
+const v3cross = Vec3.cross;
+const caAdd3 = ChunkedArray.add3;
+
+const CosSinCache = new Map<number, { cos: number[], sin: number[] }>();
+function getCosSin(radialSegments: number) {
+    if (!CosSinCache.has(radialSegments)) {
+        const cos: number[] = [];
+        const sin: number[] = [];
+        for (let j = 0; j < radialSegments; ++j) {
+            const t = 2 * Math.PI * j / radialSegments;
+            cos[j] = Math.cos(t);
+            sin[j] = Math.sin(t);
+        }
+        CosSinCache.set(radialSegments, { cos, sin });
+    }
+    return CosSinCache.get(radialSegments)!;
+}
+
+export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean) {
     const { currentGroup, vertices, normals, indices, groups } = state;
 
     let vertexCount = vertices.elementCount;
-    const di = 1 / linearSegments;
+
+    const { cos, sin } = getCosSin(radialSegments);
 
     for (let i = 0; i <= linearSegments; ++i) {
         const i3 = i * 3;
-        Vec3.fromArray(u, normalVectors, i3);
-        Vec3.fromArray(v, binormalVectors, i3);
-        Vec3.fromArray(controlPoint, controlPoints, i3);
+        v3fromArray(u, normalVectors, i3);
+        v3fromArray(v, binormalVectors, i3);
+        v3fromArray(controlPoint, controlPoints, i3);
 
         const width = widthValues[i];
         const height = heightValues[i];
 
-        const tt = di * i - 0.5;
-        const ff = 1 + (waveFactor - 1) * (Math.cos(2 * Math.PI * tt) + 1);
-        const w = ff * width, h = ff * height;
-
         for (let j = 0; j < radialSegments; ++j) {
-            const t = 2 * Math.PI * j / radialSegments;
-
-            add3AndScale2(surfacePoint, u, v, controlPoint, h * Math.cos(t), w * Math.sin(t));
+            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
             if (radialSegments === 2) {
-                // add2AndScale2(normalVector, u, v, w * Math.cos(t), h * Math.sin(t))
-                Vec3.copy(normalVector, v);
-                Vec3.normalize(normalVector, normalVector);
-                if (t !== 0 || i % 2 === 0) Vec3.negate(normalVector, normalVector);
+                v3copy(normalVector, v);
+                v3normalize(normalVector, normalVector);
+                if (j !== 0 || i % 2 === 0) v3negate(normalVector, normalVector);
             } else {
-                add2AndScale2(normalVector, u, v, w * Math.cos(t), h * Math.sin(t));
+                add2AndScale2(normalVector, u, v, width * cos[j], height * sin[j]);
             }
-            Vec3.normalize(normalVector, normalVector);
+            v3normalize(normalVector, normalVector);
 
-            ChunkedArray.add3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
-            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+            caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
+            caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
         }
     }
 
     for (let i = 0; i < linearSegments; ++i) {
         for (let j = 0; j < radialSegments; ++j) {
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + i * radialSegments + (j + 1) % radialSegments,
                 vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
                 vertexCount + i * radialSegments + j
             );
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
                 vertexCount + (i + 1) * radialSegments + j,
@@ -85,27 +102,25 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
     if (startCap) {
         const offset = 0;
         const centerVertex = vertices.elementCount;
-        Vec3.fromArray(u, normalVectors, offset);
-        Vec3.fromArray(v, binormalVectors, offset);
-        Vec3.fromArray(controlPoint, controlPoints, offset);
-        Vec3.cross(normalVector, v, u);
+        v3fromArray(u, normalVectors, offset);
+        v3fromArray(v, binormalVectors, offset);
+        v3fromArray(controlPoint, controlPoints, offset);
+        v3cross(normalVector, v, u);
 
-        ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
-        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+        caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
+        caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
         const width = widthValues[0];
         const height = heightValues[0];
 
         vertexCount = vertices.elementCount;
         for (let i = 0; i < radialSegments; ++i) {
-            const t = 2 * Math.PI * i / radialSegments;
-
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * Math.cos(t), width * Math.sin(t));
+            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
 
-            ChunkedArray.add3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
-            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+            caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
+            caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + (i + 1) % radialSegments,
                 vertexCount + i,
@@ -117,27 +132,25 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
     if (endCap) {
         const offset = linearSegments * 3;
         const centerVertex = vertices.elementCount;
-        Vec3.fromArray(u, normalVectors, offset);
-        Vec3.fromArray(v, binormalVectors, offset);
-        Vec3.fromArray(controlPoint, controlPoints, offset);
-        Vec3.cross(normalVector, u, v);
+        v3fromArray(u, normalVectors, offset);
+        v3fromArray(v, binormalVectors, offset);
+        v3fromArray(controlPoint, controlPoints, offset);
+        v3cross(normalVector, u, v);
 
-        ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
-        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+        caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
+        caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
         const width = widthValues[linearSegments];
         const height = heightValues[linearSegments];
 
         vertexCount = vertices.elementCount;
         for (let i = 0; i < radialSegments; ++i) {
-            const t = 2 * Math.PI * i / radialSegments;
-
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * Math.cos(t), width * Math.sin(t));
+            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
 
-            ChunkedArray.add3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
-            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+            caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
+            caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
-            ChunkedArray.add3(
+            caAdd3(
                 indices,
                 vertexCount + i,
                 vertexCount + (i + 1) % radialSegments,

+ 50 - 40
src/mol-geo/geometry/mesh/mesh-builder.ts

@@ -12,12 +12,22 @@ import { Cage } from '../../../mol-geo/primitive/cage';
 import { addSphere } from './builder/sphere';
 import { addCylinder } from './builder/cylinder';
 
-const tmpV = Vec3.zero();
-const tmpMat3 = Mat3.zero();
-const tmpVecA = Vec3.zero();
-const tmpVecB = Vec3.zero();
-const tmpVecC = Vec3.zero();
-const tmpVecD = Vec3.zero();
+const tmpV = Vec3();
+const tmpMat3 = Mat3();
+const tmpVecA = Vec3();
+const tmpVecB = Vec3();
+const tmpVecC = Vec3();
+const tmpVecD = Vec3();
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3fromArray = Vec3.fromArray;
+const v3triangleNormal = Vec3.triangleNormal;
+const v3copy = Vec3.copy;
+const v3transformMat4 = Vec3.transformMat4;
+const v3transformMat3 = Vec3.transformMat3;
+const mat3directionTransform = Mat3.directionTransform;
+const caAdd3 = ChunkedArray.add3;
+const caAdd = ChunkedArray.add;
 
 export namespace MeshBuilder {
     export interface State {
@@ -45,36 +55,36 @@ export namespace MeshBuilder {
         const offset = vertices.elementCount;
 
         // positions
-        ChunkedArray.add3(vertices, a[0], a[1], a[2]);
-        ChunkedArray.add3(vertices, b[0], b[1], b[2]);
-        ChunkedArray.add3(vertices, c[0], c[1], c[2]);
+        caAdd3(vertices, a[0], a[1], a[2]);
+        caAdd3(vertices, b[0], b[1], b[2]);
+        caAdd3(vertices, c[0], c[1], c[2]);
 
-        Vec3.triangleNormal(tmpV, a, b, c);
+        v3triangleNormal(tmpV, a, b, c);
         for (let i = 0; i < 3; ++i) {
-            ChunkedArray.add3(normals, tmpV[0], tmpV[1], tmpV[2]);  // normal
-            ChunkedArray.add(groups, currentGroup);  // group
+            caAdd3(normals, tmpV[0], tmpV[1], tmpV[2]);  // normal
+            caAdd(groups, currentGroup);  // group
         }
-        ChunkedArray.add3(indices, offset, offset + 1, offset + 2);
+        caAdd3(indices, offset, offset + 1, offset + 2);
     }
 
     export function addTriangleStrip(state: State, vertices: ArrayLike<number>, indices: ArrayLike<number>) {
-        Vec3.fromArray(tmpVecC, vertices, indices[0] * 3);
-        Vec3.fromArray(tmpVecD, vertices, indices[1] * 3);
+        v3fromArray(tmpVecC, vertices, indices[0] * 3);
+        v3fromArray(tmpVecD, vertices, indices[1] * 3);
         for (let i = 2, il = indices.length; i < il; i += 2) {
-            Vec3.copy(tmpVecA, tmpVecC);
-            Vec3.copy(tmpVecB, tmpVecD);
-            Vec3.fromArray(tmpVecC, vertices, indices[i] * 3);
-            Vec3.fromArray(tmpVecD, vertices, indices[i + 1] * 3);
+            v3copy(tmpVecA, tmpVecC);
+            v3copy(tmpVecB, tmpVecD);
+            v3fromArray(tmpVecC, vertices, indices[i] * 3);
+            v3fromArray(tmpVecD, vertices, indices[i + 1] * 3);
             addTriangle(state, tmpVecA, tmpVecB, tmpVecC);
             addTriangle(state, tmpVecB, tmpVecD, tmpVecC);
         }
     }
 
     export function addTriangleFan(state: State, vertices: ArrayLike<number>, indices: ArrayLike<number>) {
-        Vec3.fromArray(tmpVecA, vertices, indices[0] * 3);
+        v3fromArray(tmpVecA, vertices, indices[0] * 3);
         for (let i = 2, il = indices.length; i < il; ++i) {
-            Vec3.fromArray(tmpVecB, vertices, indices[i - 1] * 3);
-            Vec3.fromArray(tmpVecC, vertices, indices[i] * 3);
+            v3fromArray(tmpVecB, vertices, indices[i - 1] * 3);
+            v3fromArray(tmpVecC, vertices, indices[i] * 3);
             addTriangle(state, tmpVecA, tmpVecC, tmpVecB);
         }
     }
@@ -83,19 +93,19 @@ export namespace MeshBuilder {
         const { vertices: va, normals: na, indices: ia } = primitive;
         const { vertices, normals, indices, groups, currentGroup } = state;
         const offset = vertices.elementCount;
-        const n = Mat3.directionTransform(tmpMat3, t);
+        const n = mat3directionTransform(tmpMat3, t);
         for (let i = 0, il = va.length; i < il; i += 3) {
             // position
-            Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, va, i), t);
-            ChunkedArray.add3(vertices, tmpV[0], tmpV[1], tmpV[2]);
+            v3transformMat4(tmpV, v3fromArray(tmpV, va, i), t);
+            caAdd3(vertices, tmpV[0], tmpV[1], tmpV[2]);
             // normal
-            Vec3.transformMat3(tmpV, Vec3.fromArray(tmpV, na, i), n);
-            ChunkedArray.add3(normals, tmpV[0], tmpV[1], tmpV[2]);
+            v3transformMat3(tmpV, v3fromArray(tmpV, na, i), n);
+            caAdd3(normals, tmpV[0], tmpV[1], tmpV[2]);
             // group
-            ChunkedArray.add(groups, currentGroup);
+            caAdd(groups, currentGroup);
         }
         for (let i = 0, il = ia.length; i < il; i += 3) {
-            ChunkedArray.add3(indices, ia[i] + offset, ia[i + 1] + offset, ia[i + 2] + offset);
+            caAdd3(indices, ia[i] + offset, ia[i + 1] + offset, ia[i + 2] + offset);
         }
     }
 
@@ -104,19 +114,19 @@ export namespace MeshBuilder {
         const { vertices: va, normals: na, indices: ia } = primitive;
         const { vertices, normals, indices, groups, currentGroup } = state;
         const offset = vertices.elementCount;
-        const n = Mat3.directionTransform(tmpMat3, t);
+        const n = mat3directionTransform(tmpMat3, t);
         for (let i = 0, il = va.length; i < il; i += 3) {
             // position
-            Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, va, i), t);
-            ChunkedArray.add3(vertices, tmpV[0], tmpV[1], tmpV[2]);
+            v3transformMat4(tmpV, v3fromArray(tmpV, va, i), t);
+            caAdd3(vertices, tmpV[0], tmpV[1], tmpV[2]);
             // normal
-            Vec3.transformMat3(tmpV, Vec3.fromArray(tmpV, na, i), n);
-            ChunkedArray.add3(normals, -tmpV[0], -tmpV[1], -tmpV[2]);
+            v3transformMat3(tmpV, v3fromArray(tmpV, na, i), n);
+            caAdd3(normals, -tmpV[0], -tmpV[1], -tmpV[2]);
             // group
-            ChunkedArray.add(groups, currentGroup);
+            caAdd(groups, currentGroup);
         }
         for (let i = 0, il = ia.length; i < il; i += 3) {
-            ChunkedArray.add3(indices, ia[i + 2] + offset, ia[i + 1] + offset, ia[i] + offset);
+            caAdd3(indices, ia[i + 2] + offset, ia[i + 1] + offset, ia[i] + offset);
         }
     }
 
@@ -124,10 +134,10 @@ export namespace MeshBuilder {
         const { vertices: va, edges: ea } = cage;
         const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments };
         for (let i = 0, il = ea.length; i < il; i += 2) {
-            Vec3.fromArray(tmpVecA, va, ea[i] * 3);
-            Vec3.fromArray(tmpVecB, va, ea[i + 1] * 3);
-            Vec3.transformMat4(tmpVecA, tmpVecA, t);
-            Vec3.transformMat4(tmpVecB, tmpVecB, t);
+            v3fromArray(tmpVecA, va, ea[i] * 3);
+            v3fromArray(tmpVecB, va, ea[i + 1] * 3);
+            v3transformMat4(tmpVecA, tmpVecA, t);
+            v3transformMat4(tmpVecB, tmpVecB, t);
             addSphere(state, tmpVecA, radius, detail);
             addSphere(state, tmpVecB, radius, detail);
             addCylinder(state, tmpVecA, tmpVecB, 1, cylinderProps);

+ 6 - 2
src/mol-geo/geometry/points/points-builder.ts

@@ -7,6 +7,10 @@
 import { ChunkedArray } from '../../../mol-data/util';
 import { Points } from './points';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const caAdd3 = ChunkedArray.add3;
+const caAdd = ChunkedArray.add;
+
 export interface PointsBuilder {
     add(x: number, y: number, z: number, group: number): void
     getPoints(): Points
@@ -19,8 +23,8 @@ export namespace PointsBuilder {
 
         return {
             add: (x: number, y: number, z: number, group: number) => {
-                ChunkedArray.add3(centers, x, y, z);
-                ChunkedArray.add(groups, group);
+                caAdd3(centers, x, y, z);
+                caAdd(groups, group);
             },
             getPoints: () => {
                 const cb = ChunkedArray.compact(centers, true) as Float32Array;

+ 10 - 5
src/mol-geo/geometry/spheres/spheres-builder.ts

@@ -19,6 +19,11 @@ const quadIndices = new Uint16Array([
     1, 3, 2
 ]);
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const caAdd3 = ChunkedArray.add3;
+const caAdd2 = ChunkedArray.add2;
+const caAdd = ChunkedArray.add;
+
 export interface SpheresBuilder {
     add(x: number, y: number, z: number, group: number): void
     getSpheres(): Spheres
@@ -37,12 +42,12 @@ export namespace SpheresBuilder {
             add: (x: number, y: number, z: number, group: number) => {
                 const offset = centers.elementCount;
                 for (let i = 0; i < 4; ++i) {
-                    ChunkedArray.add3(centers, x, y, z);
-                    ChunkedArray.add2(mappings, quadMapping[i * 2], quadMapping[i * 2 + 1]);
-                    ChunkedArray.add(groups, group);
+                    caAdd3(centers, x, y, z);
+                    caAdd2(mappings, quadMapping[i * 2], quadMapping[i * 2 + 1]);
+                    caAdd(groups, group);
                 }
-                ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
-                ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
+                caAdd3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
+                caAdd3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
             },
             getSpheres: () => {
                 const cb = ChunkedArray.compact(centers, true) as Float32Array;

+ 32 - 27
src/mol-geo/geometry/text/text-builder.ts

@@ -14,6 +14,11 @@ const quadIndices = new Uint16Array([
     1, 3, 2
 ]);
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const caAdd3 = ChunkedArray.add3;
+const caAdd2 = ChunkedArray.add2;
+const caAdd = ChunkedArray.add;
+
 export interface TextBuilder {
     add(str: string, x: number, y: number, z: number, depth: number, scale: number, group: number): void
     getText(): Text
@@ -38,9 +43,9 @@ export namespace TextBuilder {
         const outline = fontAtlas.buffer / fontAtlas.lineHeight;
 
         const add = (x: number, y: number, z: number, depth: number, group: number) => {
-            ChunkedArray.add3(centers, x, y, z);
-            ChunkedArray.add(depths, depth);
-            ChunkedArray.add(groups, group);
+            caAdd3(centers, x, y, z);
+            caAdd(depths, depth);
+            caAdd(groups, group);
         };
 
         return {
@@ -117,18 +122,18 @@ export namespace TextBuilder {
 
                 // background
                 if (background) {
-                    ChunkedArray.add2(mappings, xLeft, yTop); // top left
-                    ChunkedArray.add2(mappings, xLeft, yBottom); // bottom left
-                    ChunkedArray.add2(mappings, xRight, yTop); // top right
-                    ChunkedArray.add2(mappings, xRight, yBottom); // bottom right
+                    caAdd2(mappings, xLeft, yTop); // top left
+                    caAdd2(mappings, xLeft, yBottom); // bottom left
+                    caAdd2(mappings, xRight, yTop); // top right
+                    caAdd2(mappings, xRight, yBottom); // bottom right
 
                     const offset = centers.elementCount;
                     for (let i = 0; i < 4; ++i) {
-                        ChunkedArray.add2(tcoords, 10, 10);
+                        caAdd2(tcoords, 10, 10);
                         add(x, y, z, depth, group);
                     }
-                    ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
-                    ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
+                    caAdd3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
+                    caAdd3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
                 }
 
                 if (tether) {
@@ -234,18 +239,18 @@ export namespace TextBuilder {
                         default:
                             throw new Error('unsupported attachment');
                     }
-                    ChunkedArray.add2(mappings, xTip, yTip); // tip
-                    ChunkedArray.add2(mappings, xBaseA, yBaseA); // base A
-                    ChunkedArray.add2(mappings, xBaseB, yBaseB); // base B
-                    ChunkedArray.add2(mappings, xBaseCenter, yBaseCenter); // base center
+                    caAdd2(mappings, xTip, yTip); // tip
+                    caAdd2(mappings, xBaseA, yBaseA); // base A
+                    caAdd2(mappings, xBaseB, yBaseB); // base B
+                    caAdd2(mappings, xBaseCenter, yBaseCenter); // base center
 
                     const offset = centers.elementCount;
                     for (let i = 0; i < 4; ++i) {
-                        ChunkedArray.add2(tcoords, 10, 10);
+                        caAdd2(tcoords, 10, 10);
                         add(x, y, z, depth, group);
                     }
-                    ChunkedArray.add3(indices, offset, offset + 1, offset + 3);
-                    ChunkedArray.add3(indices, offset, offset + 3, offset + 2);
+                    caAdd3(indices, offset, offset + 1, offset + 3);
+                    caAdd3(indices, offset, offset + 3, offset + 2);
                 }
 
                 xShift += outline;
@@ -260,25 +265,25 @@ export namespace TextBuilder {
                     const top = (c.nh - yShift) * scale;
                     const bottom = (-yShift) * scale;
 
-                    ChunkedArray.add2(mappings, left, top);
-                    ChunkedArray.add2(mappings, left, bottom);
-                    ChunkedArray.add2(mappings, right, top);
-                    ChunkedArray.add2(mappings, right, bottom);
+                    caAdd2(mappings, left, top);
+                    caAdd2(mappings, left, bottom);
+                    caAdd2(mappings, right, top);
+                    caAdd2(mappings, right, bottom);
 
                     const texWidth = fontAtlas.texture.width;
                     const texHeight = fontAtlas.texture.height;
 
-                    ChunkedArray.add2(tcoords, c.x / texWidth, c.y / texHeight); // top left
-                    ChunkedArray.add2(tcoords, c.x / texWidth, (c.y + c.h) / texHeight); // bottom left
-                    ChunkedArray.add2(tcoords, (c.x + c.w) / texWidth, c.y / texHeight); // top right
-                    ChunkedArray.add2(tcoords, (c.x + c.w) / texWidth, (c.y + c.h) / texHeight); // bottom right
+                    caAdd2(tcoords, c.x / texWidth, c.y / texHeight); // top left
+                    caAdd2(tcoords, c.x / texWidth, (c.y + c.h) / texHeight); // bottom left
+                    caAdd2(tcoords, (c.x + c.w) / texWidth, c.y / texHeight); // top right
+                    caAdd2(tcoords, (c.x + c.w) / texWidth, (c.y + c.h) / texHeight); // bottom right
 
                     xadvance += c.nw - 2 * outline;
 
                     const offset = centers.elementCount;
                     for (let i = 0; i < 4; ++i) add(x, y, z, depth, group);
-                    ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
-                    ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
+                    caAdd3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
+                    caAdd3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
                 }
             },
             getText: () => {

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

@@ -145,7 +145,7 @@ class MarchingCubesState {
         // 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;
+        this.verticesOnEdges.fill(0, start, end);
     }
 
     private interpolate(edgeNum: number) {

+ 22 - 0
src/mol-math/approx.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * adapted from https://gist.github.com/imbcmdth/6338194
+ * which is ported from https://code.google.com/archive/p/fastapprox/ (BSD licensed)
+ */
+
+const _a_fasterPow2 = new ArrayBuffer(4);
+const _i_fasterPow2 = new Int32Array(_a_fasterPow2);
+const _f_fasterPow2 = new Float32Array(_a_fasterPow2);
+
+export function fasterPow2(v: number) {
+    const clipNumber = (v < -126) ? -126 : v;
+    _i_fasterPow2[0] = ((1 << 23) * (clipNumber + 126.94269504));
+    return _f_fasterPow2[0];
+};
+
+export function fasterExp(v: number) {
+    return fasterPow2(1.442695040 * v);
+};

+ 1 - 6
src/mol-math/geometry/boundary-helper.ts

@@ -97,12 +97,7 @@ export class BoundaryHelper {
     }
 
     getBox(box?: Box3D) {
-        if (!box) box = Box3D();
-        Box3D.setEmpty(box);
-        for (let i = 0; i < this.extrema.length; i++) {
-            Box3D.add(box, this.extrema[i]);
-        }
-        return box;
+        return Box3D.fromVec3Array(box || Box3D(), this.extrema);
     }
 
     reset() {

+ 34 - 4
src/mol-math/geometry/boundary.ts

@@ -11,30 +11,35 @@ import { OrderedSet } from '../../mol-data/int';
 import { BoundaryHelper } from './boundary-helper';
 import { Box3D, Sphere3D } from '../geometry';
 
+export type Boundary = { readonly box: Box3D, readonly sphere: Sphere3D }
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3set = Vec3.set;
+const v3squaredDistance = Vec3.squaredDistance;
+
 const boundaryHelperCoarse = new BoundaryHelper('14');
 const boundaryHelperFine = new BoundaryHelper('98');
 function getBoundaryHelper(count: number) {
     return count > 10_000 ? boundaryHelperCoarse : boundaryHelperFine;
 }
 
-export type Boundary = { readonly box: Box3D, readonly sphere: Sphere3D }
+const p = Vec3();
 
 export function getBoundary(data: PositionData): Boundary {
     const { x, y, z, radius, indices } = data;
-    const p = Vec3();
     const n = OrderedSet.size(indices);
 
     const boundaryHelper = getBoundaryHelper(n);
     boundaryHelper.reset();
     for (let t = 0; t < n; t++) {
         const i = OrderedSet.getAt(indices, t);
-        Vec3.set(p, x[i], y[i], z[i]);
+        v3set(p, x[i], y[i], z[i]);
         boundaryHelper.includePositionRadius(p, (radius && radius[i]) || 0);
     }
     boundaryHelper.finishedIncludeStep();
     for (let t = 0; t < n; t++) {
         const i = OrderedSet.getAt(indices, t);
-        Vec3.set(p, x[i], y[i], z[i]);
+        v3set(p, x[i], y[i], z[i]);
         boundaryHelper.radiusPositionRadius(p, (radius && radius[i]) || 0);
     }
 
@@ -50,4 +55,29 @@ export function getBoundary(data: PositionData): Boundary {
     }
 
     return { box: boundaryHelper.getBox(), sphere };
+}
+
+export function tryAdjustBoundary(data: PositionData, boundary: Boundary): Boundary | undefined {
+    const { x, y, z, indices } = data;
+    const n = OrderedSet.size(indices);
+    const { center, radius } = boundary.sphere;
+
+    let maxDistSq = 0;
+    for (let t = 0; t < n; t++) {
+        const i = OrderedSet.getAt(indices, t);
+        v3set(p, x[i], y[i], z[i]);
+        const distSq = v3squaredDistance(p, center);
+        if (distSq > maxDistSq) maxDistSq = distSq;
+    }
+
+    const adjustedRadius = Math.sqrt(maxDistSq);
+    const deltaRadius = adjustedRadius - radius;
+    if (Math.abs(deltaRadius) < (radius / 100) * 5) {
+        // TODO: The expanded sphere extrema are not correct if the principal axes differ
+        const sphere = Sphere3D.expand(Sphere3D(), boundary.sphere, deltaRadius);
+        const box = Box3D.fromSphere3D(Box3D(), sphere);
+        return { box, sphere };
+    } else {
+        return undefined;
+    }
 }

+ 3 - 2
src/mol-math/geometry/gaussian-density/cpu.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>
  */
@@ -10,6 +10,7 @@ import { RuntimeContext } from '../../../mol-task';
 import { PositionData, DensityData } from '../common';
 import { OrderedSet } from '../../../mol-data/int';
 import { GaussianDensityProps } from '../gaussian-density';
+import { fasterExp } from '../../approx';
 
 export async function GaussianDensityCPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps): Promise<DensityData> {
     const { resolution, radiusOffset, smoothness } = props;
@@ -96,7 +97,7 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position
                         const dz = gridz[zi] - vz;
                         const dSq = dxySq + dz * dz;
                         if (dSq <= r2sq) {
-                            const dens = Math.exp(-alpha * (dSq * rSqInv));
+                            const dens = fasterExp(-alpha * (dSq * rSqInv));
                             const idx = zi + xyIdx;
                             data[idx] += dens;
                             if (dens > densData[idx]) {

+ 11 - 0
src/mol-math/geometry/primitives/box3d.ts

@@ -30,13 +30,24 @@ namespace Box3D {
         return copy(empty(), a);
     }
 
+    /** Get box from sphere, uses extrema if available */
     export function fromSphere3D(out: Box3D, sphere: Sphere3D): Box3D {
+        if (Sphere3D.hasExtrema(sphere)) return fromVec3Array(out, sphere.extrema);
         const r = Vec3.create(sphere.radius, sphere.radius, sphere.radius);
         Vec3.sub(out.min, sphere.center, r);
         Vec3.add(out.max, sphere.center, r);
         return out;
     }
 
+    /** Get box from sphere, uses extrema if available */
+    export function fromVec3Array(out: Box3D, array: Vec3[]): Box3D {
+        Box3D.setEmpty(out);
+        for (let i = 0, il = array.length; i < il; i++) {
+            Box3D.add(out, array[i]);
+        }
+        return out;
+    }
+
     export function computeBounding(data: PositionData): Box3D {
         const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
         const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);

+ 18 - 10
src/mol-math/geometry/symmetry-operator.ts

@@ -188,7 +188,9 @@ function createProjections(t: SymmetryOperator, coords: SymmetryOperator.Coordin
 }
 
 function projectCoord(xs: ArrayLike<number>) {
-    return (i: number) => xs[i];
+    return function projectCoord(i: number) {
+        return xs[i];
+    };
 }
 
 function isW1(m: Mat4) {
@@ -200,10 +202,12 @@ function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: Symm
 
     if (isW1(m)) {
         // this should always be the case.
-        return (i: number) => xx * xs[i] + yy * ys[i] + zz * zs[i] + tx;
+        return function projectX_W1(i: number) {
+            return xx * xs[i] + yy * ys[i] + zz * zs[i] + tx;
+        };
     }
 
-    return (i: number) => {
+    return function projectX(i: number) {
         const x = xs[i], y = ys[i], z = zs[i], w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1.0;
         return (xx * x + yy * y + zz * z + tx) / w;
     };
@@ -214,10 +218,12 @@ function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: Symm
 
     if (isW1(m)) {
         // this should always be the case.
-        return (i: number) => xx * xs[i] + yy * ys[i] + zz * zs[i] + ty;
+        return function projectY_W1(i: number) {
+            return xx * xs[i] + yy * ys[i] + zz * zs[i] + ty;
+        };
     }
 
-    return (i: number) => {
+    return function projectY(i: number) {
         const x = xs[i], y = ys[i], z = zs[i], w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1.0;
         return (xx * x + yy * y + zz * z + ty) / w;
     };
@@ -228,17 +234,19 @@ function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: Symm
 
     if (isW1(m)) {
         // this should always be the case.
-        return (i: number) => xx * xs[i] + yy * ys[i] + zz * zs[i] + tz;
+        return function projectZ_W1(i: number) {
+            return xx * xs[i] + yy * ys[i] + zz * zs[i] + tz;
+        };
     }
 
-    return (i: number) => {
+    return function projectZ(i: number) {
         const x = xs[i], y = ys[i], z = zs[i], w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1.0;
         return (xx * x + yy * y + zz * z + tz) / w;
     };
 }
 
 function identityPosition<T extends number>({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOperator.CoordinateMapper<T> {
-    return (i, s) => {
+    return function identityPosition(i: T, s: Vec3): Vec3 {
         s[0] = x[i];
         s[1] = y[i];
         s[2] = z[i];
@@ -249,7 +257,7 @@ function identityPosition<T extends number>({ x, y, z }: SymmetryOperator.Coordi
 function generalPosition<T extends number>({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     if (isW1(m)) {
         // this should always be the case.
-        return (i: T, r: Vec3): Vec3 => {
+        return function generalPosition_W1(i: T, r: Vec3): Vec3 {
             const x = xs[i], y = ys[i], z = zs[i];
             r[0] = m[0] * x + m[4] * y + m[8] * z + m[12];
             r[1] = m[1] * x + m[5] * y + m[9] * z + m[13];
@@ -257,7 +265,7 @@ function generalPosition<T extends number>({ matrix: m }: SymmetryOperator, { x:
             return r;
         };
     }
-    return (i: T, r: Vec3): Vec3 => {
+    return function generalPosition(i: T, r: Vec3): Vec3 {
         r[0] = xs[i];
         r[1] = ys[i];
         r[2] = zs[i];

+ 3 - 0
src/mol-model/structure/model/properties/atomic/hierarchy.ts

@@ -113,6 +113,9 @@ export interface AtomicData {
 }
 
 export interface AtomicDerivedData {
+    readonly atom: {
+        readonly atomicNumber: ArrayLike<number>
+    },
     readonly residue: {
         readonly traceElementIndex: ArrayLike<ElementIndex | -1>
         readonly directionFromElementIndex: ArrayLike<ElementIndex | -1>

+ 19 - 10
src/mol-model/structure/model/properties/utils/atomic-derived.ts

@@ -1,10 +1,10 @@
 /**
- * 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>
  */
 
-import { AtomicData } from '../atomic';
+import { AtomicData, AtomNumber } from '../atomic';
 import { AtomicIndex, AtomicDerivedData, AtomicSegments } from '../atomic/hierarchy';
 import { ElementIndex, ResidueIndex } from '../../indexing';
 import { MoleculeType, getMoleculeType, getComponentType, PolymerType, getPolymerType } from '../../types';
@@ -13,20 +13,26 @@ import { ChemicalComponentMap } from '../common';
 import { isProductionMode } from '../../../../../mol-util/debug';
 
 export function getAtomicDerivedData(data: AtomicData, segments: AtomicSegments, index: AtomicIndex, chemicalComponentMap: ChemicalComponentMap): AtomicDerivedData {
-    const { label_comp_id } = data.atoms;
-    const { _rowCount: n } = data.residues;
+    const { label_comp_id, type_symbol, _rowCount: atomCount } = data.atoms;
+    const { _rowCount: residueCount } = data.residues;
     const { offsets } = segments.residueAtomSegments;
 
-    const traceElementIndex = new Int32Array(n);
-    const directionFromElementIndex = new Int32Array(n);
-    const directionToElementIndex = new Int32Array(n);
-    const moleculeType = new Uint8Array(n);
-    const polymerType = new Uint8Array(n);
+    const atomicNumber = new Uint8Array(atomCount);
+
+    for (let i = 0; i < atomCount; ++i) {
+        atomicNumber[i] = AtomNumber(type_symbol.value(i));
+    }
+
+    const traceElementIndex = new Int32Array(residueCount);
+    const directionFromElementIndex = new Int32Array(residueCount);
+    const directionToElementIndex = new Int32Array(residueCount);
+    const moleculeType = new Uint8Array(residueCount);
+    const polymerType = new Uint8Array(residueCount);
 
     const moleculeTypeMap = new Map<string, MoleculeType>();
     const polymerTypeMap = new Map<string, PolymerType>();
 
-    for (let i = 0 as ResidueIndex; i < n; ++i) {
+    for (let i = 0 as ResidueIndex; i < residueCount; ++i) {
         const compId = label_comp_id.value(offsets[i]);
         const chemCompMap = chemicalComponentMap;
 
@@ -68,6 +74,9 @@ export function getAtomicDerivedData(data: AtomicData, segments: AtomicSegments,
     }
 
     return {
+        atom: {
+            atomicNumber: atomicNumber as unknown as ArrayLike<number>
+        },
         residue: {
             traceElementIndex: traceElementIndex as unknown as ArrayLike<ElementIndex | -1>,
             directionFromElementIndex: directionFromElementIndex as unknown as ArrayLike<ElementIndex | -1>,

+ 4 - 0
src/mol-model/structure/model/types.ts

@@ -643,6 +643,10 @@ export namespace BondType {
         return (flags & BondType.Flag.Covalent) !== 0;
     }
 
+    export function isAll(flags: BondType.Flag) {
+        return flags === Math.pow(2, 6) - 1;
+    }
+
     export const Names = {
         'covalent': Flag.Covalent,
         'metal-coordination': Flag.MetallicCoordination,

+ 18 - 7
src/mol-model/structure/structure/unit.ts

@@ -20,7 +20,7 @@ import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElement
 import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
 import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
 import { getPrincipalAxes } from './util/principal-axes';
-import { Boundary, getBoundary } from '../../../mol-math/geometry/boundary';
+import { Boundary, getBoundary, tryAdjustBoundary } from '../../../mol-math/geometry/boundary';
 import { Mat4 } from '../../../mol-math/linear-algebra';
 
 /**
@@ -204,7 +204,12 @@ namespace Unit {
         }
 
         remapModel(model: Model) {
-            const props = { ...this.props };
+            let boundary = this.props.boundary;
+            if (boundary) {
+                const { x, y, z } = this.model.atomicConformation;
+                boundary = tryAdjustBoundary({ x, y, z, indices: this.elements }, boundary);
+            }
+            const props = { ...this.props, boundary, lookup3d: undefined, principalAxes: undefined };
             const conformation = this.model.atomicConformation !== model.atomicConformation
                 ? SymmetryOperator.createMapping(this.conformation.operator, model.atomicConformation)
                 : this.conformation;
@@ -342,15 +347,21 @@ namespace Unit {
         }
 
         remapModel(model: Model): Unit.Spheres | Unit.Gaussians {
-            const props = { ...this.props };
+            const coarseConformation = this.getCoarseConformation();
+            let boundary = this.props.boundary;
+            if (boundary) {
+                const { x, y, z } = coarseConformation;
+                boundary = tryAdjustBoundary({ x, y, z, indices: this.elements }, boundary);
+            }
+            const props = { ...this.props, boundary, lookup3d: undefined, principalAxes: undefined };
             let conformation: SymmetryOperator.ArrayMapping<ElementIndex>;
             if (this.kind === Kind.Spheres) {
-                conformation = this.model.coarseConformation.spheres !== model.coarseConformation.spheres
-                    ? SymmetryOperator.createMapping(this.conformation.operator, model.atomicConformation)
+                conformation = coarseConformation !== model.coarseConformation.spheres
+                    ? SymmetryOperator.createMapping(this.conformation.operator, coarseConformation)
                     : this.conformation;
             } else if (this.kind === Kind.Gaussians) {
-                conformation = this.model.coarseConformation.gaussians !== model.coarseConformation.gaussians
-                    ? SymmetryOperator.createMapping(this.conformation.operator, model.atomicConformation)
+                conformation = coarseConformation !== model.coarseConformation.gaussians
+                    ? SymmetryOperator.createMapping(this.conformation.operator, coarseConformation)
                     : this.conformation;
             } else {
                 throw new Error('unexpected unit kind');

+ 1 - 1
src/mol-plugin-state/animation/built-in.ts

@@ -21,7 +21,7 @@ export const AnimateModelIndex = PluginStateAnimation.create({
             loop: PD.Group({ }),
             once: PD.Group({ direction: PD.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]) }, { isFlat: true })
         }, { options: [['palindrome', 'Palindrome'], ['loop', 'Loop'], ['once', 'Once']] }),
-        maxFPS: PD.Numeric(15, { min: 1, max: 30, step: 1 })
+        maxFPS: PD.Numeric(15, { min: 1, max: 60, step: 1 })
     }),
     canApply(ctx) {
         const state = ctx.state.data;

+ 1 - 1
src/mol-plugin-state/manager/camera.ts

@@ -17,7 +17,7 @@ import { StructureElement } from '../../mol-model/structure';
 // TODO: make this customizable somewhere?
 const DefaultCameraFocusOptions = {
     minRadius: 5,
-    extraRadius: 6,
+    extraRadius: 4,
     durationMs: 250
 };
 

+ 34 - 12
src/mol-plugin-state/manager/structure/focus.ts

@@ -13,6 +13,8 @@ import { Loci } from '../../../mol-model/loci';
 import { lociLabel } from '../../../mol-theme/label';
 import { PluginStateObject } from '../../objects';
 import { StateSelection } from '../../../mol-state';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { Sphere3D } from '../../../mol-math/geometry';
 
 export type FocusEntry = {
     label: string
@@ -137,26 +139,19 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
         this.set({ label, loci, category });
     }
 
-    // this.subscribeObservable(this.plugin.state.events.object.updated, o => {
-    //     if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(lastLoci)) return;
-    //     if (lastLoci.structure === o.oldObj.data) {
-    //         lastLoci = EmptyLoci;
-    //     }
-    // });
-
     constructor(private plugin: PluginContext) {
         super({ history: [] });
 
-        plugin.state.data.events.object.removed.subscribe(o => {
-            if (!PluginStateObject.Molecule.Structure.is(o.obj)) return;
+        plugin.state.data.events.object.removed.subscribe(({ obj }) => {
+            if (!PluginStateObject.Molecule.Structure.is(obj)) return;
 
-            if (this.current?.loci.structure === o.obj.data) {
+            if (this.current?.loci.structure === obj.data) {
                 this.clear();
             }
 
             const keep: FocusEntry[] = [];
             for (const e of this.history) {
-                if (e.loci.structure === o.obj.data) keep.push(e);
+                if (e.loci.structure === obj.data) keep.push(e);
             }
             if (keep.length !== this.history.length) {
                 this.history.length = 0;
@@ -164,6 +159,33 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
                 this.events.historyUpdated.next();
             }
         });
-        // plugin.state.data.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
+
+        const sphere = Sphere3D();
+
+        plugin.state.data.events.object.updated.subscribe(({ oldData, obj, action }) => {
+            if (!PluginStateObject.Molecule.Structure.is(obj)) return;
+
+            if (action === 'in-place') {
+                const current = this.state.current;
+                const structure = obj.data as Structure;
+
+                if (current && current.loci.structure === oldData) {
+                    const loci = StructureElement.Loci.remap(current.loci, structure);
+                    this.state.current = { ...current, loci };
+                    this.behaviors.current.next(this.state.current);
+
+                    Loci.getBoundingSphere(loci, sphere);
+                    const camera = this.plugin.canvas3d?.camera!;
+                    const d = camera.getTargetDistance(sphere.radius + 4); // default extraRadius
+                    if (Vec3.distance(camera.target, sphere.center) > sphere.radius ||
+                        d > camera.viewport.height / camera.zoom
+                    ) {
+                        this.plugin.managers.camera.focusSphere(sphere, { durationMs: 0 });
+                    }
+                }
+
+                // TODO remap history as well
+            }
+        });
     }
 }

+ 0 - 47
src/mol-plugin-state/transforms/model.ts

@@ -438,53 +438,6 @@ const StructureFromModel = PluginStateTransform.BuiltIn({
 
 const _translation = Vec3(), _m = Mat4(), _n = Mat4();
 
-// type StructureCoordinateSystem = typeof StructureCoordinateSystem
-// const StructureCoordinateSystem = PluginStateTransform.BuiltIn({
-//     name: 'structure-coordinate-system',
-//     display: { name: 'Coordinate System' },
-//     isDecorator: true,
-//     from: SO.Molecule.Structure,
-//     to: SO.Molecule.Structure,
-//     params: {
-//         transform: PD.MappedStatic('components', {
-//             components: PD.Group({
-//                 axis: PD.Vec3(Vec3.create(1, 0, 0)),
-//                 angle: PD.Numeric(0, { min: -180, max: 180, step: 0.1 }),
-//                 translation: PD.Vec3(Vec3.create(0, 0, 0)),
-//             }, { isFlat: true }),
-//             matrix: PD.Group({
-//                 data: PD.Mat4(Mat4.identity()),
-//                 transpose: PD.Boolean(false)
-//             }, { isFlat: true })
-//         }, { label: 'Kind' })
-//     }
-// })({
-//     canAutoUpdate({ newParams }) {
-//         return newParams.transform.name === 'components';
-//     },
-//     apply({ a, params }) {
-//         // TODO: optimze
-
-//         const transform = Mat4();
-
-//         if (params.transform.name === 'components') {
-//             const { axis, angle, translation } = params.transform.params;
-//             const center = a.data.boundary.sphere.center;
-//             Mat4.fromTranslation(_m, Vec3.negate(_translation, center));
-//             Mat4.fromTranslation(_n, Vec3.add(_translation, center, translation));
-//             const rot = Mat4.fromRotation(Mat4(), Math.PI / 180 * angle, Vec3.normalize(Vec3(), axis));
-//             Mat4.mul3(transform, _n, rot, _m);
-//         } else if (params.transform.name === 'matrix') {
-//             Mat4.copy(transform, params.transform.params.data);
-//             if (params.transform.params.transpose) Mat4.transpose(transform, transform);
-//         }
-
-//         // TODO: compose with parent's coordinate system
-//         a.data.coordinateSystem = SymmetryOperator.create('CS', transform);
-//         return new SO.Molecule.Structure(a.data, { label: a.label, description: `${a.description} [Transformed]` });
-//     }
-// });
-
 type TransformStructureConformation = typeof TransformStructureConformation
 const TransformStructureConformation = PluginStateTransform.BuiltIn({
     name: 'transform-structure-conformation',

+ 10 - 20
src/mol-plugin-state/transforms/representation.ts

@@ -36,6 +36,7 @@ import { DihedralParams, DihedralRepresentation } from '../../mol-repr/shape/loc
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
 import { Clipping } from '../../mol-theme/clipping';
 import { ObjectKeys } from '../../mol-util/type-helpers';
+import { deepEqual } from '../../mol-util';
 
 export { StructureRepresentation3D };
 export { ExplodeStructureRepresentation3D };
@@ -127,18 +128,12 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
             const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
             const provider = plugin.representation.structure.registry.get(params.type.name);
             if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
-            const props = params.type.params || {};
             const repr = provider.factory({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, provider.getParams);
             await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: a.data }, params);
             repr.setTheme(Theme.create(plugin.representation.structure.themes, { structure: a.data }, params));
 
-            // // TODO: build this into representation?
-            // if (!a.data.coordinateSystem.isIdentity) {
-            //     (cache as any)['transform'] = a.data.coordinateSystem;
-            //     repr.setState({ transform: a.data.coordinateSystem.matrix });
-            // }
-
             // TODO set initial state, repr.setState({})
+            const props = params.type.params || {};
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
             return new SO.Molecule.Structure.Representation3D({ repr, source: a }, { label: provider.label });
         });
@@ -147,24 +142,19 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
         return Task.create('Structure Representation', async ctx => {
             if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
 
-            // dispose isn't called on update so we need to handle it manually
-            const oldProvider = plugin.representation.structure.registry.get(oldParams.type.name);
-            if (oldProvider.ensureCustomProperties) oldProvider.ensureCustomProperties.detach(a.data);
-            Theme.releaseDependencies(plugin.representation.structure.themes, { structure: a.data }, oldParams);
-
             const provider = plugin.representation.structure.registry.get(newParams.type.name);
             const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
             if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
-            const props = { ...b.data.repr.props, ...newParams.type.params };
-            await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: a.data }, newParams);
-            b.data.repr.setTheme(Theme.create(plugin.representation.structure.themes, { structure: a.data }, newParams));
 
-            // // TODO: build this into representation?
-            // if ((cache as any)['transform'] !== a.data.coordinateSystem) {
-            //     (cache as any)['transform'] = a.data.coordinateSystem;
-            //     b.data.repr.setState({ transform: a.data.coordinateSystem.matrix });
-            // }
+            if (!deepEqual(oldParams, newParams) || a.data.hashCode !== b.data.source.data.hashCode) {
+                // dispose isn't called on update so we need to handle it manually
+                Theme.releaseDependencies(plugin.representation.structure.themes, { structure: b.data.source.data }, oldParams);
+
+                await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: a.data }, newParams);
+                b.data.repr.setTheme(Theme.create(plugin.representation.structure.themes, { structure: a.data }, newParams));
+            }
 
+            const props = { ...b.data.repr.props, ...newParams.type.params };
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
             b.data.source = a;
             return StateTransformer.UpdateResult.Updated;

+ 2 - 1
src/mol-plugin/behavior/dynamic/representation.ts

@@ -153,8 +153,9 @@ export const SelectLoci = PluginBehavior.create({
             this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref, obj, oldObj, action }) => {
                 const cell = this.ctx.state.data.cells.get(ref);
                 if (cell && SO.Molecule.Structure.is(cell.obj)) {
+                    if (action === 'recreate' && obj.data.hashCode === oldObj?.data.hashCode) return;
                     // TODO how to ensure that in-place updates result in compatible structures?
-                    if (obj.data.hashCode === oldObj?.data.hashCode || action === 'in-place') return;
+                    if (action === 'in-place') return;
 
                     const reprs = this.ctx.state.data.select(StateSelection.Generators.ofType(SO.Molecule.Structure.Representation3D, ref));
                     for (const repr of reprs) this.applySelectMark(repr.transform.ref, true);

+ 3 - 14
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -14,9 +14,8 @@ import { BitFlags, arrayEqual } from '../../../mol-util';
 import { createLinkCylinderMesh, LinkStyle } from './util/link';
 import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../complex-visual';
 import { VisualUpdateState } from '../../util';
-import { isHydrogen } from './util/common';
 import { BondType } from '../../../mol-model/structure/model/types';
-import { ignoreBondType, BondCylinderParams, BondIterator, getInterBondLoci, eachInterBond } from './util/bond';
+import { BondCylinderParams, BondIterator, getInterBondLoci, eachInterBond, makeInterBondIgnoreTest } from './util/bond';
 
 const tmpRefPosBondIt = new Bond.ElementBondIterator();
 function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
@@ -35,16 +34,7 @@ const tmpLoc = StructureElement.Location.create(void 0);
 function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>, mesh?: Mesh) {
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
-    const { sizeFactor, sizeAspectRatio, ignoreHydrogens, includeTypes, excludeTypes } = props;
-
-    const include = BondType.fromNames(includeTypes);
-    const exclude = BondType.fromNames(excludeTypes);
-
-    const ignoreHydrogen = ignoreHydrogens ? (edgeIndex: number) => {
-        const b = edges[edgeIndex];
-        const uA = b.unitA, uB = b.unitB;
-        return isHydrogen(uA, uA.elements[b.indexA]) || isHydrogen(uB, uB.elements[b.indexB]);
-    } : () => false;
+    const { sizeFactor, sizeAspectRatio } = props;
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
@@ -96,7 +86,7 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
             const sizeB = theme.size.size(tmpLoc);
             return Math.min(sizeA, sizeB) * sizeFactor * sizeAspectRatio;
         },
-        ignore: (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, edges[edgeIndex].props.flag)
+        ignore: makeInterBondIgnoreTest(structure, props)
     };
 
     return createLinkCylinderMesh(ctx, builderProps, props, mesh);
@@ -107,7 +97,6 @@ export const InterUnitBondCylinderParams = {
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
-    ignoreHydrogens: PD.Boolean(false),
 };
 export type InterUnitBondCylinderParams = typeof InterUnitBondCylinderParams
 

+ 3 - 14
src/mol-repr/structure/visual/bond-inter-unit-line.ts

@@ -13,9 +13,8 @@ import { BitFlags, arrayEqual } from '../../../mol-util';
 import { LinkStyle, createLinkLines } from './util/link';
 import { ComplexVisual, ComplexLinesVisual, ComplexLinesParams } from '../complex-visual';
 import { VisualUpdateState } from '../../util';
-import { isHydrogen } from './util/common';
 import { BondType } from '../../../mol-model/structure/model/types';
-import { ignoreBondType, BondIterator, getInterBondLoci, eachInterBond, BondLineParams } from './util/bond';
+import { BondIterator, getInterBondLoci, eachInterBond, BondLineParams, makeInterBondIgnoreTest } from './util/bond';
 import { Lines } from '../../../mol-geo/geometry/lines/lines';
 
 const tmpRefPosBondIt = new Bond.ElementBondIterator();
@@ -35,16 +34,7 @@ const tmpLoc = StructureElement.Location.create(void 0);
 function createInterUnitBondLines(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondLineParams>, lines?: Lines) {
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
-    const { sizeFactor, ignoreHydrogens, includeTypes, excludeTypes } = props;
-
-    const include = BondType.fromNames(includeTypes);
-    const exclude = BondType.fromNames(excludeTypes);
-
-    const ignoreHydrogen = ignoreHydrogens ? (edgeIndex: number) => {
-        const b = edges[edgeIndex];
-        const uA = b.unitA, uB = b.unitB;
-        return isHydrogen(uA, uA.elements[b.indexA]) || isHydrogen(uB, uB.elements[b.indexB]);
-    } : () => false;
+    const { sizeFactor } = props;
 
     if (!edgeCount) return Lines.createEmpty(lines);
 
@@ -96,7 +86,7 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
             const sizeB = theme.size.size(tmpLoc);
             return Math.min(sizeA, sizeB) * sizeFactor;
         },
-        ignore: (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, edges[edgeIndex].props.flag)
+        ignore: makeInterBondIgnoreTest(structure, props)
     };
 
     return createLinkLines(ctx, builderProps, props, lines);
@@ -105,7 +95,6 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
 export const InterUnitBondLineParams = {
     ...ComplexLinesParams,
     ...BondLineParams,
-    ignoreHydrogens: PD.Boolean(false),
 };
 export type InterUnitBondLineParams = typeof InterUnitBondLineParams
 

+ 11 - 17
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -11,16 +11,18 @@ import { Unit, Structure, StructureElement } from '../../../mol-model/structure'
 import { Theme } from '../../../mol-theme/theme';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { BitFlags, arrayEqual } from '../../../mol-util';
+import { arrayEqual } from '../../../mol-util';
 import { createLinkCylinderMesh, LinkStyle } from './util/link';
 import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
 import { VisualUpdateState } from '../../util';
-import { isHydrogen } from './util/common';
 import { BondType } from '../../../mol-model/structure/model/types';
-import { ignoreBondType, BondCylinderParams, BondIterator, eachIntraBond, getIntraBondLoci } from './util/bond';
+import { BondCylinderParams, BondIterator, eachIntraBond, getIntraBondLoci, makeIntraBondIgnoreTest } from './util/bond';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { IntAdjacencyGraph } from '../../../mol-math/graph';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const isBondType = BondType.is;
+
 function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
 
@@ -30,18 +32,11 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
     const bonds = unit.bonds;
     const { edgeCount, a, b, edgeProps, offset } = bonds;
     const { order: _order, flags: _flags } = edgeProps;
-    const { sizeFactor, sizeAspectRatio, ignoreHydrogens, includeTypes, excludeTypes } = props;
-
-    const include = BondType.fromNames(includeTypes);
-    const exclude = BondType.fromNames(excludeTypes);
-
-    const ignoreHydrogen = ignoreHydrogens ? (edgeIndex: number) => {
-        return isHydrogen(unit, elements[a[edgeIndex]]) || isHydrogen(unit, elements[b[edgeIndex]]);
-    } : () => false;
+    const { sizeFactor, sizeAspectRatio } = props;
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
-    const vRef = Vec3.zero();
+    const vRef = Vec3();
     const pos = unit.conformation.invariantPosition;
 
     const builderProps = {
@@ -51,7 +46,7 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
 
             if (aI > bI) [aI, bI] = [bI, aI];
             if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
-            // TODO prefer reference atoms in rings
+            // TODO prefer reference atoms within rings
 
             for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
                 const _bI = b[i];
@@ -69,8 +64,8 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
         },
         style: (edgeIndex: number) => {
             const o = _order[edgeIndex];
-            const f = BitFlags.create(_flags[edgeIndex]);
-            if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
+            const f = _flags[edgeIndex];
+            if (isBondType(f, BondType.Flag.MetallicCoordination) || isBondType(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
             } else if (o === 2) {
@@ -88,7 +83,7 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
             const sizeB = theme.size.size(location);
             return Math.min(sizeA, sizeB) * sizeFactor * sizeAspectRatio;
         },
-        ignore: (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, _flags[edgeIndex])
+        ignore: makeIntraBondIgnoreTest(unit, props)
     };
 
     const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
@@ -104,7 +99,6 @@ export const IntraUnitBondCylinderParams = {
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
-    ignoreHydrogens: PD.Boolean(false),
 };
 export type IntraUnitBondCylinderParams = typeof IntraUnitBondCylinderParams
 

+ 11 - 17
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -9,17 +9,19 @@ import { VisualContext } from '../../visual';
 import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
 import { Theme } from '../../../mol-theme/theme';
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { BitFlags, arrayEqual } from '../../../mol-util';
+import { arrayEqual } from '../../../mol-util';
 import { LinkStyle, createLinkLines } from './util/link';
 import { UnitsVisual, UnitsLinesParams, UnitsLinesVisual, StructureGroup } from '../units-visual';
 import { VisualUpdateState } from '../../util';
-import { isHydrogen } from './util/common';
 import { BondType } from '../../../mol-model/structure/model/types';
-import { ignoreBondType, BondIterator, BondLineParams, getIntraBondLoci, eachIntraBond } from './util/bond';
+import { BondIterator, BondLineParams, getIntraBondLoci, eachIntraBond, makeIntraBondIgnoreTest } from './util/bond';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Lines } from '../../../mol-geo/geometry/lines/lines';
 import { IntAdjacencyGraph } from '../../../mol-math/graph';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const isBondType = BondType.is;
+
 function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondLineParams>, lines?: Lines) {
     if (!Unit.isAtomic(unit)) return Lines.createEmpty(lines);
 
@@ -29,18 +31,11 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
     const bonds = unit.bonds;
     const { edgeCount, a, b, edgeProps, offset } = bonds;
     const { order: _order, flags: _flags } = edgeProps;
-    const { sizeFactor, ignoreHydrogens, includeTypes, excludeTypes } = props;
-
-    const include = BondType.fromNames(includeTypes);
-    const exclude = BondType.fromNames(excludeTypes);
-
-    const ignoreHydrogen = ignoreHydrogens ? (edgeIndex: number) => {
-        return isHydrogen(unit, elements[a[edgeIndex]]) || isHydrogen(unit, elements[b[edgeIndex]]);
-    } : () => false;
+    const { sizeFactor } = props;
 
     if (!edgeCount) return Lines.createEmpty(lines);
 
-    const vRef = Vec3.zero();
+    const vRef = Vec3();
     const pos = unit.conformation.invariantPosition;
 
     const builderProps = {
@@ -50,7 +45,7 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
 
             if (aI > bI) [aI, bI] = [bI, aI];
             if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
-            // TODO prefer reference atoms in rings
+            // TODO prefer reference atoms within rings
 
             for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
                 const _bI = b[i];
@@ -68,8 +63,8 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
         },
         style: (edgeIndex: number) => {
             const o = _order[edgeIndex];
-            const f = BitFlags.create(_flags[edgeIndex]);
-            if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
+            const f = _flags[edgeIndex];
+            if (isBondType(f, BondType.Flag.MetallicCoordination) || isBondType(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
             } else if (o === 2) {
@@ -87,7 +82,7 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
             const sizeB = theme.size.size(location);
             return Math.min(sizeA, sizeB) * sizeFactor;
         },
-        ignore: (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, _flags[edgeIndex])
+        ignore: makeIntraBondIgnoreTest(unit, props)
     };
 
     const l = createLinkLines(ctx, builderProps, props, lines);
@@ -101,7 +96,6 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
 export const IntraUnitBondLineParams = {
     ...UnitsLinesParams,
     ...BondLineParams,
-    ignoreHydrogens: PD.Boolean(false),
 };
 export type IntraUnitBondLineParams = typeof IntraUnitBondLineParams
 

+ 15 - 10
src/mol-repr/structure/visual/element-point.ts

@@ -12,10 +12,9 @@ import { Theme } from '../../../mol-theme/theme';
 import { Points } from '../../../mol-geo/geometry/points/points';
 import { PointsBuilder } from '../../../mol-geo/geometry/points/points-builder';
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { ElementIterator, getElementLoci, eachElement } from './util/element';
+import { ElementIterator, getElementLoci, eachElement, makeElementIgnoreTest } from './util/element';
 import { VisualUpdateState } from '../../util';
 import { Sphere3D } from '../../../mol-math/geometry';
-import { isTrace, isHydrogen } from './util/common';
 
 export const ElementPointParams = {
     ...UnitsPointsParams,
@@ -29,21 +28,27 @@ export type ElementPointParams = typeof ElementPointParams
 // TODO size
 
 export function createElementPoint(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ElementPointParams>, points: Points) {
-    const { ignoreHydrogens, traceOnly } = props; // TODO sizeFactor
+    // TODO sizeFactor
 
     const elements = unit.elements;
     const n = elements.length;
     const builder = PointsBuilder.create(n, n / 10, points);
 
+    const p = Vec3();
     const pos = unit.conformation.invariantPosition;
-    const p = Vec3.zero();
+    const ignore = makeElementIgnoreTest(unit, props);
 
-    for (let i = 0; i < n; ++i) {
-        if (ignoreHydrogens && isHydrogen(unit, elements[i])) continue;
-        if (traceOnly && !isTrace(unit, elements[i])) continue;
-
-        pos(elements[i], p);
-        builder.add(p[0], p[1], p[2], i);
+    if (ignore) {
+        for (let i = 0; i < n; ++i) {
+            if (ignore(unit, elements[i])) continue;
+            pos(elements[i], p);
+            builder.add(p[0], p[1], p[2], i);
+        }
+    } else {
+        for (let i = 0; i < n; ++i) {
+            pos(elements[i], p);
+            builder.add(p[0], p[1], p[2], i);
+        }
     }
 
     const pt = builder.getPoints();

+ 1 - 1
src/mol-repr/structure/visual/polymer-trace-mesh.ts

@@ -141,7 +141,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
             } else if (radialSegments === 4) {
                 addSheet(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0, startCap, endCap);
             } else {
-                addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, 1, startCap, endCap);
+                addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap);
             }
         }
 

+ 1 - 1
src/mol-repr/structure/visual/polymer-tube-mesh.ts

@@ -95,7 +95,7 @@ function createPolymerTubeMesh(ctx: VisualContext, unit: Unit, structure: Struct
         } else if (radialSegments === 4) {
             addSheet(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0, startCap, endCap);
         } else {
-            addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, 1, startCap, endCap);
+            addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap);
         }
 
         ++i;

+ 51 - 0
src/mol-repr/structure/visual/util/bond.ts

@@ -14,10 +14,12 @@ import { ObjectKeys } from '../../../../mol-util/type-helpers';
 import { PickingId } from '../../../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from '../../../../mol-model/loci';
 import { Interval, OrderedSet } from '../../../../mol-data/int';
+import { isH, isHydrogen } from './common';
 
 export const BondParams = {
     includeTypes: PD.MultiSelect(ObjectKeys(BondType.Names), PD.objectToOptions(BondType.Names)),
     excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)),
+    ignoreHydrogens: PD.Boolean(false),
 };
 export const DefaultBondProps = PD.getDefaultValues(BondParams);
 export type BondProps = typeof DefaultBondProps
@@ -40,6 +42,55 @@ export function ignoreBondType(include: BondType.Flag, exclude: BondType.Flag, f
     return !BondType.is(include, f) || BondType.is(exclude, f);
 }
 
+export function makeIntraBondIgnoreTest(unit: Unit.Atomic, props: BondProps): undefined | ((edgeIndex: number) => boolean) {
+    const elements = unit.elements;
+    const { atomicNumber } = unit.model.atomicHierarchy.derived.atom;
+    const bonds = unit.bonds;
+    const { a, b, edgeProps } = bonds;
+    const { flags: _flags } = edgeProps;
+
+    const { ignoreHydrogens, includeTypes, excludeTypes } = props;
+
+    const include = BondType.fromNames(includeTypes);
+    const exclude = BondType.fromNames(excludeTypes);
+
+    const allBondTypes = BondType.isAll(include) && BondType.Flag.None === exclude;
+
+    if (!allBondTypes && ignoreHydrogens) {
+        return (edgeIndex: number) => isH(atomicNumber, elements[a[edgeIndex]]) || isH(atomicNumber, elements[b[edgeIndex]]) || ignoreBondType(include, exclude, _flags[edgeIndex]);
+    } else if (!allBondTypes) {
+        return (edgeIndex: number) => ignoreBondType(include, exclude, _flags[edgeIndex]);
+    } else if (ignoreHydrogens) {
+        return (edgeIndex: number) => isH(atomicNumber, elements[a[edgeIndex]]) || isH(atomicNumber, elements[b[edgeIndex]]);
+    }
+}
+
+export function makeInterBondIgnoreTest(structure: Structure, props: BondProps): undefined | ((edgeIndex: number) => boolean) {
+    const bonds = structure.interUnitBonds;
+    const { edges } = bonds;
+
+    const { ignoreHydrogens, includeTypes, excludeTypes } = props;
+
+    const include = BondType.fromNames(includeTypes);
+    const exclude = BondType.fromNames(excludeTypes);
+
+    const allBondTypes = BondType.isAll(include) && BondType.Flag.None === exclude;
+
+    const ignoreHydrogen = (edgeIndex: number) => {
+        const b = edges[edgeIndex];
+        const uA = b.unitA, uB = b.unitB;
+        return isHydrogen(uA, uA.elements[b.indexA]) || isHydrogen(uB, uB.elements[b.indexB]);
+    };
+
+    if (!allBondTypes && ignoreHydrogens) {
+        return (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, edges[edgeIndex].props.flag);
+    } else if (!allBondTypes) {
+        return (edgeIndex: number) => ignoreBondType(include, exclude, edges[edgeIndex].props.flag);
+    } else if (ignoreHydrogens) {
+        return (edgeIndex: number) => ignoreHydrogen(edgeIndex);
+    }
+}
+
 export namespace BondIterator {
     export function fromGroup(structureGroup: StructureGroup): LocationIterator {
         const { group, structure } = structureGroup;

+ 5 - 3
src/mol-repr/structure/visual/util/common.ts

@@ -10,7 +10,7 @@ import { TransformData, createTransform } from '../../../../mol-geo/geometry/tra
 import { OrderedSet, SortedArray } from '../../../../mol-data/int';
 import { EmptyLoci, Loci } from '../../../../mol-model/loci';
 import { PhysicalSizeTheme } from '../../../../mol-theme/size/physical';
-import { AtomicNumbers, AtomNumber } from '../../../../mol-model/structure/model/properties/atomic';
+import { AtomicNumbers } from '../../../../mol-model/structure/model/properties/atomic';
 import { fillSerial } from '../../../../mol-util/array';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { AssignableArrayLike } from '../../../../mol-util/type-helpers';
@@ -269,8 +269,10 @@ export function getStructureConformationAndRadius(structure: Structure, ignoreHy
 const _H = AtomicNumbers['H'];
 export function isHydrogen(unit: Unit, element: ElementIndex) {
     if (Unit.isCoarse(unit)) return false;
-    if (AtomNumber(unit.model.atomicHierarchy.atoms.type_symbol.value(element)) === _H) return true;
-    return false;
+    return unit.model.atomicHierarchy.derived.atom.atomicNumber[element] === _H;
+}
+export function isH(atomicNumber: ArrayLike<number>, element: ElementIndex) {
+    return atomicNumber[element] === _H;
 }
 
 export function isTrace(unit: Unit, element: ElementIndex) {

+ 41 - 22
src/mol-repr/structure/visual/util/element.ts

@@ -6,7 +6,7 @@
  */
 
 import { Vec3 } from '../../../../mol-math/linear-algebra';
-import { Unit, StructureElement, Structure } from '../../../../mol-model/structure';
+import { Unit, StructureElement, Structure, ElementIndex } from '../../../../mol-model/structure';
 import { Loci, EmptyLoci } from '../../../../mol-model/loci';
 import { Interval, OrderedSet } from '../../../../mol-data/int';
 import { Mesh } from '../../../../mol-geo/geometry/mesh/mesh';
@@ -20,32 +20,50 @@ import { Theme } from '../../../../mol-theme/theme';
 import { StructureGroup } from '../../../../mol-repr/structure/units-visual';
 import { Spheres } from '../../../../mol-geo/geometry/spheres/spheres';
 import { SpheresBuilder } from '../../../../mol-geo/geometry/spheres/spheres-builder';
-import { isHydrogen, isTrace } from './common';
+import { isTrace, isH } from './common';
 import { Sphere3D } from '../../../../mol-math/geometry';
 
-export interface ElementSphereMeshProps {
-    detail: number,
-    sizeFactor: number,
+type ElementProps = {
     ignoreHydrogens: boolean,
     traceOnly: boolean,
 }
 
+export type ElementSphereMeshProps = {
+    detail: number,
+    sizeFactor: number,
+} & ElementProps
+
+export function makeElementIgnoreTest(unit: Unit, props: ElementProps): undefined | ((unit: Unit, i: ElementIndex) => boolean) {
+    const { ignoreHydrogens, traceOnly } = props;
+
+    const { atomicNumber } = unit.model.atomicHierarchy.derived.atom;
+    const isCoarse = Unit.isCoarse(unit);
+
+    if (!isCoarse && ignoreHydrogens && traceOnly) {
+        return (unit: Unit, element: ElementIndex) => isH(atomicNumber, element) && !isTrace(unit, element);
+    } else if (!isCoarse && ignoreHydrogens) {
+        return (unit: Unit, element: ElementIndex) => isH(atomicNumber, element);
+    } else if (!isCoarse && traceOnly) {
+        return (unit: Unit, element: ElementIndex) => !isTrace(unit, element);
+    }
+}
+
 export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: ElementSphereMeshProps, mesh?: Mesh): Mesh {
-    const { detail, sizeFactor, ignoreHydrogens, traceOnly } = props;
+    const { detail, sizeFactor } = props;
 
     const { elements } = unit;
     const elementCount = elements.length;
     const vertexCount = elementCount * sphereVertexCount(detail);
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 2, mesh);
 
-    const v = Vec3.zero();
+    const v = Vec3();
     const pos = unit.conformation.invariantPosition;
+    const ignore = makeElementIgnoreTest(unit, props);
     const l = StructureElement.Location.create(structure);
     l.unit = unit;
 
     for (let i = 0; i < elementCount; i++) {
-        if (ignoreHydrogens && isHydrogen(unit, elements[i])) continue;
-        if (traceOnly && !isTrace(unit, elements[i])) continue;
+        if (ignore && ignore(unit, elements[i])) continue;
 
         l.element = elements[i];
         pos(elements[i], v);
@@ -62,27 +80,28 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
     return m;
 }
 
-export interface ElementSphereImpostorProps {
-    ignoreHydrogens: boolean,
-    traceOnly: boolean
-}
+export type ElementSphereImpostorProps = ElementProps
 
 export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: ElementSphereImpostorProps, spheres?: Spheres): Spheres {
-    const { ignoreHydrogens, traceOnly } = props;
-
     const { elements } = unit;
     const elementCount = elements.length;
     const builder = SpheresBuilder.create(elementCount, elementCount / 2, spheres);
 
-    const v = Vec3.zero();
+    const v = Vec3();
     const pos = unit.conformation.invariantPosition;
+    const ignore = makeElementIgnoreTest(unit, props);
 
-    for (let i = 0; i < elementCount; i++) {
-        if (ignoreHydrogens && isHydrogen(unit, elements[i])) continue;
-        if (traceOnly && !isTrace(unit, elements[i])) continue;
-
-        pos(elements[i], v);
-        builder.add(v[0], v[1], v[2], i);
+    if (ignore) {
+        for (let i = 0; i < elementCount; i++) {
+            if (ignore(unit, elements[i])) continue;
+            pos(elements[i], v);
+            builder.add(v[0], v[1], v[2], i);
+        }
+    } else {
+        for (let i = 0; i < elementCount; i++) {
+            pos(elements[i], v);
+            builder.add(v[0], v[1], v[2], i);
+        }
     }
 
     const s = builder.getSpheres();

+ 28 - 22
src/mol-repr/structure/visual/util/link.ts

@@ -71,7 +71,7 @@ export interface LinkBuilderProps {
     ignore?: (edgeIndex: number) => boolean
 }
 
-export enum LinkStyle {
+export const enum LinkStyle {
     Solid = 0,
     Dashed = 1,
     Double = 2,
@@ -79,6 +79,13 @@ export enum LinkStyle {
     Disk = 4
 }
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3scale = Vec3.scale;
+const v3add = Vec3.add;
+const v3sub = Vec3.sub;
+const v3setMagnitude = Vec3.setMagnitude;
+const v3dot = Vec3.dot;
+
 /**
  * Each edge is included twice to allow for coloring/picking
  * the half closer to the first vertex, i.e. vertex a.
@@ -113,7 +120,12 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
         builderState.currentGroup = edgeIndex;
 
-        if (linkStyle === LinkStyle.Dashed) {
+        if (linkStyle === LinkStyle.Solid) {
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
+            cylinderProps.topCap = cylinderProps.bottomCap = linkCap;
+
+            addCylinder(builderState, va, vb, 0.5, cylinderProps);
+        } else if (linkStyle === LinkStyle.Dashed) {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius / 3;
             cylinderProps.topCap = cylinderProps.bottomCap = true;
 
@@ -124,7 +136,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
             const absOffset = (linkRadius - multiRadius) * linkSpacing;
 
             calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
-            Vec3.setMagnitude(vShift, vShift, absOffset);
+            v3setMagnitude(vShift, vShift, absOffset);
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius;
             cylinderProps.topCap = cylinderProps.bottomCap = linkCap;
@@ -132,12 +144,12 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
             if (order === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps);
             addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps);
         } else if (linkStyle === LinkStyle.Disk) {
-            Vec3.scale(tmpV12, Vec3.sub(tmpV12, vb, va), 0.475);
-            Vec3.add(va, va, tmpV12);
-            Vec3.sub(vb, vb, tmpV12);
+            v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
+            v3add(va, va, tmpV12);
+            v3sub(vb, vb, tmpV12);
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
-            if (Vec3.dot(tmpV12, up) > 0) {
+            if (v3dot(tmpV12, up) > 0) {
                 cylinderProps.topCap = false;
                 cylinderProps.bottomCap = linkCap;
             } else {
@@ -145,11 +157,6 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
                 cylinderProps.bottomCap = false;
             }
 
-            addCylinder(builderState, va, vb, 0.5, cylinderProps);
-        } else {
-            cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
-            cylinderProps.topCap = cylinderProps.bottomCap = linkCap;
-
             addCylinder(builderState, va, vb, 0.5, cylinderProps);
         }
     }
@@ -179,12 +186,13 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
         if (ignore && ignore(edgeIndex)) continue;
 
         position(va, vb, edgeIndex);
-        Vec3.scale(vb, Vec3.add(vb, va, vb), 0.5);
+        v3scale(vb, v3add(vb, va, vb), 0.5);
 
-        // TODO use line width?
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
 
-        if (linkStyle === LinkStyle.Dashed) {
+        if (linkStyle === LinkStyle.Solid) {
+            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
+        } else if (linkStyle === LinkStyle.Dashed) {
             builder.addFixedCountDashes(va, vb, 7, edgeIndex);
         } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
             const order = LinkStyle.Double ? 2 : 3;
@@ -192,19 +200,17 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
             const absOffset = (1 - multiRadius) * linkSpacing;
 
             calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
-            Vec3.setMagnitude(vShift, vShift, absOffset);
+            v3setMagnitude(vShift, vShift, absOffset);
 
             if (order === 3) builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
             builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], edgeIndex);
             builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], edgeIndex);
         } else if (linkStyle === LinkStyle.Disk) {
-            Vec3.scale(tmpV12, Vec3.sub(tmpV12, vb, va), 0.475);
-            Vec3.add(va, va, tmpV12);
-            Vec3.sub(vb, vb, tmpV12);
+            v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
+            v3add(va, va, tmpV12);
+            v3sub(vb, vb, tmpV12);
 
-            // TODO what to do here?
-            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
-        } else {
+            // TODO what to do here? Line as disk doesn't work well.
             builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
         }
     }

+ 37 - 25
src/mol-repr/structure/visual/util/polymer/curve-segment.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>
  */
@@ -8,6 +8,18 @@ import { Vec3 } from '../../../../../mol-math/linear-algebra';
 import { NumberArray } from '../../../../../mol-util/type-helpers';
 import { lerp } from '../../../../../mol-math/interpolate';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3fromArray = Vec3.fromArray;
+const v3toArray = Vec3.toArray;
+const v3normalize = Vec3.normalize;
+const v3sub = Vec3.sub;
+const v3spline = Vec3.spline;
+const v3slerp = Vec3.slerp;
+const v3copy = Vec3.copy;
+const v3cross = Vec3.cross;
+const v3orthogonalize = Vec3.orthogonalize;
+const v3matchDirection = Vec3.matchDirection;
+
 export interface CurveSegmentState {
     curvePoints: NumberArray,
     tangentVectors: NumberArray,
@@ -43,9 +55,9 @@ export function interpolateCurveSegment(state: CurveSegmentState, controls: Curv
     interpolateNormals(state, controls);
 }
 
-const tanA = Vec3.zero();
-const tanB = Vec3.zero();
-const curvePoint = Vec3.zero();
+const tanA = Vec3();
+const tanB = Vec3();
+const curvePoint = Vec3();
 
 export function interpolatePointsAndTangents(state: CurveSegmentState, controls: CurveSegmentControls, tension: number, shift: number) {
     const { curvePoints, tangentVectors, linearSegments } = state;
@@ -60,18 +72,18 @@ export function interpolatePointsAndTangents(state: CurveSegmentState, controls:
         const t = j * 1.0 / linearSegments;
         if (t < shift1) {
             const te = lerp(tensionBeg, tension, t);
-            Vec3.spline(curvePoint, p0, p1, p2, p3, t + shift, te);
-            Vec3.spline(tanA, p0, p1, p2, p3, t + shift + 0.01, tensionBeg);
-            Vec3.spline(tanB, p0, p1, p2, p3, t + shift - 0.01, tensionBeg);
+            v3spline(curvePoint, p0, p1, p2, p3, t + shift, te);
+            v3spline(tanA, p0, p1, p2, p3, t + shift + 0.01, tensionBeg);
+            v3spline(tanB, p0, p1, p2, p3, t + shift - 0.01, tensionBeg);
         } else {
             const te = lerp(tension, tensionEnd, t);
-            Vec3.spline(curvePoint, p1, p2, p3, p4, t - shift1, te);
-            Vec3.spline(tanA, p1, p2, p3, p4, t - shift1 + 0.01, te);
-            Vec3.spline(tanB, p1, p2, p3, p4, t - shift1 - 0.01, te);
+            v3spline(curvePoint, p1, p2, p3, p4, t - shift1, te);
+            v3spline(tanA, p1, p2, p3, p4, t - shift1 + 0.01, te);
+            v3spline(tanB, p1, p2, p3, p4, t - shift1 - 0.01, te);
         }
-        Vec3.toArray(curvePoint, curvePoints, j * 3);
-        Vec3.normalize(tangentVec, Vec3.sub(tangentVec, tanA, tanB));
-        Vec3.toArray(tangentVec, tangentVectors, j * 3);
+        v3toArray(curvePoint, curvePoints, j * 3);
+        v3normalize(tangentVec, v3sub(tangentVec, tanA, tanB));
+        v3toArray(tangentVec, tangentVectors, j * 3);
     }
 }
 
@@ -95,27 +107,27 @@ export function interpolateNormals(state: CurveSegmentState, controls: CurveSegm
 
     const n = curvePoints.length / 3;
 
-    Vec3.fromArray(firstTangentVec, tangentVectors, 0);
-    Vec3.fromArray(lastTangentVec, tangentVectors, (n - 1) * 3);
+    v3fromArray(firstTangentVec, tangentVectors, 0);
+    v3fromArray(lastTangentVec, tangentVectors, (n - 1) * 3);
 
-    Vec3.orthogonalize(firstNormalVec, firstTangentVec, firstDirection);
-    Vec3.orthogonalize(lastNormalVec, lastTangentVec, lastDirection);
-    Vec3.matchDirection(lastNormalVec, lastNormalVec, firstNormalVec);
+    v3orthogonalize(firstNormalVec, firstTangentVec, firstDirection);
+    v3orthogonalize(lastNormalVec, lastTangentVec, lastDirection);
+    v3matchDirection(lastNormalVec, lastNormalVec, firstNormalVec);
 
-    Vec3.copy(prevNormal, firstNormalVec);
+    v3copy(prevNormal, firstNormalVec);
 
     for (let i = 0; i < n; ++i) {
         const t = i === 0 ? 0 : 1 / (n - i);
 
-        Vec3.fromArray(tangentVec, tangentVectors, i * 3);
+        v3fromArray(tangentVec, tangentVectors, i * 3);
 
-        Vec3.orthogonalize(normalVec, tangentVec, Vec3.slerp(tmpNormal, prevNormal, lastNormalVec, t));
-        Vec3.toArray(normalVec, normalVectors, i * 3);
+        v3orthogonalize(normalVec, tangentVec, v3slerp(tmpNormal, prevNormal, lastNormalVec, t));
+        v3toArray(normalVec, normalVectors, i * 3);
 
-        Vec3.copy(prevNormal, normalVec);
+        v3copy(prevNormal, normalVec);
 
-        Vec3.normalize(binormalVec, Vec3.cross(binormalVec, tangentVec, normalVec));
-        Vec3.toArray(binormalVec, binormalVectors, i * 3);
+        v3normalize(binormalVec, v3cross(binormalVec, tangentVec, normalVec));
+        v3toArray(binormalVec, binormalVectors, i * 3);
     }
 }
 

+ 5 - 4
src/mol-state/state.ts

@@ -42,7 +42,7 @@ class State {
             removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
         },
         object: {
-            updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(),
+            updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject, oldData?: any }>(),
             created: this.ev<State.ObjectEvent & { obj: StateObject }>(),
             removed: this.ev<State.ObjectEvent & { obj?: StateObject }>()
         },
@@ -545,7 +545,7 @@ async function update(ctx: UpdateContext) {
                 if (!transform.state.isGhost && update.obj !== StateObject.Null) newCurrent = update.ref;
             }
         } else if (update.action === 'updated') {
-            ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj });
+            ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj, oldData: update.oldData });
         } else if (update.action === 'replaced') {
             ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'recreate', obj: update.obj, oldObj: update.oldObj });
         }
@@ -780,7 +780,7 @@ function doError(ctx: UpdateContext, ref: Ref, errorObject: any | undefined, sil
 
 type UpdateNodeResult =
     | { ref: Ref, action: 'created', obj: StateObject }
-    | { ref: Ref, action: 'updated', obj: StateObject }
+    | { ref: Ref, action: 'updated', oldData?: any, obj: StateObject }
     | { ref: Ref, action: 'replaced', oldObj?: StateObject, obj: StateObject }
     | { action: 'none' }
 
@@ -870,6 +870,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
     } else {
         const oldParams = current.params.values;
         const oldCache = current.cache;
+        const oldData = current.obj?.data;
         const newParams = params.values;
         current.params = params;
 
@@ -891,7 +892,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
             }
             case StateTransformer.UpdateResult.Updated:
                 updateTag(current.obj, transform);
-                return { ref: currentRef, action: 'updated', obj: current.obj! };
+                return { ref: currentRef, action: 'updated', oldData, obj: current.obj! };
             case StateTransformer.UpdateResult.Null: {
                 dispose(transform, current.obj, oldParams, oldCache, ctx.parent.globalContext);