Bladeren bron

clip objects & per group clipping

- variants: instance, pixel
Alexander Rose 4 jaren geleden
bovenliggende
commit
89486ea9e2
46 gewijzigde bestanden met toevoegingen van 933 en 133 verwijderingen
  1. 2 1
      package.json
  2. 67 0
      src/mol-geo/geometry/clipping-data.ts
  3. 6 1
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  4. 6 1
      src/mol-geo/geometry/image/image.ts
  5. 6 1
      src/mol-geo/geometry/lines/lines.ts
  6. 6 1
      src/mol-geo/geometry/mesh/mesh.ts
  7. 6 1
      src/mol-geo/geometry/points/points.ts
  8. 6 0
      src/mol-geo/geometry/spheres/spheres.ts
  9. 6 1
      src/mol-geo/geometry/text/text.ts
  10. 7 2
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  11. 5 1
      src/mol-gl/_spec/renderer.spec.ts
  12. 7 0
      src/mol-gl/renderable/direct-volume.ts
  13. 26 44
      src/mol-gl/renderable/schema.ts
  14. 75 2
      src/mol-gl/renderer.ts
  15. 9 1
      src/mol-gl/shader-code.ts
  16. 5 0
      src/mol-gl/shader/chunks/assign-clipping-varying.glsl.ts
  17. 5 2
      src/mol-gl/shader/chunks/assign-position.glsl.ts
  18. 13 0
      src/mol-gl/shader/chunks/clip-instance.glsl.ts
  19. 11 0
      src/mol-gl/shader/chunks/clip-pixel.glsl.ts
  20. 102 0
      src/mol-gl/shader/chunks/common-clip.glsl.ts
  21. 16 0
      src/mol-gl/shader/chunks/common-frag-params.glsl.ts
  22. 19 0
      src/mol-gl/shader/chunks/common-vert-params.glsl.ts
  23. 3 0
      src/mol-gl/shader/mesh.frag.ts
  24. 4 1
      src/mol-gl/shader/mesh.vert.ts
  25. 1 1
      src/mol-gl/shader/text.vert.ts
  26. 2 1
      src/mol-gl/webgl/program.ts
  27. 28 37
      src/mol-gl/webgl/uniform.ts
  28. 13 0
      src/mol-math/linear-algebra/3d/vec4.ts
  29. 65 0
      src/mol-plugin-state/helpers/structure-clipping.ts
  30. 14 6
      src/mol-plugin-state/manager/structure/component.ts
  31. 97 0
      src/mol-plugin-state/transforms/representation.ts
  32. 1 1
      src/mol-plugin-ui/controls/parameters.tsx
  33. 12 13
      src/mol-plugin-ui/structure/selection.tsx
  34. 16 3
      src/mol-plugin-ui/viewport/simple-settings.tsx
  35. 6 2
      src/mol-repr/representation.ts
  36. 6 0
      src/mol-repr/structure/complex-representation.ts
  37. 4 0
      src/mol-repr/structure/complex-visual.ts
  38. 9 2
      src/mol-repr/structure/units-representation.ts
  39. 4 0
      src/mol-repr/structure/units-visual.ts
  40. 2 1
      src/mol-repr/structure/visual/util/bond.ts
  41. 28 0
      src/mol-repr/visual.ts
  42. 4 0
      src/mol-repr/volume/representation.ts
  43. 197 0
      src/mol-theme/clipping.ts
  44. 2 2
      src/mol-theme/transparency.ts
  45. 1 1
      src/mol-util/param-definition.ts
  46. 3 3
      src/tests/browser/marching-cubes.ts

+ 2 - 1
package.json

@@ -79,7 +79,8 @@
   "contributors": [
     "Alexander Rose <alexander.rose@weirdbyte.de>",
     "David Sehnal <david.sehnal@gmail.com>",
-    "Sebastian Bittrich <sebastian.bittrich@rcsb.org>"
+    "Sebastian Bittrich <sebastian.bittrich@rcsb.org>",
+    "Ludovic Autin <autin@scripps.edu>"
   ],
   "license": "MIT",
   "devDependencies": {

+ 67 - 0
src/mol-geo/geometry/clipping-data.ts

@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from '../../mol-util/value-cell';
+import { Vec2 } from '../../mol-math/linear-algebra';
+import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
+import { Clipping } from '../../mol-theme/clipping';
+
+export type ClippingData = {
+    dClipObjectCount: ValueCell<number>,
+    dClipVariant: ValueCell<string>,
+
+    tClipping: ValueCell<TextureImage<Uint8Array>>
+    uClippingTexDim: ValueCell<Vec2>
+    dClipping: ValueCell<boolean>,
+}
+
+export function applyClippingGroups(array: Uint8Array, start: number, end: number, groups: Clipping.Groups) {
+    for (let i = start; i < end; ++i) {
+        array[i] = groups;
+    }
+    return true;
+}
+
+export function clearClipping(array: Uint8Array, start: number, end: number) {
+    array.fill(0, start, end);
+}
+
+export function createClipping(count: number, clippingData?: ClippingData): ClippingData {
+    const clipping = createTextureImage(Math.max(1, count), 1, Uint8Array, clippingData && clippingData.tClipping.ref.value.array);
+    if (clippingData) {
+        ValueCell.update(clippingData.tClipping, clipping);
+        ValueCell.update(clippingData.uClippingTexDim, Vec2.create(clipping.width, clipping.height));
+        ValueCell.update(clippingData.dClipping, count > 0);
+        return clippingData;
+    } else {
+        return {
+            dClipObjectCount: ValueCell.create(0),
+            dClipVariant: ValueCell.create('instance'),
+
+            tClipping: ValueCell.create(clipping),
+            uClippingTexDim: ValueCell.create(Vec2.create(clipping.width, clipping.height)),
+            dClipping: ValueCell.create(count > 0),
+        };
+    }
+}
+
+const emptyClippingTexture = { array: new Uint8Array(1), width: 1, height: 1 };
+export function createEmptyClipping(clippingData?: ClippingData): ClippingData {
+    if (clippingData) {
+        ValueCell.update(clippingData.tClipping, emptyClippingTexture);
+        ValueCell.update(clippingData.uClippingTexDim, Vec2.create(1, 1));
+        return clippingData;
+    } else {
+        return {
+            dClipObjectCount: ValueCell.create(0),
+            dClipVariant: ValueCell.create('instance'),
+
+            tClipping: ValueCell.create(emptyClippingTexture),
+            uClippingTexDim: ValueCell.create(Vec2.create(1, 1)),
+            dClipping: ValueCell.create(false),
+        };
+    }
+}

+ 6 - 1
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -12,7 +12,7 @@ import { DirectVolumeValues } from '../../../mol-gl/renderable/direct-volume';
 import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Texture } from '../../../mol-gl/webgl/texture';
 import { Box3D, Sphere3D } from '../../../mol-math/geometry';
-import { Mat4, Vec2, Vec3 } from '../../../mol-math/linear-algebra';
+import { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { Theme } from '../../../mol-theme/theme';
 import { ValueCell } from '../../../mol-util';
 import { Color } from '../../../mol-util/color';
@@ -26,6 +26,7 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { TransformData } from '../transform-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { createTransferFunctionTexture, getControlPointsFromVec2Array } from './transfer-function';
+import { createEmptyClipping } from '../clipping-data';
 
 const VolumeBox = Box();
 const RenderModeOptions = [['isosurface', 'Isosurface'], ['volume', 'Volume']] as [string, string][];
@@ -140,6 +141,7 @@ export namespace DirectVolume {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: VolumeBox.indices.length, groupCount, instanceCount };
 
@@ -156,6 +158,7 @@ export namespace DirectVolume {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
             ...BaseGeometry.createValues(props, counts),
 
@@ -163,6 +166,7 @@ export namespace DirectVolume {
             elements: ValueCell.create(VolumeBox.indices as Uint32Array),
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
 
             uIsoValue: ValueCell.create(props.isoValueNorm),
             uBboxMin: bboxMin,
@@ -204,6 +208,7 @@ export namespace DirectVolume {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
     }
 

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

@@ -9,7 +9,7 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { RenderableState } from '../../../mol-gl/renderable';
 import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere, TextureImage } from '../../../mol-gl/renderable/util';
 import { Sphere3D } from '../../../mol-math/geometry';
-import { Vec2 } from '../../../mol-math/linear-algebra';
+import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
 import { Theme } from '../../../mol-theme/theme';
 import { ValueCell } from '../../../mol-util';
 import { Color } from '../../../mol-util/color';
@@ -23,6 +23,7 @@ import { TransformData } from '../transform-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { ImageValues } from '../../../mol-gl/renderable/image';
 import { fillSerial } from '../../../mol-util/array';
+import { createEmptyClipping } from '../clipping-data';
 
 const QuadIndices = new Uint32Array([
     0, 1, 2,
@@ -137,6 +138,7 @@ namespace Image {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: QuadIndices.length, groupCount, instanceCount };
 
@@ -148,6 +150,7 @@ namespace Image {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
             ...BaseGeometry.createValues(props, counts),
 
@@ -159,6 +162,7 @@ namespace Image {
             aGroup: ValueCell.create(fillSerial(new Float32Array(4))),
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
 
             dInterpolation: ValueCell.create(props.interpolation),
 
@@ -188,6 +192,7 @@ namespace Image {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
     }
 

+ 6 - 1
src/mol-geo/geometry/lines/lines.ts

@@ -5,7 +5,7 @@
  */
 
 import { ValueCell } from '../../../mol-util';
-import { Mat4 } from '../../../mol-math/linear-algebra';
+import { Mat4, Vec4 } from '../../../mol-math/linear-algebra';
 import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
 import { GeometryUtils } from '../geometry';
 import { createColors } from '../color-data';
@@ -25,6 +25,7 @@ import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
+import { createEmptyClipping } from '../clipping-data';
 
 /** Wide line */
 export interface Lines {
@@ -184,6 +185,7 @@ export namespace Lines {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: lines.lineCount * 2 * 3, groupCount, instanceCount };
 
@@ -198,11 +200,13 @@ export namespace Lines {
             elements: lines.indexBuffer,
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
             ...color,
             ...size,
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
@@ -234,6 +238,7 @@ export namespace Lines {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
     }
 }

+ 6 - 1
src/mol-geo/geometry/mesh/mesh.ts

@@ -6,7 +6,7 @@
  */
 
 import { ValueCell } from '../../../mol-util';
-import { Vec3, Mat4, Mat3 } from '../../../mol-math/linear-algebra';
+import { Vec3, Mat4, Mat3, Vec4 } from '../../../mol-math/linear-algebra';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { transformPositionArray, transformDirectionArray, computeIndexedVertexNormals, GroupMapping, createGroupMapping} from '../../util';
 import { GeometryUtils } from '../geometry';
@@ -23,6 +23,7 @@ import { Color } from '../../../mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
+import { createEmptyClipping } from '../clipping-data';
 
 export interface Mesh {
     readonly kind: 'mesh',
@@ -355,6 +356,7 @@ export namespace Mesh {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount };
 
@@ -368,10 +370,12 @@ export namespace Mesh {
             elements: mesh.indexBuffer,
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
             ...color,
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
@@ -405,6 +409,7 @@ export namespace Mesh {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
     }
 }

+ 6 - 1
src/mol-geo/geometry/points/points.ts

@@ -5,7 +5,7 @@
  */
 
 import { ValueCell } from '../../../mol-util';
-import { Mat4 } from '../../../mol-math/linear-algebra';
+import { Mat4, Vec4 } from '../../../mol-math/linear-algebra';
 import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
 import { GeometryUtils } from '../geometry';
 import { createColors } from '../color-data';
@@ -24,6 +24,7 @@ import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
+import { createEmptyClipping } from '../clipping-data';
 
 /** Point cloud */
 export interface Points {
@@ -143,6 +144,7 @@ export namespace Points {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: points.pointCount, groupCount, instanceCount };
 
@@ -154,11 +156,13 @@ export namespace Points {
             aGroup: points.groupBuffer,
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
             ...color,
             ...size,
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
@@ -192,6 +196,7 @@ export namespace Points {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
     }
 

+ 6 - 0
src/mol-geo/geometry/spheres/spheres.ts

@@ -22,6 +22,8 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
 import { GroupMapping, createGroupMapping } from '../../util';
+import { createEmptyClipping } from '../clipping-data';
+import { Vec4 } from '../../../mol-math/linear-algebra';
 
 export interface Spheres {
     readonly kind: 'spheres',
@@ -147,6 +149,7 @@ export namespace Spheres {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount };
 
@@ -161,11 +164,13 @@ export namespace Spheres {
             elements: spheres.indexBuffer,
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
             ...color,
             ...size,
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
 
             padding: ValueCell.create(padding),
@@ -200,6 +205,7 @@ export namespace Spheres {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
         ValueCell.update(values.padding, padding);
     }

+ 6 - 1
src/mol-geo/geometry/text/text.ts

@@ -18,7 +18,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
 import { TextureImage, createTextureImage, calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { TextValues } from '../../../mol-gl/renderable/text';
 import { Color } from '../../../mol-util/color';
-import { Vec3 } from '../../../mol-math/linear-algebra';
+import { Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { FontAtlasParams } from './font-atlas';
 import { RenderableState } from '../../../mol-gl/renderable';
 import { clamp } from '../../../mol-math/interpolate';
@@ -28,6 +28,7 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
 import { GroupMapping, createGroupMapping } from '../../util';
+import { createEmptyClipping } from '../clipping-data';
 
 type TextAttachment = (
     'bottom-left' | 'bottom-center' | 'bottom-right' |
@@ -195,6 +196,7 @@ export namespace Text {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: text.charCount * 2 * 3, groupCount, instanceCount };
 
@@ -210,11 +212,13 @@ export namespace Text {
             elements: text.indexBuffer,
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
             ...color,
             ...size,
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
 
             aTexCoord: text.tcoordBuffer,
@@ -269,6 +273,7 @@ export namespace Text {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
         ValueCell.update(values.padding, padding);
     }

+ 7 - 2
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -20,8 +20,9 @@ import { createEmptyTransparency } from '../transparency-data';
 import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
 import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Texture } from '../../../mol-gl/webgl/texture';
-import { Vec2 } from '../../../mol-math/linear-algebra';
+import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
 import { fillSerial } from '../../../mol-util/array';
+import { createEmptyClipping } from '../clipping-data';
 
 export interface TextureMesh {
     readonly kind: 'texture-mesh',
@@ -93,6 +94,7 @@ export namespace TextureMesh {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
 
         const counts = { drawCount: textureMesh.vertexCount, groupCount, instanceCount };
 
@@ -107,11 +109,13 @@ export namespace TextureMesh {
             aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount))),
             boundingSphere: ValueCell.create(transformBoundingSphere),
             invariantBoundingSphere: ValueCell.create(Sphere3D.clone(textureMesh.boundingSphere)),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(textureMesh.boundingSphere)),
 
             ...color,
             ...marker,
             ...overpaint,
             ...transparency,
+            ...clipping,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
@@ -149,6 +153,7 @@ export namespace TextureMesh {
         }
         if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
         }
     }
 }

+ 5 - 1
src/mol-gl/_spec/renderer.spec.ts

@@ -7,7 +7,7 @@
 import { createGl } from './gl.shim';
 
 import { Camera } from '../../mol-canvas3d/camera';
-import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
+import { Vec3, Mat4, Vec4 } from '../../mol-math/linear-algebra';
 import { ValueCell } from '../../mol-util';
 
 import Renderer from '../renderer';
@@ -24,6 +24,7 @@ import { Color } from '../../mol-util/color';
 import { Sphere3D } from '../../mol-math/geometry';
 import { createEmptyOverpaint } from '../../mol-geo/geometry/overpaint-data';
 import { createEmptyTransparency } from '../../mol-geo/geometry/transparency-data';
+import { createEmptyClipping } from '../../mol-geo/geometry/clipping-data';
 
 function createRenderer(gl: WebGLRenderingContext) {
     const ctx = createContext(gl);
@@ -43,6 +44,7 @@ function createPoints() {
     const marker = createEmptyMarkers();
     const overpaint = createEmptyOverpaint();
     const transparency = createEmptyTransparency();
+    const clipping = createEmptyClipping();
 
     const aTransform = ValueCell.create(new Float32Array(16));
     const m4 = Mat4.identity();
@@ -63,10 +65,12 @@ function createPoints() {
         ...size,
         ...overpaint,
         ...transparency,
+        ...clipping,
 
         uAlpha: ValueCell.create(1.0),
         uInstanceCount: ValueCell.create(1),
         uGroupCount: ValueCell.create(3),
+        uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere.ref.value)),
 
         alpha: ValueCell.create(1.0),
         drawCount: ValueCell.create(3),

+ 7 - 0
src/mol-gl/renderable/direct-volume.ts

@@ -29,8 +29,15 @@ export const DirectVolumeSchema = {
     dTransparency: DefineSpec('boolean'),
     dTransparencyVariant: DefineSpec('string', ['single', 'multi']),
 
+    dClipObjectCount: DefineSpec('number'),
+    dClipVariant: DefineSpec('string', ['instance', 'pixel']),
+    uClippingTexDim: UniformSpec('v2'),
+    tClipping: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    dClipping: DefineSpec('boolean'),
+
     uInstanceCount: UniformSpec('i'),
     uGroupCount: UniformSpec('i'),
+    uInvariantBoundingSphere: UniformSpec('v4'),
 
     aInstance: AttributeSpec('float32', 1, 1),
     aTransform: AttributeSpec('float32', 16, 1),

+ 26 - 44
src/mol-gl/renderable/schema.ts

@@ -1,22 +1,21 @@
 /**
- * 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 { ValueCell } from '../../mol-util';
-import { AttributeItemSize, ElementsKind, AttributeValues, AttributeKind } from '../webgl/buffer';
-import { UniformKind, UniformValues } from '../webgl/uniform';
+import { AttributeItemSize, ElementsKind, AttributeValues, AttributeKind, DataTypeArrayType } from '../webgl/buffer';
+import { UniformKind, UniformValues, UniformKindValue } from '../webgl/uniform';
 import { DefineKind, DefineValues } from '../shader-code';
-import { Vec2, Vec3, Vec4, Mat3, Mat4 } from '../../mol-math/linear-algebra';
-import { TextureImage, TextureVolume } from './util';
-import { TextureValues, TextureType, TextureFormat, TextureFilter, TextureKind, Texture } from '../webgl/texture';
+import { Mat4 } from '../../mol-math/linear-algebra';
+import { TextureValues, TextureType, TextureFormat, TextureFilter, TextureKind, TextureKindValue } from '../webgl/texture';
 import { Sphere3D } from '../../mol-math/geometry';
 
 export type ValueKindType = {
     'number': number
     'string': string
-    'boolean': string
+    'boolean': boolean
     'any': any
 
     'm4': Mat4,
@@ -27,40 +26,7 @@ export type ValueKind = keyof ValueKindType
 
 //
 
-export type KindValue = {
-    'f': number
-    'i': number
-    'v2': Vec2
-    'v3': Vec3
-    'v4': Vec4
-    'm3': Mat3
-    'm4': Mat4
-    't': number
-
-    'uint8': Uint8Array
-    'int8': Int8Array
-    'uint16': Uint16Array
-    'int16': Int16Array
-    'uint32': Uint32Array
-    'int32': Int32Array
-    'float32': Float32Array
-
-    'image-uint8': TextureImage<Uint8Array>
-    'image-float32': TextureImage<Float32Array>
-    'image-depth': TextureImage<Uint8Array> // TODO should be Uint32Array
-    'volume-uint8': TextureVolume<Uint8Array>
-    'volume-float32': TextureVolume<Float32Array>
-    'texture': Texture
-    'texture2d': Texture
-    'texture3d': Texture
-
-    'number': number
-    'string': string
-    'boolean': boolean
-    'any': any
-
-    'sphere': Sphere3D
-}
+export type KindValue = UniformKindValue & DataTypeArrayType & TextureKindValue & ValueKindType
 
 export type Values<S extends RenderableSchema> = { readonly [k in keyof S]: ValueCell<KindValue[S[k]['kind']]> }
 
@@ -163,6 +129,11 @@ export const GlobalUniformSchema = {
 
     uTransparentBackground: UniformSpec('i'),
 
+    uClipObjectType: UniformSpec('i[]'),
+    uClipObjectPosition: UniformSpec('v3[]'),
+    uClipObjectRotation: UniformSpec('v4[]'),
+    uClipObjectScale: UniformSpec('v3[]'),
+
     // all the following could in principle be per object
     // as a kind of 'material' parameter set
     // would need to test performance implications
@@ -228,21 +199,31 @@ export type OverpaintSchema = typeof OverpaintSchema
 export type OverpaintValues = Values<OverpaintSchema>
 
 export const TransparencySchema = {
-    // aTransparency: AttributeSpec('float32', 1, 0), // TODO
     uTransparencyTexDim: UniformSpec('v2'),
     tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     dTransparency: DefineSpec('boolean'),
-    // dTransparencyType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']), // TODO
     dTransparencyVariant: DefineSpec('string', ['single', 'multi']),
 } as const;
 export type TransparencySchema = typeof TransparencySchema
 export type TransparencyValues = Values<TransparencySchema>
 
+export const ClippingSchema = {
+    dClipObjectCount: DefineSpec('number'),
+    dClipVariant: DefineSpec('string', ['instance', 'pixel']),
+
+    uClippingTexDim: UniformSpec('v2'),
+    tClipping: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    dClipping: DefineSpec('boolean'),
+} as const;
+export type ClippingSchema = typeof ClippingSchema
+export type ClippingValues = Values<ClippingSchema>
+
 export const BaseSchema = {
     ...ColorSchema,
     ...MarkerSchema,
     ...OverpaintSchema,
     ...TransparencySchema,
+    ...ClippingSchema,
 
     aInstance: AttributeSpec('float32', 1, 1),
     aGroup: AttributeSpec('float32', 1, 0),
@@ -258,6 +239,7 @@ export const BaseSchema = {
     uAlpha: UniformSpec('f', true),
     uInstanceCount: UniformSpec('i'),
     uGroupCount: UniformSpec('i'),
+    uInvariantBoundingSphere: UniformSpec('v4'),
 
     drawCount: ValueSpec('number'),
     instanceCount: ValueSpec('number'),
@@ -272,7 +254,7 @@ export const BaseSchema = {
     /** additional per-instance transform, see aTransform */
     extraTransform: ValueSpec('float32'),
 
-    /** bounding sphere taking aTransform into account */
+    /** bounding sphere taking aTransform into account and encompases all instances */
     boundingSphere: ValueSpec('sphere'),
     /** bounding sphere NOT taking aTransform into account */
     invariantBoundingSphere: ValueSpec('sphere'),

+ 75 - 2
src/mol-gl/renderer.ts

@@ -9,13 +9,15 @@ import { Camera } from '../mol-canvas3d/camera';
 
 import Scene from './scene';
 import { WebGLContext } from './webgl/context';
-import { Mat4, Vec3, Vec4, Vec2 } from '../mol-math/linear-algebra';
+import { Mat4, Vec3, Vec4, Vec2, Quat } from '../mol-math/linear-algebra';
 import { Renderable } from './renderable';
 import { Color } from '../mol-util/color';
-import { ValueCell } from '../mol-util';
+import { ValueCell, deepEqual } from '../mol-util';
 import { RenderableValues, GlobalUniformValues, BaseValues } from './renderable/schema';
 import { GraphicsRenderVariant } from './webgl/render-item';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
+import { Clipping } from '../mol-theme/clipping';
+import { stringToWords } from '../mol-util/string';
 
 export interface RendererStats {
     programCount: number
@@ -71,6 +73,19 @@ export const RendererParams = {
         metallic: PD.Group({}),
         plastic: PD.Group({}),
     }, { label: 'Lighting', description: 'Style in which the 3D scene is rendered/lighted' }),
+
+    clip: PD.Group({
+        variant: PD.Select('instance', PD.arrayToOptions<Clipping.Variant>(['instance', 'pixel'])),
+        objects: PD.ObjectList({
+            type: PD.Select('plane', PD.objectToOptions(Clipping.Type, t => stringToWords(t))),
+            position: PD.Vec3(Vec3()),
+            rotation: PD.Group({
+                axis: PD.Vec3(Vec3.create(1, 0, 0)),
+                angle: PD.Numeric(0, { min: -180, max: 180, step: 0.1 }),
+            }, { isExpanded: true }),
+            scale: PD.Vec3(Vec3.create(1, 1, 1)),
+        }, o => stringToWords(o.type))
+    })
 };
 export type RendererProps = PD.Values<typeof RendererParams>
 
@@ -106,11 +121,44 @@ function getStyle(props: RendererProps['style']) {
     }
 }
 
+type Clip = {
+    variant: Clipping.Variant
+    objects: {
+        count: number
+        type: number[]
+        position: number[]
+        rotation: number[]
+        scale: number[]
+    }
+}
+
+const tmpQuat = Quat();
+function getClip(props: RendererProps['clip'], clip?: Clip): Clip {
+    const { type, position, rotation, scale } = clip?.objects || {
+        type: (new Array(5)).fill(1),
+        position: (new Array(5 * 3)).fill(0),
+        rotation: (new Array(5 * 4)).fill(0),
+        scale: (new Array(5 * 3)).fill(1),
+    };
+    for (let i = 0, il = props.objects.length; i < il; ++i) {
+        const p = props.objects[i];
+        type[i] = Clipping.Type[p.type];
+        Vec3.toArray(p.position, position, i * 3);
+        Quat.toArray(Quat.setAxisAngle(tmpQuat, p.rotation.axis, p.rotation.angle), rotation, i * 4);
+        Vec3.toArray(p.scale, scale, i * 3);
+    }
+    return {
+        variant: props.variant,
+        objects: { count: props.objects.length, type, position, rotation, scale }
+    };
+}
+
 namespace Renderer {
     export function create(ctx: WebGLContext, props: Partial<RendererProps> = {}): Renderer {
         const { gl, state, stats } = ctx;
         const p = PD.merge(RendererParams, PD.getDefaultValues(RendererParams), props);
         const style = getStyle(p.style);
+        const clip = getClip(p.clip);
 
         const viewport = Viewport();
         const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor);
@@ -151,6 +199,11 @@ namespace Renderer {
             uFogColor: ValueCell.create(bgColor),
             uTransparentBackground: ValueCell.create(0),
 
+            uClipObjectType: ValueCell.create(clip.objects.type),
+            uClipObjectPosition: ValueCell.create(clip.objects.position),
+            uClipObjectRotation: ValueCell.create(clip.objects.rotation),
+            uClipObjectScale: ValueCell.create(clip.objects.scale),
+
             // the following are general 'material' uniforms
             uLightIntensity: ValueCell.create(style.lightIntensity),
             uAmbientIntensity: ValueCell.create(style.ambientIntensity),
@@ -177,6 +230,17 @@ namespace Renderer {
                 return;
             }
 
+            let definesNeedUpdate = false;
+            if (r.values.dClipObjectCount.ref.value !== clip.objects.count) {
+                ValueCell.update(r.values.dClipObjectCount, clip.objects.count);
+                definesNeedUpdate = true;
+            }
+            if (r.values.dClipVariant.ref.value !== clip.variant) {
+                ValueCell.update(r.values.dClipVariant, clip.variant);
+                definesNeedUpdate = true;
+            }
+            if (definesNeedUpdate) r.update();
+
             const program = r.getProgram(variant);
             if (state.currentProgramId !== program.id) {
                 // console.log('new program')
@@ -341,6 +405,15 @@ namespace Renderer {
                     ValueCell.updateIfChanged(globalUniforms.uRoughness, style.roughness);
                     ValueCell.updateIfChanged(globalUniforms.uReflectivity, style.reflectivity);
                 }
+
+                if (props.clip !== undefined && !deepEqual(props.clip, p.clip)) {
+                    p.clip = props.clip;
+                    Object.assign(clip, getClip(props.clip, clip));
+                    ValueCell.update(globalUniforms.uClipObjectPosition, clip.objects.position);
+                    ValueCell.update(globalUniforms.uClipObjectRotation, clip.objects.rotation);
+                    ValueCell.update(globalUniforms.uClipObjectScale, clip.objects.scale);
+                    ValueCell.update(globalUniforms.uClipObjectType, clip.objects.type);
+                }
             },
             setViewport: (x: number, y: number, width: number, height: number) => {
                 gl.viewport(x, y, width, height);

+ 9 - 1
src/mol-gl/shader-code.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>
  */
@@ -34,6 +34,7 @@ import apply_fog from './shader/chunks/apply-fog.glsl';
 import apply_interior_color from './shader/chunks/apply-interior-color.glsl';
 import apply_light_color from './shader/chunks/apply-light-color.glsl';
 import apply_marker_color from './shader/chunks/apply-marker-color.glsl';
+import assign_clipping_varying from './shader/chunks/assign-clipping-varying.glsl';
 import assign_color_varying from './shader/chunks/assign-color-varying.glsl';
 import assign_group from './shader/chunks/assign-group.glsl';
 import assign_marker_varying from './shader/chunks/assign-marker-varying.glsl';
@@ -41,8 +42,11 @@ import assign_material_color from './shader/chunks/assign-material-color.glsl';
 import assign_position from './shader/chunks/assign-position.glsl';
 import assign_size from './shader/chunks/assign-size.glsl';
 import check_picking_alpha from './shader/chunks/check-picking-alpha.glsl';
+import clip_instance from './shader/chunks/clip-instance.glsl';
+import clip_pixel from './shader/chunks/clip-pixel.glsl';
 import color_frag_params from './shader/chunks/color-frag-params.glsl';
 import color_vert_params from './shader/chunks/color-vert-params.glsl';
+import common_clip from './shader/chunks/common-clip.glsl';
 import common_frag_params from './shader/chunks/common-frag-params.glsl';
 import common_vert_params from './shader/chunks/common-vert-params.glsl';
 import common from './shader/chunks/common.glsl';
@@ -59,6 +63,7 @@ const ShaderChunks: { [k: string]: string } = {
     apply_interior_color,
     apply_light_color,
     apply_marker_color,
+    assign_clipping_varying,
     assign_color_varying,
     assign_group,
     assign_marker_varying,
@@ -66,8 +71,11 @@ const ShaderChunks: { [k: string]: string } = {
     assign_position,
     assign_size,
     check_picking_alpha,
+    clip_instance,
+    clip_pixel,
     color_frag_params,
     color_vert_params,
+    common_clip,
     common_frag_params,
     common_vert_params,
     common,

+ 5 - 0
src/mol-gl/shader/chunks/assign-clipping-varying.glsl.ts

@@ -0,0 +1,5 @@
+export default `
+#if dClipObjectCount != 0 && defined(dClipping)
+    vClipping = readFromTexture(tClipping, aInstance * float(uGroupCount) + group, uClippingTexDim).a;
+#endif
+`;

+ 5 - 2
src/mol-gl/shader/chunks/assign-position.glsl.ts

@@ -1,11 +1,14 @@
 export default `
-mat4 modelView = uView * uModel * aTransform;
+mat4 model = uModel * aTransform;
+mat4 modelView = uView * model;
 #ifdef dGeoTexture
     vec3 position = readFromTexture(tPositionGroup, aGroup, uGeoTexDim).xyz;
 #else
     vec3 position = aPosition;
 #endif
-vec4 mvPosition = modelView * vec4(position, 1.0);
+vec4 position4 = vec4(position, 1.0);
+vModelPosition = (model * position4).xyz;
+vec4 mvPosition = modelView * position4;
 vViewPosition = mvPosition.xyz;
 gl_Position = uProjection * mvPosition;
 `;

+ 13 - 0
src/mol-gl/shader/chunks/clip-instance.glsl.ts

@@ -0,0 +1,13 @@
+export default `
+#if defined(dClipVariant_instance) && dClipObjectCount != 0
+    int flag = 0;
+    #if defined(dClipping)
+        flag = int(floor(vClipping * 255.0 + 0.5));
+    #endif
+
+    vec4 mCenter = model * vec4(uInvariantBoundingSphere.xyz, 1.0);
+    if (clipTest(vec4(mCenter.xyz, uInvariantBoundingSphere.w), flag))
+        // move out of [ -w, +w ] to 'discard' in vert shader
+        gl_Position.z = 2.0 * gl_Position.w;
+#endif
+`;

+ 11 - 0
src/mol-gl/shader/chunks/clip-pixel.glsl.ts

@@ -0,0 +1,11 @@
+export default `
+#if defined(dClipVariant_pixel) && dClipObjectCount != 0
+    int flag = 0;
+    #if defined(dClipping)
+        flag = int(floor(vClipping * 255.0 + 0.5));
+    #endif
+
+    if (clipTest(vec4(vModelPosition, 0.0), flag))
+        discard;
+#endif
+`;

+ 102 - 0
src/mol-gl/shader/chunks/common-clip.glsl.ts

@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Ludovic Autin <autin@scripps.edu>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export default `
+#if dClipObjectCount != 0
+    vec3 quaternionTransform(vec4 q, vec3 v) {
+        vec3 t = 2.0 * cross(q.xyz, v);
+        return v + q.w * t + cross(q.xyz, t);
+    }
+
+    vec4 computePlane(vec3 normal, vec3 inPoint) {
+        return vec4(normalize(normal), -dot(normal, inPoint));
+    }
+
+    float planeSD(vec4 plane, vec3 center) {
+        return -dot(plane.xyz, center - plane.xyz * -plane.w);
+    }
+
+    float sphereSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+        return (
+            length(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position) / size) - 1.0
+        ) * min(min(size.x, size.y), size.z);
+    }
+
+    float cubeSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+        vec3 d = abs(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position)) - size;
+        return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
+    }
+
+    float cylinderSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+        vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
+
+        vec2 d = abs(vec2(length(t.xz), t.y)) - size.xy;
+        return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
+    }
+
+    float infiniteConeSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+        vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
+
+        float q = length(t.xy);
+        return dot(size.xy, vec2(q, t.z));
+    }
+
+    float getSignedDistance(vec3 center, int type, vec3 position, vec4 rotation, vec3 scale) {
+        if (type == 1) {
+            vec3 normal = quaternionTransform(rotation, vec3(0.0, 1.0, 0.0));
+            vec4 plane = computePlane(normal, position);
+            return planeSD(plane, center);
+        } else if (type == 2) {
+            return sphereSD(position, rotation, scale * 0.5, center);
+        } else if (type == 3) {
+            return cubeSD(position, rotation, scale * 0.5, center);
+        } else if (type == 4) {
+            return cylinderSD(position, rotation, scale * 0.5, center);
+        } else if (type == 5) {
+            return infiniteConeSD(position, rotation, scale * 0.5, center);
+        } else {
+            return 0.1;
+        }
+    }
+
+    #if __VERSION__ != 300
+        // 8-bit
+        int bitwiseAnd(int a, int b) {
+            int d = 128;
+            int result = 0;
+            for (int i = 0; i < 8; ++i) {
+                if (d <= 0) break;
+                if (a >= d && b >= d) result += d;
+                if (a >= d) a -= d;
+                if (b >= d) b -= d;
+                d /= 2;
+            }
+            return result;
+        }
+
+        bool hasBit(int mask, int bit) {
+            return bitwiseAnd(mask, bit) == 0;
+        }
+    #else
+        bool hasBit(int mask, int bit) {
+            return (mask & bit) == 0;
+        }
+    #endif
+
+    // flag is a bit-flag for clip-objects to ignore (note, object ids start at 1 not 0)
+    bool clipTest(vec4 sphere, int flag) {
+        for (int i = 0; i < dClipObjectCount; ++i) {
+            if (flag == 0 || hasBit(flag, i + 1)) {
+                // TODO take sphere radius into account?
+                if (getSignedDistance(sphere.xyz, uClipObjectType[i], uClipObjectPosition[i], uClipObjectRotation[i], uClipObjectScale[i]) <= 0.0)
+                    return true;
+            }
+        }
+        return false;
+    }
+#endif
+`;

+ 16 - 0
src/mol-gl/shader/chunks/common-frag-params.glsl.ts

@@ -3,6 +3,21 @@ uniform int uObjectId;
 uniform int uInstanceCount;
 uniform int uGroupCount;
 
+#if dClipObjectCount != 0
+    uniform int uClipObjectType[dClipObjectCount];
+    uniform vec3 uClipObjectPosition[dClipObjectCount];
+    uniform vec4 uClipObjectRotation[dClipObjectCount];
+    uniform vec3 uClipObjectScale[dClipObjectCount];
+
+    #if defined(dClipping)
+        #if __VERSION__ != 300
+            varying float vClipping;
+        #else
+            flat in float vClipping;
+        #endif
+    #endif
+#endif
+
 uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
 #if __VERSION__ != 300
@@ -11,6 +26,7 @@ uniform vec3 uSelectColor;
     flat in float vMarker;
 #endif
 
+varying vec3 vModelPosition;
 varying vec3 vViewPosition;
 
 uniform vec2 uViewOffset;

+ 19 - 0
src/mol-gl/shader/chunks/common-vert-params.glsl.ts

@@ -5,6 +5,24 @@ uniform vec3 uCameraPosition;
 uniform int uObjectId;
 uniform int uInstanceCount;
 uniform int uGroupCount;
+uniform vec4 uInvariantBoundingSphere;
+
+#if dClipObjectCount != 0
+    uniform int uClipObjectType[dClipObjectCount];
+    uniform vec3 uClipObjectPosition[dClipObjectCount];
+    uniform vec4 uClipObjectRotation[dClipObjectCount];
+    uniform vec3 uClipObjectScale[dClipObjectCount];
+
+    #if defined(dClipping)
+        uniform vec2 uClippingTexDim;
+        uniform sampler2D tClipping;
+        #if __VERSION__ != 300
+            varying float vClipping;
+        #else
+            flat out float vClipping;
+        #endif
+    #endif
+#endif
 
 uniform vec2 uMarkerTexDim;
 uniform sampler2D tMarker;
@@ -14,5 +32,6 @@ uniform sampler2D tMarker;
     flat out float vMarker;
 #endif
 
+varying vec3 vModelPosition;
 varying vec3 vViewPosition;
 `;

+ 3 - 0
src/mol-gl/shader/mesh.frag.ts

@@ -13,8 +13,11 @@ precision highp int;
 #include color_frag_params
 #include light_frag_params
 #include normal_frag_params
+#include common_clip
 
 void main() {
+    #include clip_pixel
+
     // Workaround for buggy gl_FrontFacing (e.g. on some integrated Intel GPUs)
     #if defined(enabledStandardDerivatives)
         vec3 fdx = dFdx(vViewPosition);

+ 4 - 1
src/mol-gl/shader/mesh.vert.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>
  */
@@ -12,6 +12,7 @@ precision highp int;
 #include read_from_texture
 #include common_vert_params
 #include color_vert_params
+#include common_clip
 
 #ifdef dGeoTexture
     uniform vec2 uGeoTexDim;
@@ -34,7 +35,9 @@ void main(){
     #include assign_group
     #include assign_color_varying
     #include assign_marker_varying
+    #include assign_clipping_varying
     #include assign_position
+    #include clip_instance
 
     #ifdef dGeoTexture
         vec3 normal = readFromTexture(tNormal, aGroup, uGeoTexDim).xyz;

+ 1 - 1
src/mol-gl/shader/text.vert.ts

@@ -64,7 +64,7 @@ void main(void){
     vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
 
     if (vTexCoord.x == 10.0) { // indicates background plane
-        // move a bit to the back, tkaing ditsnace to camera into account to avoid z-fighting
+        // move a bit to the back, taking distance to camera into account to avoid z-fighting
         offsetZ -= 0.001 * distance(uCameraPosition, (uProjection * mvCorner).xyz);
     }
 

+ 2 - 1
src/mol-gl/webgl/program.ts

@@ -86,7 +86,8 @@ function checkActiveUniforms(gl: GLRenderingContext, program: WebGLProgram, sche
                 // name assigned by `gl.shim.ts`, ignore for checks
                 continue;
             }
-            const spec = schema[name];
+            const baseName = name.replace(/[[0-9]+\]$/, ''); // 'array' uniforms
+            const spec = schema[baseName];
             if (spec === undefined) {
                 throw new Error(`missing 'uniform' or 'texture' with name '${name}' in schema`);
             }

+ 28 - 37
src/mol-gl/webgl/uniform.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,45 +8,33 @@ import { Mat3, Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
 import { ValueCell } from '../../mol-util';
 import { GLRenderingContext } from './compat';
 import { RenderableSchema } from '../../mol-gl/renderable/schema';
+import { ValueOf } from '../../mol-util/type-helpers';
 
 export type UniformKindValue = {
-    'f': number
-    'i': number
-    'v2': Vec2
-    'v3': Vec3
-    'v4': Vec4
-    'm3': Mat3
-    'm4': Mat4
-    't': number
+    'f': number; 'f[]': number[]
+    'i': number; 'i[]': number[]
+    'v2': Vec2; 'v2[]': number[]
+    'v3': Vec3; 'v3[]': number[]
+    'v4': Vec4; 'v4[]': number[]
+    'm3': Mat3; 'm3[]': number[]
+    'm4': Mat4; 'm4[]': number[]
+    't': number; 't[]': number[]
 }
 export type UniformKind = keyof UniformKindValue
-export type UniformType = number | Vec2 | Vec3 | Vec4 | Mat3 | Mat4
+export type UniformType = ValueOf<UniformKindValue>
 
 export type UniformValues = { [k: string]: ValueCell<UniformType> }
 export type UniformsList = [string, ValueCell<UniformType>][]
 
 export function getUniformType(gl: GLRenderingContext, kind: UniformKind) {
     switch (kind) {
-        case 'f': return gl.FLOAT;
-        case 'i': return gl.INT;
-        case 'v2': return gl.FLOAT_VEC2;
-        case 'v3': return gl.FLOAT_VEC3;
-        case 'v4': return gl.FLOAT_VEC4;
-        case 'm3': return gl.FLOAT_MAT3;
-        case 'm4': return gl.FLOAT_MAT4;
-        default: console.error(`unknown uniform kind '${kind}'`);
-    }
-}
-
-export function setUniform(gl: GLRenderingContext, location: WebGLUniformLocation | null, kind: UniformKind, value: any) {
-    switch (kind) {
-        case 'f': gl.uniform1f(location, value); break;
-        case 'i': case 't': gl.uniform1i(location, value); break;
-        case 'v2': gl.uniform2fv(location, value); break;
-        case 'v3': gl.uniform3fv(location, value); break;
-        case 'v4': gl.uniform4fv(location, value); break;
-        case 'm3': gl.uniformMatrix3fv(location, false, value); break;
-        case 'm4': gl.uniformMatrix4fv(location, false, value); break;
+        case 'f': case 'f[]': return gl.FLOAT;
+        case 'i': case 'i[]': return gl.INT;
+        case 'v2': case 'v2[]': return gl.FLOAT_VEC2;
+        case 'v3': case 'v3[]': return gl.FLOAT_VEC3;
+        case 'v4': case 'v4[]': return gl.FLOAT_VEC4;
+        case 'm3': case 'm3[]': return gl.FLOAT_MAT3;
+        case 'm4': case 'm4[]': return gl.FLOAT_MAT4;
         default: console.error(`unknown uniform kind '${kind}'`);
     }
 }
@@ -55,24 +43,27 @@ export type UniformSetter = (gl: GLRenderingContext, location: number, value: an
 export type UniformSetters = { [k: string]: UniformSetter }
 
 function uniform1f (gl: GLRenderingContext, location: number, value: any) { gl.uniform1f(location, value); }
+function uniform1fv (gl: GLRenderingContext, location: number, value: any) { gl.uniform1fv(location, value); }
 function uniform1i (gl: GLRenderingContext, location: number, value: any) { gl.uniform1i(location, value); }
+function uniform1iv (gl: GLRenderingContext, location: number, value: any) { gl.uniform1iv(location, value); }
 function uniform2fv (gl: GLRenderingContext, location: number, value: any) { gl.uniform2fv(location, value); }
 function uniform3fv (gl: GLRenderingContext, location: number, value: any) { gl.uniform3fv(location, value); }
 function uniform4fv (gl: GLRenderingContext, location: number, value: any) { gl.uniform4fv(location, value); }
 function uniformMatrix3fv (gl: GLRenderingContext, location: number, value: any) { gl.uniformMatrix3fv(location, false, value); }
 function uniformMatrix4fv (gl: GLRenderingContext, location: number, value: any) { gl.uniformMatrix4fv(location, false, value); }
 
-function getUniformSetter(kind: UniformKind) {
+function getUniformSetter(kind: UniformKind): UniformSetter {
     switch (kind) {
         case 'f': return uniform1f;
+        case 'f[]': return uniform1fv;
         case 'i': case 't': return uniform1i;
-        case 'v2': return uniform2fv;
-        case 'v3': return uniform3fv;
-        case 'v4': return uniform4fv;
-        case 'm3': return uniformMatrix3fv;
-        case 'm4': return uniformMatrix4fv;
+        case 'i[]': case 't[]': return uniform1iv;
+        case 'v2': case 'v2[]': return uniform2fv;
+        case 'v3': case 'v3[]': return uniform3fv;
+        case 'v4': case 'v4[]': return uniform4fv;
+        case 'm3': case 'm3[]': return uniformMatrix3fv;
+        case 'm4': case 'm4[]': return uniformMatrix4fv;
     }
-    throw new Error(`unknown uniform kind '${kind}'`);
 }
 
 export function getUniformSetters(schema: RenderableSchema) {

+ 13 - 0
src/mol-math/linear-algebra/3d/vec4.ts

@@ -20,6 +20,7 @@
 import Mat4 from './mat4';
 import { EPSILON } from '../3d';
 import { NumberArray } from '../../../mol-util/type-helpers';
+import { Sphere3D } from '../../geometry/primitives/sphere3d';
 
 interface Vec4 extends Array<number> { [d: number]: number, '@type': 'vec4', length: 4 }
 
@@ -53,6 +54,18 @@ namespace Vec4 {
         return out;
     }
 
+    export function fromSphere(out: Vec4, sphere: Sphere3D) {
+        out[0] = sphere.center[0];
+        out[1] = sphere.center[1];
+        out[2] = sphere.center[2];
+        out[3] = sphere.radius;
+        return out;
+    }
+
+    export function ofSphere(sphere: Sphere3D) {
+        return fromSphere(zero(), sphere);
+    }
+
     export function hasNaN(a: Vec4) {
         return isNaN(a[0]) || isNaN(a[1]) || isNaN(a[2]) || isNaN(a[3]);
     }

+ 65 - 0
src/mol-plugin-state/helpers/structure-clipping.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2019-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>
+ */
+
+import { Structure, StructureElement } from '../../mol-model/structure';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { PluginContext } from '../../mol-plugin/context';
+import { StateBuilder, StateObjectCell, StateSelection, StateTransform } from '../../mol-state';
+import { StructureComponentRef } from '../manager/structure/hierarchy-state';
+import { EmptyLoci, Loci } from '../../mol-model/loci';
+import { Clipping } from '../../mol-theme/clipping';
+
+type ClippingEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, clipping?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.ClippingStructureRepresentation3DFromBundle>>) => void
+const ClippingManagerTag = 'clipping-controls';
+
+export async function setStructureClipping(plugin: PluginContext, components: StructureComponentRef[], groups: Clipping.Groups, lociGetter: (structure: Structure) => StructureElement.Loci | EmptyLoci, types?: string[]) {
+    await eachRepr(plugin, components, (update, repr, clippingCell) => {
+        if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
+
+        const structure = repr.obj!.data.source.data;
+        // always use the root structure to get the loci so the clipping
+        // stays applicable as long as the root structure does not change
+        const loci = lociGetter(structure.root);
+        if (Loci.isEmpty(loci)) return;
+
+        const layer = {
+            bundle: StructureElement.Bundle.fromLoci(loci),
+            groups
+        };
+
+        if (clippingCell) {
+            const bundleLayers = [...clippingCell.params!.values.layers, layer];
+            const filtered = getFilteredBundle(bundleLayers, structure);
+            update.to(clippingCell).update(Clipping.toBundle(filtered));
+        } else {
+            const filtered = getFilteredBundle([layer], structure);
+            update.to(repr.transform.ref)
+                .apply(StateTransforms.Representation.ClippingStructureRepresentation3DFromBundle, Clipping.toBundle(filtered), { tags: ClippingManagerTag });
+        }
+    });
+}
+
+function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: ClippingEachReprCallback) {
+    const state = plugin.state.data;
+    const update = state.build();
+    for (const c of components) {
+        for (const r of c.representations) {
+            const clipping = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.ClippingStructureRepresentation3DFromBundle, r.cell.transform.ref).withTag(ClippingManagerTag));
+            callback(update, r.cell, clipping[0]);
+        }
+    }
+
+    return update.commit({ doNotUpdateCurrent: true });
+}
+
+/** filter clipping layers for given structure */
+function getFilteredBundle(layers: Clipping.BundleLayer[], structure: Structure) {
+    const clipping = Clipping.ofBundle(layers, structure.root);
+    const merged = Clipping.merge(clipping);
+    return Clipping.filter(merged, structure);
+}

+ 14 - 6
src/mol-plugin-state/manager/structure/component.ts

@@ -26,6 +26,8 @@ import { createStructureColorThemeParams, createStructureSizeThemeParams } from
 import { StructureSelectionQueries, StructureSelectionQuery } from '../../helpers/structure-selection-query';
 import { StructureRepresentation3D } from '../../transforms/representation';
 import { StructureHierarchyRef, StructureComponentRef, StructureRef, StructureRepresentationRef } from './hierarchy-state';
+import { Clipping } from '../../../mol-theme/clipping';
+import { setStructureClipping } from '../../helpers/structure-clipping';
 
 export { StructureComponentManager };
 
@@ -358,7 +360,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
         }, { canUndo: 'Add Selection' });
     }
 
-    async applyColor(params: StructureComponentManager.ColorParams, structures?: ReadonlyArray<StructureRef>) {
+    async applyTheme(params: StructureComponentManager.ThemeParams, structures?: ReadonlyArray<StructureRef>) {
         return this.plugin.dataTransaction(async () => {
             const xs = structures || this.currentStructures;
             if (xs.length === 0) return;
@@ -367,12 +369,15 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
             for (const s of xs) {
                 if (params.action.name === 'reset') {
                     await clearStructureOverpaint(this.plugin, s.components, params.representations);
-                } else {
+                } else if (params.action.name === 'color') {
                     const p = params.action.params;
                     await setStructureOverpaint(this.plugin, s.components, p.color, getLoci, params.representations, p.opacity);
+                } else if (params.action.name === 'clipping') {
+                    const p = params.action.params;
+                    await setStructureClipping(this.plugin, s.components, Clipping.Groups.fromNames(p.excludeGroups), getLoci, params.representations);
                 }
             }
-        }, { canUndo: 'Apply Color' });
+        }, { canUndo: 'Apply Theme' });
     }
 
     private modifyComponent(builder: StateBuilder.Root, component: StructureComponentRef, by: Structure, action: StructureComponentManager.ModifyAction) {
@@ -448,19 +453,22 @@ namespace StructureComponentManager {
     }
     export type AddParams = { selection: StructureSelectionQuery, options: { checkExisting: boolean, label: string }, representation: string }
 
-    export function getColorParams(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
+    export function getThemeParams(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
         return {
             action: PD.MappedStatic('color', {
                 color: PD.Group({
                     color: PD.Color(ColorNames.blue, { isExpanded: true }),
                     opacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
                 }, { isFlat: true }),
-                reset: PD.EmptyGroup()
+                reset: PD.EmptyGroup(),
+                clipping: PD.Group({
+                    excludeGroups: PD.MultiSelect([] as Clipping.Groups.Names[], PD.objectToOptions(Clipping.Groups.Names)),
+                }, { isFlat: true }),
             }),
             representations: PD.MultiSelect([], getRepresentationTypes(plugin, pivot), { emptyValue: 'All' })
         };
     }
-    export type ColorParams = PD.Values<ReturnType<typeof getColorParams>>
+    export type ThemeParams = PD.Values<ReturnType<typeof getThemeParams>>
 
     export function getRepresentationTypes(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
         return pivot?.cell.obj?.data

+ 97 - 0
src/mol-plugin-state/transforms/representation.ts

@@ -34,6 +34,8 @@ import { OrientationRepresentation, OrientationParams } from '../../mol-repr/sha
 import { AngleParams, AngleRepresentation } from '../../mol-repr/shape/loci/angle';
 import { DihedralParams, DihedralRepresentation } from '../../mol-repr/shape/loci/dihedral';
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
+import { Clipping } from '../../mol-theme/clipping';
+import { ObjectKeys } from '../../mol-util/type-helpers';
 
 export { StructureRepresentation3D };
 export { ExplodeStructureRepresentation3D };
@@ -42,6 +44,8 @@ export { OverpaintStructureRepresentation3DFromScript };
 export { OverpaintStructureRepresentation3DFromBundle };
 export { TransparencyStructureRepresentation3DFromScript };
 export { TransparencyStructureRepresentation3DFromBundle };
+export { ClippingStructureRepresentation3DFromScript };
+export { ClippingStructureRepresentation3DFromBundle };
 export { VolumeRepresentation3D };
 
 type StructureRepresentation3D = typeof StructureRepresentation3D
@@ -438,6 +442,99 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
     }
 });
 
+type ClippingStructureRepresentation3DFromScript = typeof ClippingStructureRepresentation3DFromScript
+const ClippingStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn({
+    name: 'clipping-structure-representation-3d-from-script',
+    display: 'Clipping 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: {
+        layers: PD.ObjectList({
+            script: PD.Script(Script('(sel.atom.all)', 'mol-script')),
+            groups: PD.Converted((g: Clipping.Groups) => Clipping.Groups.toNames(g), n => Clipping.Groups.fromNames(n), PD.MultiSelect(ObjectKeys(Clipping.Groups.Names), PD.objectToOptions(Clipping.Groups.Names))),
+        }, e => `${Clipping.Groups.toNames(e.groups).length} group(s)`, {
+            defaultValue: [{
+                script: Script('(sel.atom.all)', 'mol-script'),
+                groups: Clipping.Groups.Flag.None,
+            }]
+        }),
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        const structure = a.data.source.data;
+        const clipping = Clipping.ofScript(params.layers, structure);
+
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { clipping },
+            initialState: { clipping: Clipping.Empty },
+            info: structure,
+            source: a
+        }, { label: `Clipping (${clipping.layers.length} Layers)` });
+    },
+    update({ a, b, newParams, oldParams }) {
+        const structure = b.data.info as Structure;
+        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
+        const oldClipping = b.data.state.clipping!;
+        const newClipping = Clipping.ofScript(newParams.layers, structure);
+        if (Clipping.areEqual(oldClipping, newClipping)) return StateTransformer.UpdateResult.Unchanged;
+
+        b.data.state.clipping = newClipping;
+        b.data.source = a;
+        b.label = `Clipping (${newClipping.layers.length} Layers)`;
+        return StateTransformer.UpdateResult.Updated;
+    }
+});
+
+type ClippingStructureRepresentation3DFromBundle = typeof ClippingStructureRepresentation3DFromBundle
+const ClippingStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn({
+    name: 'clipping-structure-representation-3d-from-bundle',
+    display: 'Clipping 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: {
+        layers: PD.ObjectList({
+            bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
+            groups: PD.Converted((g: Clipping.Groups) => Clipping.Groups.toNames(g), n => Clipping.Groups.fromNames(n), PD.MultiSelect(ObjectKeys(Clipping.Groups.Names), PD.objectToOptions(Clipping.Groups.Names))),
+        }, e => `${Clipping.Groups.toNames(e.groups).length} group(s)`, {
+            defaultValue: [{
+                bundle: StructureElement.Bundle.Empty,
+                groups: Clipping.Groups.Flag.None,
+            }],
+            isHidden: true
+        }),
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        const structure = a.data.source.data;
+        const clipping = Clipping.ofBundle(params.layers, structure);
+
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { clipping },
+            initialState: { clipping: Clipping.Empty },
+            info: structure,
+            source: a
+        }, { label: `Clipping (${clipping.layers.length} Layers)` });
+    },
+    update({ a, b, newParams, oldParams }) {
+        const structure = b.data.info as Structure;
+        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
+        const oldClipping = b.data.state.clipping!;
+        const newClipping = Clipping.ofBundle(newParams.layers, structure);
+        if (Clipping.areEqual(oldClipping, newClipping)) return StateTransformer.UpdateResult.Unchanged;
+
+        b.data.state.clipping = newClipping;
+        b.data.source = a;
+        b.label = `Clipping (${newClipping.layers.length} Layers)`;
+        return StateTransformer.UpdateResult.Updated;
+    }
+});
+
 //
 
 export namespace VolumeRepresentation3DHelpers {

+ 1 - 1
src/mol-plugin-ui/controls/parameters.tsx

@@ -1093,7 +1093,7 @@ class ObjectListItem extends React.PureComponent<ObjectListItemProps, { isExpand
     state = { isExpanded: false };
 
     update = (v: object) => {
-        this.setState({ isExpanded: false });
+        // this.setState({ isExpanded: false }); // TODO auto update? mark changed state?
         this.props.actions.update(v, this.props.index);
     }
 

+ 12 - 13
src/mol-plugin-ui/structure/selection.tsx

@@ -43,7 +43,6 @@ export class ToggleSelectionModeButton extends PurePluginUIComponent<{ inline?:
     }
 }
 
-
 const StructureSelectionParams = {
     granularity: InteractivityManager.Params.granularity,
 };
@@ -53,7 +52,7 @@ interface StructureSelectionActionsControlsState {
     isBusy: boolean,
     canUndo: boolean,
 
-    action?: StructureSelectionModifier | 'color' | 'add-repr' | 'help'
+    action?: StructureSelectionModifier | 'theme' | 'add-repr' | 'help'
 }
 
 const ActionHeader = new Map<StructureSelectionModifier, string>([
@@ -159,7 +158,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
     toggleRemove = this.showAction('remove')
     toggleIntersect = this.showAction('intersect')
     toggleSet = this.showAction('set')
-    toggleColor = this.showAction('color')
+    toggleTheme = this.showAction('theme')
     toggleAddRepr = this.showAction('add-repr')
     toggleHelp = this.showAction('help')
 
@@ -196,7 +195,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                 <ToggleButton icon={IntersectSvg} title={`${ActionHeader.get('intersect')}. Hold shift key to keep menu open.`} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
                 <ToggleButton icon={SetSvg} title={`${ActionHeader.get('set')}. Hold shift key to keep menu open.`} toggle={this.toggleSet} isSelected={this.state.action === 'set'} disabled={this.isDisabled} />
 
-                <ToggleButton icon={BrushSvg} title='Color Selection' toggle={this.toggleColor} isSelected={this.state.action === 'color'} disabled={this.isDisabled} style={{ marginLeft: '10px' }}  />
+                <ToggleButton icon={BrushSvg} title='Apply Theme to Selection' toggle={this.toggleTheme} isSelected={this.state.action === 'theme'} disabled={this.isDisabled} style={{ marginLeft: '10px' }}  />
                 <ToggleButton icon={CubeSvg} title='Create Representation of Selection' toggle={this.toggleAddRepr} isSelected={this.state.action === 'add-repr'} disabled={this.isDisabled} />
                 <IconButton svg={RemoveSvg} title='Subtract Selection from Representations' onClick={this.subtract} disabled={this.isDisabled} />
                 <IconButton svg={RestoreSvg} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} title={undoTitle} />
@@ -204,12 +203,12 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                 <ToggleButton icon={HelpOutlineSvg} title='Show/hide help' toggle={this.toggleHelp} style={{ marginLeft: '10px' }} isSelected={this.state.action === 'help'} />
                 <IconButton svg={CancelOutlinedSvg} title='Turn selection mode off' onClick={this.turnOff} />
             </div>
-            {(this.state.action && this.state.action !== 'color' && this.state.action !== 'add-repr' && this.state.action !== 'help') && <div className='msp-selection-viewport-controls-actions'>
+            {(this.state.action && this.state.action !== 'theme' && this.state.action !== 'add-repr' && this.state.action !== 'help') && <div className='msp-selection-viewport-controls-actions'>
                 <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} title='Click to close.' items={this.queries} onSelect={this.selectQuery} noOffset />
             </div>}
-            {this.state.action === 'color' && <div className='msp-selection-viewport-controls-actions'>
-                <ControlGroup header='Color' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor} topRightIcon={CloseSvg}>
-                    <ApplyColorControls onApply={this.toggleColor} />
+            {this.state.action === 'theme' && <div className='msp-selection-viewport-controls-actions'>
+                <ControlGroup header='Theme' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleTheme} topRightIcon={CloseSvg}>
+                    <ApplyThemeControls onApply={this.toggleTheme} />
                 </ControlGroup>
             </div>}
             {this.state.action === 'add-repr' && <div className='msp-selection-viewport-controls-actions'>
@@ -306,21 +305,21 @@ export class StructureSelectionStatsControls extends PluginUIComponent<{ hideOnE
 }
 
 interface ApplyColorControlsState {
-    values: StructureComponentManager.ColorParams
+    values: StructureComponentManager.ThemeParams
 }
 
 interface ApplyColorControlsProps {
     onApply?: () => void
 }
 
-class ApplyColorControls extends PurePluginUIComponent<ApplyColorControlsProps, ApplyColorControlsState> {
-    _params = memoizeLatest((pivot: StructureRef | undefined) => StructureComponentManager.getColorParams(this.plugin, pivot));
+class ApplyThemeControls extends PurePluginUIComponent<ApplyColorControlsProps, ApplyColorControlsState> {
+    _params = memoizeLatest((pivot: StructureRef | undefined) => StructureComponentManager.getThemeParams(this.plugin, pivot));
     get params() { return this._params(this.plugin.managers.structure.component.pivotStructure); }
 
     state = { values: ParamDefinition.getDefaultValues(this.params) };
 
     apply = () => {
-        this.plugin.managers.structure.component.applyColor(this.state.values);
+        this.plugin.managers.structure.component.applyTheme(this.state.values);
         this.props.onApply?.();
     }
 
@@ -330,7 +329,7 @@ class ApplyColorControls extends PurePluginUIComponent<ApplyColorControlsProps,
         return <>
             <ParameterControls params={this.params} values={this.state.values} onChangeValues={this.paramsChanged} />
             <Button icon={BrushSvg} className='msp-btn-commit msp-btn-commit-on' onClick={this.apply} style={{ marginTop: '1px' }}>
-                Apply Coloring
+                Apply Theme
             </Button>
         </>;
     }

+ 16 - 3
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -62,7 +62,10 @@ const SimpleSettingsParams = {
         outline: Canvas3DParams.postprocessing.params.outline,
         fog: Canvas3DParams.cameraFog,
     }, { pivot: 'renderStyle' }),
-    clipping: Canvas3DParams.cameraClipping,
+    clipping: PD.Group<any>({
+        ...Canvas3DParams.cameraClipping.params,
+        ...(Canvas3DParams.renderer.params.clip as any).params as any
+    }, { pivot: 'radius' }),
     layout: PD.MultiSelect([] as LayoutOptions[], PD.objectToOptions(LayoutOptions)),
 };
 
@@ -108,7 +111,10 @@ const SimpleSettingsMapping = ParamMapping({
                 outline: canvas.postprocessing.outline,
                 fog: canvas.cameraFog
             },
-            clipping: canvas.cameraClipping
+            clipping: {
+                ...canvas.cameraClipping,
+                ...canvas.renderer.clip
+            }
         };
     },
     update(s, props) {
@@ -122,7 +128,14 @@ const SimpleSettingsMapping = ParamMapping({
         canvas.postprocessing.occlusion = s.lighting.occlusion;
         canvas.postprocessing.outline = s.lighting.outline;
         canvas.cameraFog = s.lighting.fog;
-        canvas.cameraClipping = s.clipping;
+        canvas.cameraClipping = {
+            radius: s.clipping.radius,
+            far: s.clipping.far,
+        };
+        canvas.renderer.clip = {
+            variant: s.clipping.variant,
+            objects: s.clipping.objects,
+        };
 
         props.layout = s.layout;
     },

+ 6 - 2
src/mol-repr/representation.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -22,6 +22,7 @@ import { getQualityProps } from './util';
 import { BaseGeometry } from '../mol-geo/geometry/base';
 import { Visual } from './visual';
 import { CustomProperty } from '../mol-model-props/common/custom-property';
+import { Clipping } from '../mol-theme/clipping';
 
 // export interface RepresentationProps {
 //     visuals?: string[]
@@ -173,6 +174,8 @@ namespace Representation {
         overpaint: Overpaint
         /** Per group transparency applied to the representation's renderobjects */
         transparency: Transparency
+        /** Bit mask of per group clipping applied to the representation's renderobjects */
+        clipping: Clipping
         /** Controls if the representation's renderobjects are synced automatically with GPU or not */
         syncManually: boolean
         /** A transformation applied to the representation's renderobjects */
@@ -181,7 +184,7 @@ namespace Representation {
         markerActions: MarkerActions
     }
     export function createState(): State {
-        return { visible: true, alphaFactor: 1, pickable: true, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty, transparency: Transparency.Empty, markerActions: MarkerActions.All };
+        return { visible: true, alphaFactor: 1, pickable: true, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty, transparency: Transparency.Empty, clipping: Clipping.Empty, markerActions: MarkerActions.All };
     }
     export function updateState(state: State, update: Partial<State>) {
         if (update.visible !== undefined) state.visible = update.visible;
@@ -189,6 +192,7 @@ namespace Representation {
         if (update.pickable !== undefined) state.pickable = update.pickable;
         if (update.overpaint !== undefined) state.overpaint = update.overpaint;
         if (update.transparency !== undefined) state.transparency = update.transparency;
+        if (update.clipping !== undefined) state.clipping = update.clipping;
         if (update.syncManually !== undefined) state.syncManually = update.syncManually;
         if (update.transform !== undefined) Mat4.copy(state.transform, update.transform);
         if (update.markerActions !== undefined) state.markerActions = update.markerActions;

+ 6 - 0
src/mol-repr/structure/complex-representation.ts

@@ -18,6 +18,7 @@ import { EmptyLoci, Loci, isEveryLoci, isDataLoci } from '../../mol-model/loci';
 import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
 import { Overpaint } from '../../mol-theme/overpaint';
 import { StructureParams } from './params';
+import { Clipping } from '../../mol-theme/clipping';
 
 export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0;
@@ -94,6 +95,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
             visual.setOverpaint(remappedOverpaint);
         }
         if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency);
+        if (state.clipping !== undefined && visual) {
+            // Remap loci from equivalent structure to the current structure
+            const remappedClipping = Clipping.remap(state.clipping, _structure);
+            visual.setClipping(remappedClipping);
+        }
         if (state.transform !== undefined && visual) visual.setTransform(state.transform);
         if (state.unitTransforms !== undefined && visual) {
             // Since ComplexVisuals always renders geometries between units, the application

+ 4 - 0
src/mol-repr/structure/complex-visual.ts

@@ -30,6 +30,7 @@ import { SizeTheme } from '../../mol-theme/size';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 import { createMarkers } from '../../mol-geo/geometry/marker-data';
 import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams } from './params';
+import { Clipping } from '../../mol-theme/clipping';
 
 export interface  ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
 
@@ -217,6 +218,9 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
         setTransparency(transparency: Transparency) {
             Visual.setTransparency(renderObject, transparency, lociApply, true);
         },
+        setClipping(clipping: Clipping) {
+            Visual.setClipping(renderObject, clipping, lociApply, true);
+        },
         destroy() {
             // TODO
             renderObject = undefined;

+ 9 - 2
src/mol-repr/structure/units-representation.ts

@@ -23,6 +23,7 @@ import { Transparency } from '../../mol-theme/transparency';
 import { Mat4, EPSILON } from '../../mol-math/linear-algebra';
 import { Interval } from '../../mol-data/int';
 import { StructureParams } from './params';
+import { Clipping } from '../../mol-theme/clipping';
 
 export interface UnitsVisual<P extends StructureParams> extends Visual<StructureGroup, P> { }
 
@@ -191,13 +192,14 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
     }
 
     function setVisualState(visual: UnitsVisual<P>, group: Unit.SymmetryGroup, state: Partial<StructureRepresentationState>) {
-        const { visible, alphaFactor, pickable, overpaint, transparency, transform, unitTransforms } = state;
+        const { visible, alphaFactor, pickable, overpaint, transparency, clipping, transform, unitTransforms } = state;
 
         if (visible !== undefined) visual.setVisibility(visible);
         if (alphaFactor !== undefined) visual.setAlphaFactor(alphaFactor);
         if (pickable !== undefined) visual.setPickable(pickable);
         if (overpaint !== undefined) visual.setOverpaint(overpaint);
         if (transparency !== undefined) visual.setTransparency(transparency);
+        if (clipping !== undefined) visual.setClipping(clipping);
         if (transform !== undefined) visual.setTransform(transform);
         if (unitTransforms !== undefined) {
             if (unitTransforms) {
@@ -210,7 +212,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
     }
 
     function setState(state: Partial<StructureRepresentationState>) {
-        const { visible, alphaFactor, pickable, overpaint, transparency, transform, unitTransforms, syncManually, markerActions } = state;
+        const { visible, alphaFactor, pickable, overpaint, transparency, clipping, transform, unitTransforms, syncManually, markerActions } = state;
         const newState: Partial<StructureRepresentationState> = {};
 
         if (visible !== _state.visible) newState.visible = visible;
@@ -224,6 +226,11 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         if (transparency !== undefined && !Transparency.areEqual(transparency, _state.transparency)) {
             newState.transparency = transparency;
         }
+        if (clipping !== undefined && !Clipping.areEqual(clipping, _state.clipping)) {
+            if (_structure) {
+                newState.clipping = Clipping.remap(clipping, _structure);
+            }
+        }
         if (transform !== undefined && !Mat4.areEqual(transform, _state.transform, EPSILON)) {
             newState.transform = transform;
         }

+ 4 - 0
src/mol-repr/structure/units-visual.ts

@@ -36,6 +36,7 @@ import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 import { SizeValues } from '../../mol-gl/renderable/schema';
 import { StructureParams, StructureMeshParams, StructureSpheresParams, StructurePointsParams, StructureLinesParams, StructureTextParams, StructureDirectVolumeParams, StructureTextureMeshParams } from './params';
+import { Clipping } from '../../mol-theme/clipping';
 
 export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
 
@@ -268,6 +269,9 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
         setTransparency(transparency: Transparency) {
             Visual.setTransparency(renderObject, transparency, lociApply, true);
         },
+        setClipping(clipping: Clipping) {
+            Visual.setClipping(renderObject, clipping, lociApply, true);
+        },
         destroy() {
             // TODO
             renderObject = undefined;

+ 2 - 1
src/mol-repr/structure/visual/util/bond.ts

@@ -10,10 +10,11 @@ import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { LocationIterator } from '../../../../mol-geo/util/location-iterator';
 import { StructureGroup } from '../../units-visual';
 import { LinkCylinderParams } from './link';
+import { ObjectKeys } from '../../../../mol-util/type-helpers';
 
 export const BondCylinderParams = {
     ...LinkCylinderParams,
-    includeTypes: PD.MultiSelect(Object.keys(BondType.Names) as BondType.Names[], PD.objectToOptions(BondType.Names)),
+    includeTypes: PD.MultiSelect(ObjectKeys(BondType.Names), PD.objectToOptions(BondType.Names)),
     excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)),
 };
 export const DefaultBondCylinderProps = PD.getDefaultValues(BondCylinderParams);

+ 28 - 0
src/mol-repr/visual.ts

@@ -21,6 +21,8 @@ import { createOverpaint, clearOverpaint, applyOverpaintColor } from '../mol-geo
 import { Interval } from '../mol-data/int';
 import { Transparency } from '../mol-theme/transparency';
 import { createTransparency, clearTransparency, applyTransparencyValue } from '../mol-geo/geometry/transparency-data';
+import { Clipping } from '../mol-theme/clipping';
+import { createClipping, applyClippingGroups, clearClipping } from '../mol-geo/geometry/clipping-data';
 
 export interface VisualContext {
     readonly runtime: RuntimeContext
@@ -42,6 +44,7 @@ interface Visual<D, P extends PD.Params> {
     setTransform: (matrix?: Mat4, instanceMatrices?: Float32Array | null) => void
     setOverpaint: (overpaint: Overpaint) => void
     setTransparency: (transparency: Transparency) => void
+    setClipping: (clipping: Clipping) => void
     destroy: () => void
 }
 namespace Visual {
@@ -128,6 +131,31 @@ namespace Visual {
         ValueCell.update(tTransparency, tTransparency.ref.value);
     }
 
+    export function setClipping(renderObject: GraphicsRenderObject | undefined, clipping: Clipping, lociApply: LociApply, clear: boolean) {
+        if (!renderObject) return;
+
+        const { tClipping, uGroupCount, instanceCount } = renderObject.values;
+        const count = uGroupCount.ref.value * instanceCount.ref.value;
+
+        // ensure texture has right size
+        createClipping(clipping.layers.length ? count : 0, renderObject.values);
+        const { array } = tClipping.ref.value;
+
+        // clear if requested
+        if (clear) clearClipping(array, 0, count);
+
+        for (let i = 0, il = clipping.layers.length; i < il; ++i) {
+            const { loci, groups } = clipping.layers[i];
+            const apply = (interval: Interval) => {
+                const start = Interval.start(interval);
+                const end = Interval.end(interval);
+                return applyClippingGroups(array, start, end, groups);
+            };
+            lociApply(loci, apply, false);
+        }
+        ValueCell.update(tClipping, tClipping.ref.value);
+    }
+
     export function setTransform(renderObject: GraphicsRenderObject | undefined, transform?: Mat4, instanceTransforms?: Float32Array | null) {
         if (!renderObject || (!transform && !instanceTransforms)) return;
 

+ 4 - 0
src/mol-repr/volume/representation.ts

@@ -29,6 +29,7 @@ import { BaseGeometry } from '../../mol-geo/geometry/base';
 import { Subject } from 'rxjs';
 import { Task } from '../../mol-task';
 import { SizeValues } from '../../mol-gl/renderable/schema';
+import { Clipping } from '../../mol-theme/clipping';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<Volume, P> { }
 
@@ -191,6 +192,9 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
         setTransparency(transparency: Transparency) {
             return Visual.setTransparency(renderObject, transparency, lociApply, true);
         },
+        setClipping(clipping: Clipping) {
+            return Visual.setClipping(renderObject, clipping, lociApply, true);
+        },
         destroy() {
             // TODO
             renderObject = undefined;

+ 197 - 0
src/mol-theme/clipping.ts

@@ -0,0 +1,197 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci } from '../mol-model/loci';
+import { StructureElement, Structure } from '../mol-model/structure';
+import { Script } from '../mol-script/script';
+import BitFlags from '../mol-util/bit-flags';
+
+export { Clipping };
+
+type Clipping = { readonly layers: ReadonlyArray<Clipping.Layer> }
+
+function Clipping(layers: ReadonlyArray<Clipping.Layer>): Clipping {
+    return { layers };
+}
+
+namespace Clipping {
+    export type Layer = { readonly loci: StructureElement.Loci, readonly groups: Groups }
+    export const Empty: Clipping = { layers: [] };
+
+    export type Groups = BitFlags<Groups.Flag>
+    export namespace Groups {
+        export const is: (g: Groups, f: Flag) => boolean = BitFlags.has;
+        export const enum Flag {
+            None = 0x0,
+            One = 0x1,
+            Two = 0x2,
+            Three = 0x4,
+            Four = 0x8,
+            Five = 0x10,
+            Six = 0x20,
+        }
+
+        export function create(flags: Flag): Groups {
+            return BitFlags.create(flags);
+        }
+
+        export const Names = {
+            'one': Flag.One,
+            'two': Flag.Two,
+            'three': Flag.Three,
+            'four': Flag.Four,
+            'five': Flag.Five,
+            'six': Flag.Six,
+        };
+        export type Names = keyof typeof Names
+
+        export function isName(name: string): name is Names {
+            return name in Names;
+        }
+
+        export function fromName(name: Names): Flag {
+            switch (name) {
+                case 'one': return Flag.One;
+                case 'two': return Flag.Two;
+                case 'three': return Flag.Three;
+                case 'four': return Flag.Four;
+                case 'five': return Flag.Five;
+                case 'six': return Flag.Six;
+            }
+        }
+
+        export function fromNames(names: Names[]): Flag {
+            let f = Flag.None;
+            for (let i = 0, il = names.length; i < il; ++i) {
+                f |= fromName(names[i]);
+            }
+            return f;
+        }
+
+        export function toNames(groups: Groups): Names[] {
+            let names: Names[] = [];
+            if (is(groups, Flag.One)) names.push('one');
+            if (is(groups, Flag.Two)) names.push('two');
+            if (is(groups, Flag.Three)) names.push('three');
+            if (is(groups, Flag.Four)) names.push('four');
+            if (is(groups, Flag.Five)) names.push('five');
+            if (is(groups, Flag.Six)) names.push('six');
+            return names;
+        }
+    }
+
+    /** Clip object types */
+    export const Type = {
+        none: 0, // to switch clipping off
+        plane: 1,
+        sphere: 2,
+        cube: 3,
+        cylinder: 4,
+        infiniteCone: 5,
+    };
+
+    export type Variant = 'instance' | 'pixel'
+
+    export function areEqual(cA: Clipping, cB: Clipping) {
+        if (cA.layers.length === 0 && cB.layers.length === 0) return true;
+        if (cA.layers.length !== cB.layers.length) return false;
+        for (let i = 0, il = cA.layers.length; i < il; ++i) {
+            if (cA.layers[i].groups !== cB.layers[i].groups) return false;
+            if (!Loci.areEqual(cA.layers[i].loci, cB.layers[i].loci)) return false;
+        }
+        return true;
+    }
+
+    export function isEmpty(clipping: Clipping) {
+        return clipping.layers.length === 0;
+    }
+
+    export function remap(clipping: Clipping, structure: Structure) {
+        const layers: Clipping.Layer[] = [];
+        for (const layer of clipping.layers) {
+            let { loci, groups } = layer;
+            loci = StructureElement.Loci.remap(loci, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, groups });
+            }
+        }
+        return { layers };
+    }
+
+    export function merge(clipping: Clipping): Clipping {
+        if (isEmpty(clipping)) return clipping;
+        const { structure } = clipping.layers[0].loci;
+        const map = new Map<Groups, StructureElement.Loci>();
+        let shadowed = StructureElement.Loci.none(structure);
+        for (let i = 0, il = clipping.layers.length; i < il; ++i) {
+            let { loci, groups } = clipping.layers[il - i - 1]; // process from end
+            loci = StructureElement.Loci.subtract(loci, shadowed);
+            shadowed = StructureElement.Loci.union(loci, shadowed);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                if (map.has(groups)) {
+                    loci = StructureElement.Loci.union(loci, map.get(groups)!);
+                }
+                map.set(groups, loci);
+            }
+        }
+        const layers: Clipping.Layer[] = [];
+        map.forEach((loci, groups) => {
+            layers.push({ loci, groups });
+        });
+        return { layers };
+    }
+
+    export function filter(clipping: Clipping, filter: Structure): Clipping {
+        if (isEmpty(clipping)) return clipping;
+        const { structure } = clipping.layers[0].loci;
+        const layers: Clipping.Layer[] = [];
+        for (const layer of clipping.layers) {
+            let { loci, groups } = layer;
+            // filter by first map to the `filter` structure and
+            // then map back to the original structure of the clipping loci
+            const filtered = StructureElement.Loci.remap(loci, filter);
+            loci = StructureElement.Loci.remap(filtered, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, groups });
+            }
+        }
+        return { layers };
+    }
+
+    export type ScriptLayer = { script: Script, groups: Groups }
+    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Clipping {
+        const layers: Clipping.Layer[] = [];
+        for (let i = 0, il = scriptLayers.length; i < il; ++i) {
+            const { script, groups } = scriptLayers[i];
+            const loci = Script.toLoci(script, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, groups });
+            }
+        }
+        return { layers };
+    }
+
+    export type BundleLayer = { bundle: StructureElement.Bundle, groups: Groups }
+    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Clipping {
+        const layers: Clipping.Layer[] = [];
+        for (let i = 0, il = bundleLayers.length; i < il; ++i) {
+            const { bundle, groups } = bundleLayers[i];
+            const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
+            layers.push({ loci, groups });
+        }
+        return { layers };
+    }
+
+    export function toBundle(clipping: Clipping) {
+        const layers: BundleLayer[] = [];
+        for (let i = 0, il = clipping.layers.length; i < il; ++i) {
+            let { loci, groups } = clipping.layers[i];
+            const bundle = StructureElement.Bundle.fromLoci(loci);
+            layers.push({ bundle, groups });
+        }
+        return { layers };
+    }
+}

+ 2 - 2
src/mol-theme/transparency.ts

@@ -27,11 +27,11 @@ namespace Transparency {
         return true;
     }
 
-    export function ofScript(script: Script, value: number, variant: Transparency.Variant, structure: Structure): Transparency {
+    export function ofScript(script: Script, value: number, variant: Variant, structure: Structure): Transparency {
         return { loci: Script.toLoci(script, structure), value, variant };
     }
 
-    export function ofBundle(bundle: StructureElement.Bundle, value: number, variant: Transparency.Variant, structure: Structure): Transparency {
+    export function ofBundle(bundle: StructureElement.Bundle, value: number, variant: Variant, structure: Structure): Transparency {
         return { loci: StructureElement.Bundle.toLoci(bundle, structure), value, variant };
     }
 }

+ 1 - 1
src/mol-util/param-definition.ts

@@ -423,7 +423,7 @@ export namespace ParamDefinition {
         return false;
     }
 
-    export function merge(params: Params, a: any, b: any): any {
+    export function merge<P extends Params>(params: P, a: any, b: any): Values<P> {
         if (a === undefined) return { ...b };
         if (b === undefined) return { ...a };
 

+ 3 - 3
src/tests/browser/marching-cubes.ts

@@ -6,7 +6,7 @@
 
 import './index.html';
 import { resizeCanvas } from '../../mol-canvas3d/util';
-import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
+import { Canvas3DParams, Canvas3D } from '../../mol-canvas3d/canvas3d';
 import { ColorNames } from '../../mol-util/color/names';
 import { PositionData, Box3D, Sphere3D } from '../../mol-math/geometry';
 import { OrderedSet } from '../../mol-data/int';
@@ -31,10 +31,10 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
+const canvas3d = Canvas3D.fromCanvas(canvas, PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
     renderer: { backgroundColor: ColorNames.white },
     camera: { mode: 'orthographic' }
-});
+}));
 canvas3d.animate();
 
 async function init() {