JonStargaryen há 4 anos atrás
pai
commit
96ac561279
50 ficheiros alterados com 544 adições e 268 exclusões
  1. 9 9
      package.json
  2. 12 1
      src/mol-canvas3d/canvas3d.ts
  3. 2 2
      src/mol-geo/geometry/overpaint-data.ts
  4. 2 4
      src/mol-geo/geometry/transparency-data.ts
  5. 1 1
      src/mol-gl/_spec/renderer.spec.ts
  6. 9 0
      src/mol-gl/renderer.ts
  7. 1 1
      src/mol-gl/shader/chunks/assign-position.glsl.ts
  8. 1 1
      src/mol-gl/shader/chunks/clip-instance.glsl.ts
  9. 4 3
      src/mol-gl/shader/chunks/clip-pixel.glsl.ts
  10. 3 1
      src/mol-gl/shader/lines.frag.ts
  11. 9 3
      src/mol-gl/shader/lines.vert.ts
  12. 3 1
      src/mol-gl/shader/points.frag.ts
  13. 5 1
      src/mol-gl/shader/points.vert.ts
  14. 4 1
      src/mol-gl/shader/spheres.frag.ts
  15. 9 2
      src/mol-gl/shader/spheres.vert.ts
  16. 3 1
      src/mol-gl/shader/text.frag.ts
  17. 9 2
      src/mol-gl/shader/text.vert.ts
  18. 1 7
      src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
  19. 2 2
      src/mol-model/structure/model/types.ts
  20. 30 5
      src/mol-plugin-state/builder/structure/representation-preset.ts
  21. 6 6
      src/mol-plugin-state/helpers/structure-clipping.ts
  22. 10 10
      src/mol-plugin-state/helpers/structure-overpaint.ts
  23. 74 0
      src/mol-plugin-state/helpers/structure-transparency.ts
  24. 13 3
      src/mol-plugin-state/manager/camera.ts
  25. 25 9
      src/mol-plugin-state/manager/structure/component.ts
  26. 5 2
      src/mol-plugin-state/manager/structure/focus.ts
  27. 1 1
      src/mol-plugin-state/manager/structure/selection.ts
  28. 33 22
      src/mol-plugin-state/transforms/representation.ts
  29. 18 4
      src/mol-plugin-ui/controls/color.tsx
  30. 3 3
      src/mol-plugin-ui/controls/icons.tsx
  31. 2 2
      src/mol-plugin-ui/skin/base/components/controls.scss
  32. 2 2
      src/mol-plugin-ui/state/snapshots.tsx
  33. 1 1
      src/mol-plugin-ui/state/update-transform.tsx
  34. 30 33
      src/mol-plugin-ui/structure/components.tsx
  35. 3 2
      src/mol-plugin-ui/structure/focus.tsx
  36. 2 2
      src/mol-plugin-ui/structure/measurements.tsx
  37. 14 14
      src/mol-plugin-ui/structure/selection.tsx
  38. 1 1
      src/mol-plugin-ui/viewport.tsx
  39. 6 11
      src/mol-plugin/behavior/dynamic/camera.ts
  40. 20 20
      src/mol-plugin/behavior/dynamic/representation.ts
  41. 0 2
      src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts
  42. 1 6
      src/mol-plugin/behavior/static/representation.ts
  43. 2 2
      src/mol-plugin/context.ts
  44. 6 1
      src/mol-repr/structure/complex-representation.ts
  45. 3 1
      src/mol-repr/structure/units-representation.ts
  46. 16 18
      src/mol-repr/structure/visual/util/polymer/trace-iterator.ts
  47. 11 11
      src/mol-repr/visual.ts
  48. 2 2
      src/mol-state/state.ts
  49. 14 15
      src/mol-theme/overpaint.ts
  50. 101 14
      src/mol-theme/transparency.ts

+ 9 - 9
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "0.7.1-dev.4",
+  "version": "0.7.1-dev.11",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -92,8 +92,8 @@
     "@graphql-codegen/typescript-graphql-request": "^1.13.5",
     "@graphql-codegen/typescript-operations": "^1.13.5",
     "@types/cors": "^2.8.6",
-    "@typescript-eslint/eslint-plugin": "^2.32.0",
-    "@typescript-eslint/parser": "^2.32.0",
+    "@typescript-eslint/eslint-plugin": "^2.33.0",
+    "@typescript-eslint/parser": "^2.33.0",
     "benchmark": "^2.1.4",
     "concurrently": "^5.2.0",
     "cpx2": "^2.0.0",
@@ -112,18 +112,18 @@
     "simple-git": "^2.4.0",
     "style-loader": "^1.2.1",
     "ts-jest": "^25.5.1",
-    "typescript": "^3.8.3",
+    "typescript": "^3.9.2",
     "webpack": "^4.43.0",
     "webpack-cli": "^3.3.11",
     "webpack-version-file-plugin": "^0.4.0"
   },
   "dependencies": {
     "@types/argparse": "^1.0.38",
-    "@types/benchmark": "^1.0.31",
+    "@types/benchmark": "^1.0.33",
     "@types/compression": "1.7.0",
     "@types/express": "^4.17.6",
-    "@types/jest": "^25.2.1",
-    "@types/node": "^13.13.5",
+    "@types/jest": "^25.2.2",
+    "@types/node": "^14.0.1",
     "@types/node-fetch": "^2.5.7",
     "@types/react": "^16.9.35",
     "@types/react-dom": "^16.9.8",
@@ -139,8 +139,8 @@
     "react": "^16.13.1",
     "react-dom": "^16.13.1",
     "rxjs": "^6.5.5",
-    "swagger-ui-dist": "^3.25.2",
-    "tslib": "^1.11.2",
+    "swagger-ui-dist": "^3.25.3",
+    "tslib": "^2.0.0",
     "util.promisify": "^1.0.1",
     "xhr2": "^0.2.0"
   }

+ 12 - 1
src/mol-canvas3d/canvas3d.ts

@@ -265,6 +265,7 @@ namespace Canvas3D {
         }
 
         let forceNextDraw = false;
+        let forceDrawAfterAllCommited = false;
         let currentTime = 0;
 
         function draw(force?: boolean) {
@@ -300,7 +301,13 @@ namespace Canvas3D {
         function commit(isSynchronous: boolean = false) {
             const allCommited = commitScene(isSynchronous);
             // Only reset the camera after the full scene has been commited.
-            if (allCommited) resolveCameraReset();
+            if (allCommited) {
+                resolveCameraReset();
+                if (forceDrawAfterAllCommited) {
+                    draw(true);
+                    forceDrawAfterAllCommited = false;
+                }
+            }
         }
 
         function resolveCameraReset() {
@@ -393,6 +400,7 @@ namespace Canvas3D {
             reprRenderObjects.set(repr, newRO);
 
             scene.update(repr.renderObjects, false);
+            forceDrawAfterAllCommited = true;
             if (isDebugMode) consoleStats();
         }
 
@@ -404,6 +412,7 @@ namespace Canvas3D {
                 renderObjects.forEach(o => scene.remove(o));
                 reprRenderObjects.delete(repr);
                 scene.update(repr.renderObjects, false, true);
+                forceDrawAfterAllCommited = true;
                 if (isDebugMode) consoleStats();
             }
         }
@@ -470,6 +479,7 @@ namespace Canvas3D {
                 } else {
                     scene.update(void 0, !!keepSphere);
                 }
+                forceDrawAfterAllCommited = true;
             },
             clear: () => {
                 reprUpdatedSubscriptions.forEach(v => v.unsubscribe());
@@ -489,6 +499,7 @@ namespace Canvas3D {
                 if (scene.syncVisibility()) {
                     if (debugHelper.isEnabled) debugHelper.update();
                 }
+                requestDraw(true);
             },
 
             // draw,

+ 2 - 2
src/mol-geo/geometry/overpaint-data.ts

@@ -15,10 +15,10 @@ export type OverpaintData = {
     dOverpaint: ValueCell<boolean>,
 }
 
-export function applyOverpaintColor(array: Uint8Array, start: number, end: number, color: Color, alpha: number) {
+export function applyOverpaintColor(array: Uint8Array, start: number, end: number, color: Color) {
     for (let i = start; i < end; ++i) {
         Color.toArray(color, array, i * 4);
-        array[i * 4 + 3] = alpha * 255;
+        array[i * 4 + 3] = 255;
     }
     return true;
 }

+ 2 - 4
src/mol-geo/geometry/transparency-data.ts

@@ -7,7 +7,6 @@
 import { ValueCell } from '../../mol-util/value-cell';
 import { Vec2 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
-import { Transparency } from '../../mol-theme/transparency';
 
 export type TransparencyData = {
     tTransparency: ValueCell<TextureImage<Uint8Array>>
@@ -27,20 +26,19 @@ export function clearTransparency(array: Uint8Array, start: number, end: number)
     array.fill(0, start, end);
 }
 
-export function createTransparency(count: number, variant: Transparency.Variant, transparencyData?: TransparencyData): TransparencyData {
+export function createTransparency(count: number, transparencyData?: TransparencyData): TransparencyData {
     const transparency = createTextureImage(Math.max(1, count), 1, Uint8Array, transparencyData && transparencyData.tTransparency.ref.value.array);
     if (transparencyData) {
         ValueCell.update(transparencyData.tTransparency, transparency);
         ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height));
         ValueCell.update(transparencyData.dTransparency, count > 0);
-        ValueCell.update(transparencyData.dTransparencyVariant, variant);
         return transparencyData;
     } else {
         return {
             tTransparency: ValueCell.create(transparency),
             uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)),
             dTransparency: ValueCell.create(count > 0),
-            dTransparencyVariant: ValueCell.create(variant),
+            dTransparencyVariant: ValueCell.create('single'),
         };
     }
 }

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

@@ -128,7 +128,7 @@ describe('renderer', () => {
         scene.add(points);
         scene.commit();
         expect(ctx.stats.resourceCounts.attribute).toBe(4);
-        expect(ctx.stats.resourceCounts.texture).toBe(5);
+        expect(ctx.stats.resourceCounts.texture).toBe(6);
         expect(ctx.stats.resourceCounts.vertexArray).toBe(5);
         expect(ctx.stats.resourceCounts.program).toBe(5);
         expect(ctx.stats.resourceCounts.shader).toBe(10);

+ 9 - 0
src/mol-gl/renderer.ts

@@ -18,6 +18,7 @@ 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';
+import { Transparency } from '../mol-theme/transparency';
 
 export interface RendererStats {
     programCount: number
@@ -51,6 +52,7 @@ export const RendererParams = {
 
     // the following are general 'material' parameters
     pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
+    transparencyVariant: PD.Select('single', PD.arrayToOptions<Transparency.Variant>(['single', 'multi'])),
 
     interiorDarkening: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
     interiorColorFlag: PD.Boolean(true, { label: 'Use Interior Color' }),
@@ -239,6 +241,10 @@ namespace Renderer {
                 ValueCell.update(r.values.dClipVariant, clip.variant);
                 definesNeedUpdate = true;
             }
+            if (r.values.dTransparencyVariant.ref.value !== p.transparencyVariant) {
+                ValueCell.update(r.values.dTransparencyVariant, p.transparencyVariant);
+                definesNeedUpdate = true;
+            }
             if (definesNeedUpdate) r.update();
 
             const program = r.getProgram(variant);
@@ -373,6 +379,9 @@ namespace Renderer {
                     p.pickingAlphaThreshold = props.pickingAlphaThreshold;
                     ValueCell.update(globalUniforms.uPickingAlphaThreshold, p.pickingAlphaThreshold);
                 }
+                if (props.transparencyVariant !== undefined && props.transparencyVariant !== p.transparencyVariant) {
+                    p.transparencyVariant = props.transparencyVariant;
+                }
 
                 if (props.interiorDarkening !== undefined && props.interiorDarkening !== p.interiorDarkening) {
                     p.interiorDarkening = props.interiorDarkening;

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

@@ -7,7 +7,7 @@ mat4 modelView = uView * model;
     vec3 position = aPosition;
 #endif
 vec4 position4 = vec4(position, 1.0);
-vModelPosition = (model * position4).xyz;
+vModelPosition = (model * position4).xyz; // for clipping in frag shader
 vec4 mvPosition = modelView * position4;
 vViewPosition = mvPosition.xyz;
 gl_Position = uProjection * mvPosition;

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

@@ -5,7 +5,7 @@ export default `
         flag = int(floor(vClipping * 255.0 + 0.5));
     #endif
 
-    vec4 mCenter = model * vec4(uInvariantBoundingSphere.xyz, 1.0);
+    vec4 mCenter = uModel * aTransform * 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;

+ 4 - 3
src/mol-gl/shader/chunks/clip-pixel.glsl.ts

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

+ 3 - 1
src/mol-gl/shader/lines.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 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>
  */
@@ -11,8 +11,10 @@ precision highp int;
 #include common
 #include common_frag_params
 #include color_frag_params
+#include common_clip
 
 void main(){
+    #include clip_pixel
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)

+ 9 - 3
src/mol-gl/shader/lines.vert.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 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>
  *
@@ -15,6 +15,7 @@ precision highp int;
 #include common_vert_params
 #include color_vert_params
 #include size_vert_params
+#include common_clip
 
 uniform float uPixelRatio;
 uniform float uViewportHeight;
@@ -42,6 +43,7 @@ void main(){
     #include assign_group
     #include assign_color_varying
     #include assign_marker_varying
+    #include assign_clipping_varying
     #include assign_size
 
     mat4 modelView = uView * uModel * aTransform;
@@ -51,10 +53,12 @@ void main(){
     vec4 end = modelView * vec4(aEnd, 1.0);
 
     // assign position
-    vec3 position = (aMapping.y < 0.5) ? aStart : aEnd;
-    vec4 mvPosition = modelView * vec4(position, 1.0);
+    vec4 position4 = vec4((aMapping.y < 0.5) ? aStart : aEnd, 1.0);
+    vec4 mvPosition = modelView * position4;
     vViewPosition = mvPosition.xyz;
 
+    vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
+
     // special case for perspective projection, and segments that terminate either in, or behind, the camera plane
     // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
     // but we need to perform ndc-space calculations in the shader, so we must address this issue directly
@@ -114,5 +118,7 @@ void main(){
     offset *= clip.w;
     clip.xy += offset;
     gl_Position = clip;
+
+    #include clip_instance
 }
 `;

+ 3 - 1
src/mol-gl/shader/points.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 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>
  */
@@ -11,6 +11,7 @@ precision highp int;
 #include common
 #include common_frag_params
 #include color_frag_params
+#include common_clip
 
 #ifdef dPointFilledCircle
     uniform float uPointEdgeBleach;
@@ -20,6 +21,7 @@ const vec2 center = vec2(0.5);
 const float radius = 0.5;
 
 void main(){
+    #include clip_pixel
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)

+ 5 - 1
src/mol-gl/shader/points.vert.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 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>
  */
@@ -13,6 +13,7 @@ precision highp int;
 #include common_vert_params
 #include color_vert_params
 #include size_vert_params
+#include common_clip
 
 uniform float uPixelRatio;
 uniform float uViewportHeight;
@@ -26,6 +27,7 @@ void main(){
     #include assign_group
     #include assign_color_varying
     #include assign_marker_varying
+    #include assign_clipping_varying
     #include assign_position
     #include assign_size
 
@@ -36,5 +38,7 @@ void main(){
     #endif
 
     gl_Position = uProjection * mvPosition;
+
+    #include clip_instance
 }
 `;

+ 4 - 1
src/mol-gl/shader/spheres.frag.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>
  */
@@ -12,6 +12,7 @@ precision highp int;
 #include common_frag_params
 #include color_frag_params
 #include light_frag_params
+#include common_clip
 
 uniform mat4 uProjection;
 
@@ -73,6 +74,8 @@ bool Impostor(out vec3 cameraPos, out vec3 cameraNormal){
 }
 
 void main(void){
+    #include clip_pixel
+
     bool flag = Impostor(cameraPos, cameraNormal);
     #ifndef dDoubleSided
         if (interior)

+ 9 - 2
src/mol-gl/shader/spheres.vert.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>
  */
@@ -13,6 +13,7 @@ precision highp int;
 #include common_vert_params
 #include color_vert_params
 #include size_vert_params
+#include common_clip
 
 uniform mat4 uModelView;
 uniform mat4 uInvProjection;
@@ -77,11 +78,13 @@ void main(void){
     #include assign_group
     #include assign_color_varying
     #include assign_marker_varying
+    #include assign_clipping_varying
     #include assign_size
 
     vRadius = size * matrixScale(uModelView);
 
-    vec4 mvPosition = uModelView * aTransform * vec4(aPosition, 1.0);
+    vec4 position4 = vec4(aPosition, 1.0);
+    vec4 mvPosition = uModelView * aTransform * position4;
     mvPosition.z -= vRadius; // avoid clipping, added again in fragment shader
 
     gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
@@ -91,5 +94,9 @@ void main(void){
     vec4 vPoint4 = uInvProjection * gl_Position;
     vPoint = vPoint4.xyz / vPoint4.w;
     vPointViewPosition = -mvPosition.xyz / mvPosition.w;
+
+    vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
+
+    #include clip_instance
 }
 `;

+ 3 - 1
src/mol-gl/shader/text.frag.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>
  */
@@ -11,6 +11,7 @@ precision highp int;
 #include common
 #include common_frag_params
 #include color_frag_params
+#include common_clip
 
 uniform sampler2D tFont;
 
@@ -29,6 +30,7 @@ void main2(){
 }
 
 void main(){
+    #include clip_pixel
     #include assign_material_color
 
     if (vTexCoord.x > 1.0) {

+ 9 - 2
src/mol-gl/shader/text.vert.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>
  */
@@ -13,6 +13,7 @@ precision highp int;
 #include common_vert_params
 #include color_vert_params
 #include size_vert_params
+#include common_clip
 
 uniform mat4 uModelView;
 
@@ -40,6 +41,7 @@ void main(void){
     #include assign_group
     #include assign_color_varying
     #include assign_marker_varying
+    #include assign_clipping_varying
     #include assign_size
 
     vTexCoord = aTexCoord;
@@ -50,7 +52,10 @@ void main(void){
     float offsetY = uOffsetY * scale;
     float offsetZ = (uOffsetZ + aDepth * 0.95) * scale;
 
-    vec4 mvPosition = uModelView * aTransform * vec4(aPosition, 1.0);
+    vec4 position4 = vec4(aPosition, 1.0);
+    vec4 mvPosition = uModelView * aTransform * position4;
+
+    vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
 
     // TODO
     // #ifdef FIXED_SIZE
@@ -83,5 +88,7 @@ void main(void){
     gl_Position = uProjection * mvCorner;
 
     vViewPosition = -mvCorner.xyz;
+
+    #include clip_instance
 }
 `;

+ 1 - 7
src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts

@@ -5,7 +5,7 @@
  */
 
 import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
-import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
+import { Vec3 } from '../../../mol-math/linear-algebra';
 import { Loci, EmptyLoci } from '../../../mol-model/loci';
 import { Interval } from '../../../mol-data/int';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -33,10 +33,6 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
     const { x, y, z, members, offsets } = features;
     const { edgeCount, a, b, edgeProps: { flag } } = contacts;
     const { sizeFactor } = props;
-    const { matrix } = unit.conformation.operator;
-
-    const operator = Mat4.copy(Mat4(), structure.coordinateSystem.inverse);
-    Mat4.mul(operator, operator, matrix);
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
@@ -44,9 +40,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
         linkCount: edgeCount * 2,
         position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
             Vec3.set(posA, x[a[edgeIndex]], y[a[edgeIndex]], z[a[edgeIndex]]);
-            Vec3.transformMat4(posA, posA, operator);
             Vec3.set(posB, x[b[edgeIndex]], y[b[edgeIndex]], z[b[edgeIndex]]);
-            Vec3.transformMat4(posB, posB, operator);
         },
         style: (edgeIndex: number) => LinkCylinderStyle.Dashed,
         radius: (edgeIndex: number) => {

+ 2 - 2
src/mol-model/structure/model/types.ts

@@ -118,7 +118,7 @@ export const PolymerTypeAtomRoleId: { [k in PolymerType]: { [k in AtomRole]: Set
         coarseBackbone: new Set(['CA'])
     },
     [PolymerType.RNA]: {
-        trace: new Set(['C4\'', 'C4*']),
+        trace: new Set(['O3\'', 'O3*']),
         directionFrom: new Set(['C4\'', 'C4*']),
         directionTo: new Set(['C3\'', 'C3*']),
         backboneStart: new Set(['P']),
@@ -126,7 +126,7 @@ export const PolymerTypeAtomRoleId: { [k in PolymerType]: { [k in AtomRole]: Set
         coarseBackbone: new Set(['P'])
     },
     [PolymerType.DNA]: {
-        trace: new Set(['C3\'', 'C3*']),
+        trace: new Set(['O3\'', 'O3*']),
         directionFrom: new Set(['C3\'', 'C3*']),
         directionTo: new Set(['C1\'', 'C1*']),
         backboneStart: new Set(['P']),

+ 30 - 5
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -114,9 +114,14 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
             coarse: await presetStaticComponent(plugin, structureCell, 'coarse')
         };
 
+        const structure = structureCell.obj!.data;
+        const cartoonProps = {
+            sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2,
+        };
+
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams, color }, { tag: 'polymer' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color }, { tag: 'polymer' }),
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color }, { tag: 'ligand' }),
             nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color: color || 'polymer-id' }, { tag: 'non-standard' }),
             branchedBallAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.3 }, color }, { tag: 'branched-ball-and-stick' }),
@@ -147,10 +152,19 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
             nucleic: await presetSelectionComponent(plugin, structureCell, 'nucleic'),
         };
 
+        const structure = structureCell.obj!.data;
+        const cartoonProps = {
+            sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2,
+        };
+        const gaussianProps = {
+            radiusOffset: structure.isCoarseGrained ? 2 : 0,
+            smoothness: structure.isCoarseGrained ? 0.5 : 1.5,
+        };
+
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams, color }, { tag: 'protein' }),
-            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams, color }, { tag: 'nucleic' })
+            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color }, { tag: 'protein' }),
+            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color }, { tag: 'nucleic' })
         };
 
         await update.commit({ revertOnError: true });
@@ -179,12 +193,18 @@ const coarseSurface = StructureRepresentationPresetProvider({
         if (size === Structure.Size.Gigantic) {
             Object.assign(gaussianProps, {
                 traceOnly: true,
-                radiusOffset: 1,
+                radiusOffset: 2,
                 smoothness: 0.5,
                 visuals: ['structure-gaussian-surface-mesh']
             });
         } else if(size === Structure.Size.Huge) {
             Object.assign(gaussianProps, {
+                radiusOffset: structure.isCoarseGrained ? 2 : 0,
+                smoothness: 0.5,
+            });
+        } else if(structure.isCoarseGrained) {
+            Object.assign(gaussianProps, {
+                radiusOffset: 2,
                 smoothness: 0.5,
             });
         }
@@ -214,9 +234,14 @@ const polymerCartoon = StructureRepresentationPresetProvider({
             polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
         };
 
+        const structure = structureCell.obj!.data;
+        const cartoonProps = {
+            sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2
+        };
+
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams, color }, { tag: 'polymer' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color }, { tag: 'polymer' })
         };
 
         await update.commit({ revertOnError: true });

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

@@ -14,17 +14,17 @@ 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
+type ClippingEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, clipping?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.ClippingStructureRepresentation3DFromBundle>>) => Promise<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) => {
+export async function setStructureClipping(plugin: PluginContext, components: StructureComponentRef[], groups: Clipping.Groups, lociGetter: (structure: Structure) => Promise<StructureElement.Loci | EmptyLoci>, types?: string[]) {
+    await eachRepr(plugin, components, async (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);
+        const loci = await lociGetter(structure.root);
         if (Loci.isEmpty(loci)) return;
 
         const layer = {
@@ -44,13 +44,13 @@ export async function setStructureClipping(plugin: PluginContext, components: St
     });
 }
 
-function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: ClippingEachReprCallback) {
+async 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]);
+            await callback(update, r.cell, clipping[0]);
         }
     }
 

+ 10 - 10
src/mol-plugin-state/helpers/structure-overpaint.ts

@@ -15,17 +15,17 @@ import { Color } from '../../mol-util/color';
 import { StructureComponentRef } from '../manager/structure/hierarchy-state';
 import { EmptyLoci, Loci } from '../../mol-model/loci';
 
-type OverpaintEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, overpaint?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle>>) => void
+type OverpaintEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, overpaint?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle>>) => Promise<void>
 const OverpaintManagerTag = 'overpaint-controls';
 
-export async function setStructureOverpaint(plugin: PluginContext, components: StructureComponentRef[], color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci | EmptyLoci, types?: string[], alpha = 1) {
-    await eachRepr(plugin, components, (update, repr, overpaintCell) => {
+export async function setStructureOverpaint(plugin: PluginContext, components: StructureComponentRef[], color: Color | -1, lociGetter: (structure: Structure) => Promise<StructureElement.Loci | EmptyLoci>, types?: string[]) {
+    await eachRepr(plugin, components, async (update, repr, overpaintCell) => {
         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 overpaint
         // stays applicable as long as the root structure does not change
-        const loci = lociGetter(structure.root);
+        const loci = await lociGetter(structure.root);
         if (Loci.isEmpty(loci)) return;
 
         const layer = {
@@ -37,17 +37,17 @@ export async function setStructureOverpaint(plugin: PluginContext, components: S
         if (overpaintCell) {
             const bundleLayers = [...overpaintCell.params!.values.layers, layer];
             const filtered = getFilteredBundle(bundleLayers, structure);
-            update.to(overpaintCell).update(Overpaint.toBundle(filtered, alpha));
+            update.to(overpaintCell).update(Overpaint.toBundle(filtered));
         } else {
             const filtered = getFilteredBundle([layer], structure);
             update.to(repr.transform.ref)
-                .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered, alpha), { tags: OverpaintManagerTag });
+                .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered), { tags: OverpaintManagerTag });
         }
     });
 }
 
 export async function clearStructureOverpaint(plugin: PluginContext, components: StructureComponentRef[], types?: string[]) {
-    await eachRepr(plugin, components, (update, repr, overpaintCell) => {
+    await eachRepr(plugin, components, async (update, repr, overpaintCell) => {
         if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
         if (overpaintCell) {
             update.delete(overpaintCell.transform.ref);
@@ -55,13 +55,13 @@ export async function clearStructureOverpaint(plugin: PluginContext, components:
     });
 }
 
-function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: OverpaintEachReprCallback) {
+async function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: OverpaintEachReprCallback) {
     const state = plugin.state.data;
     const update = state.build();
     for (const c of components) {
         for (const r of c.representations) {
             const overpaint = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, r.cell.transform.ref).withTag(OverpaintManagerTag));
-            callback(update, r.cell, overpaint[0]);
+            await callback(update, r.cell, overpaint[0]);
         }
     }
 
@@ -70,7 +70,7 @@ function eachRepr(plugin: PluginContext, components: StructureComponentRef[], ca
 
 /** filter overpaint layers for given structure */
 function getFilteredBundle(layers: Overpaint.BundleLayer[], structure: Structure) {
-    const overpaint = Overpaint.ofBundle(layers, 1, structure.root);
+    const overpaint = Overpaint.ofBundle(layers, structure.root);
     const merged = Overpaint.merge(overpaint);
     return Overpaint.filter(merged, structure);
 }

+ 74 - 0
src/mol-plugin-state/helpers/structure-transparency.ts

@@ -0,0 +1,74 @@
+/**
+ * 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 { Transparency } from '../../mol-theme/transparency';
+
+type TransparencyEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, transparency?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle>>) => Promise<void>
+const TransparencyManagerTag = 'transparency-controls';
+
+export async function setStructureTransparency(plugin: PluginContext, components: StructureComponentRef[], value: number, lociGetter: (structure: Structure) => Promise<StructureElement.Loci | EmptyLoci>, types?: string[]) {
+    await eachRepr(plugin, components, async (update, repr, transparencyCell) => {
+        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 transparency
+        // stays applicable as long as the root structure does not change
+        const loci = await lociGetter(structure.root);
+        if (Loci.isEmpty(loci)) return;
+
+        const layer = {
+            bundle: StructureElement.Bundle.fromLoci(loci),
+            value,
+        };
+
+        if (transparencyCell) {
+            const bundleLayers = [...transparencyCell.params!.values.layers, layer];
+            const filtered = getFilteredBundle(bundleLayers, structure);
+            update.to(transparencyCell).update(Transparency.toBundle(filtered));
+        } else {
+            const filtered = getFilteredBundle([layer], structure);
+            update.to(repr.transform.ref)
+                .apply(StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle, Transparency.toBundle(filtered), { tags: TransparencyManagerTag });
+        }
+    });
+}
+
+export async function clearStructureTransparency(plugin: PluginContext, components: StructureComponentRef[], types?: string[]) {
+    await eachRepr(plugin, components, async (update, repr, transparencyCell) => {
+        if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
+        if (transparencyCell) {
+            update.delete(transparencyCell.transform.ref);
+        }
+    });
+}
+
+async function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: TransparencyEachReprCallback) {
+    const state = plugin.state.data;
+    const update = state.build();
+    for (const c of components) {
+        for (const r of c.representations) {
+            const transparency = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle, r.cell.transform.ref).withTag(TransparencyManagerTag));
+            await callback(update, r.cell, transparency[0]);
+        }
+    }
+
+    return update.commit({ doNotUpdateCurrent: true });
+}
+
+/** filter transparency layers for given structure */
+function getFilteredBundle(layers: Transparency.BundleLayer[], structure: Structure) {
+    const transparency = Transparency.ofBundle(layers, structure.root);
+    const merged = Transparency.merge(transparency);
+    return Transparency.filter(merged, structure);
+}

+ 13 - 3
src/mol-plugin-state/manager/camera.ts

@@ -12,6 +12,7 @@ import { Camera } from '../../mol-canvas3d/camera';
 import { Loci } from '../../mol-model/loci';
 import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
 import { GraphicsRenderObject } from '../../mol-gl/render-object';
+import { StructureElement } from '../../mol-model/structure';
 
 // TODO: make this customizable somewhere?
 const DefaultCameraFocusOptions = {
@@ -25,6 +26,15 @@ export type CameraFocusOptions = typeof DefaultCameraFocusOptions
 export class CameraManager {
     private boundaryHelper = new BoundaryHelper('98');
 
+    private transformedLoci(loci: Loci) {
+        if (StructureElement.Loci.is(loci)) {
+            // use decorated (including 3d transforms) parent structure
+            const parent = this.plugin.helpers.substructureParent.get(loci.structure)?.obj?.data;
+            if (parent) loci = StructureElement.Loci.remap(loci, parent);
+        }
+        return loci;
+    }
+
     focusRenderObjects(objects?: ReadonlyArray<GraphicsRenderObject>, options?: Partial<CameraFocusOptions>) {
         if (!objects) return;
         const spheres: Sphere3D[] = [];
@@ -47,7 +57,7 @@ export class CameraManager {
         if (Array.isArray(loci) && loci.length > 1) {
             const spheres = [];
             for (const l of loci) {
-                const s = Loci.getBoundingSphere(l);
+                const s = Loci.getBoundingSphere(this.transformedLoci(l));
                 if (s) spheres.push(s);
             }
 
@@ -64,9 +74,9 @@ export class CameraManager {
             sphere = this.boundaryHelper.getSphere();
         } else if (Array.isArray(loci)) {
             if (loci.length === 0) return;
-            sphere = Loci.getBoundingSphere(loci[0]);
+            sphere = Loci.getBoundingSphere(this.transformedLoci(loci[0]));
         } else {
-            sphere = Loci.getBoundingSphere(loci);
+            sphere = Loci.getBoundingSphere(this.transformedLoci(loci));
         }
 
         if (sphere) {

+ 25 - 9
src/mol-plugin-state/manager/structure/component.ts

@@ -21,13 +21,14 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { StructureRepresentationPresetProvider } from '../../builder/structure/representation-preset';
 import { StatefulPluginComponent } from '../../component';
 import { StructureComponentParams } from '../../helpers/structure-component';
-import { clearStructureOverpaint, setStructureOverpaint } from '../../helpers/structure-overpaint';
+import { setStructureOverpaint } from '../../helpers/structure-overpaint';
 import { createStructureColorThemeParams, createStructureSizeThemeParams } from '../../helpers/structure-representation-params';
 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';
+import { setStructureTransparency } from '../../helpers/structure-transparency';
 
 export { StructureComponentManager };
 
@@ -361,17 +362,20 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
     }
 
     async applyTheme(params: StructureComponentManager.ThemeParams, structures?: ReadonlyArray<StructureRef>) {
-        return this.plugin.dataTransaction(async () => {
+        return this.plugin.dataTransaction(async ctx => {
             const xs = structures || this.currentStructures;
             if (xs.length === 0) return;
-            const getLoci = (s: Structure) => this.plugin.managers.structure.selection.getLoci(s);
 
+            const getLoci = async (s: Structure) => StructureSelection.toLociWithSourceUnits(await params.selection.getSelection(this.plugin, ctx, s));
             for (const s of xs) {
                 if (params.action.name === 'reset') {
-                    await clearStructureOverpaint(this.plugin, s.components, params.representations);
+                    await setStructureOverpaint(this.plugin, s.components, -1, getLoci, params.representations);
                 } else if (params.action.name === 'color') {
                     const p = params.action.params;
-                    await setStructureOverpaint(this.plugin, s.components, p.color, getLoci, params.representations, p.opacity);
+                    await setStructureOverpaint(this.plugin, s.components, p.color, getLoci, params.representations);
+                } else if (params.action.name === 'transparency') {
+                    const p = params.action.params;
+                    await setStructureTransparency(this.plugin, s.components, p.value, getLoci, params.representations);
                 } 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);
@@ -405,6 +409,15 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
         }
     }
 
+    updateLabel(component: StructureComponentRef, label: string) {
+        const params: StructureComponentParams = {
+            type: component.cell.params?.values.type,
+            nullIfEmpty: component.cell.params?.values.nullIfEmpty,
+            label
+        };
+        this.dataState.build().to(component.cell).update(params).commit();
+    }
+
     private get dataState() {
         return this.plugin.state.data;
     }
@@ -432,14 +445,13 @@ namespace StructureComponentManager {
     };
     export type Options = PD.Values<typeof OptionsParams>
 
-    export function getAddParams(plugin: PluginContext, params?: { pivot?: StructureRef, allowNone: boolean, hideSelection?: boolean, checkExisting?: boolean, defaultSelection?: StructureSelectionQuery }) {
+    export function getAddParams(plugin: PluginContext, params?: { pivot?: StructureRef, allowNone: boolean, hideSelection?: boolean, checkExisting?: boolean }) {
         const { options } = plugin.query.structure.registry;
         params = {
             pivot: plugin.managers.structure.component.pivotStructure,
             allowNone: true,
             hideSelection: false,
             checkExisting: false,
-            defaultSelection: StructureSelectionQueries.current,
             ...params
         };
         return {
@@ -454,13 +466,17 @@ namespace StructureComponentManager {
     export type AddParams = { selection: StructureSelectionQuery, options: { checkExisting: boolean, label: string }, representation: string }
 
     export function getThemeParams(plugin: PluginContext, pivot: StructureRef | StructureComponentRef | undefined) {
+        const { options } = plugin.query.structure.registry;
         return {
+            selection: PD.Select(options[1][0], options, { isHidden: false }),
             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({ label: 'Reset Color' }),
+                transparency: PD.Group({
+                    value: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
+                }, { isFlat: true }),
                 clipping: PD.Group({
                     excludeGroups: PD.MultiSelect([] as Clipping.Groups.Names[], PD.objectToOptions(Clipping.Groups.Names)),
                 }, { isFlat: true }),

+ 5 - 2
src/mol-plugin-state/manager/structure/focus.ts

@@ -93,7 +93,7 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
     }
 
     addFromLoci(anyLoci: Loci) {
-        const union = this.state.current && StructureElement.Loci.is(anyLoci)
+        const union = this.state.current && StructureElement.Loci.is(anyLoci) && anyLoci.structure === this.state.current.loci.structure
             ? StructureElement.Loci.union(anyLoci, this.state.current.loci)
             : anyLoci;
         this.setFromLoci(union);
@@ -124,7 +124,10 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
     }
 
     setSnapshot(snapshot: StructureFocusSnapshot) {
-        if (!snapshot.current) return;
+        if (!snapshot.current) {
+            this.clear();
+            return;
+        }
 
         const { label, ref, bundle, category } = snapshot.current;
         const structure = this.plugin.state.data.select(StateSelection.Generators.byRef(ref))[0]?.obj?.data as Structure;

+ 1 - 1
src/mol-plugin-state/manager/structure/selection.ts

@@ -260,7 +260,7 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
         if (!structure) return;
 
         // oldObj is not defined for inserts (e.g. TransformStructureConformation)
-        if (!oldObj || Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
+        if (!oldObj?.data || Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
             this.entries.set(ref, remapSelectionEntry(this.entries.get(ref)!, structure));
 
             // remap referenceLoci & prevHighlight if needed and possible

+ 33 - 22
src/mol-plugin-state/transforms/representation.ts

@@ -279,7 +279,6 @@ const OverpaintStructureRepresentation3DFromScript = PluginStateTransform.BuiltI
                 clear: false
             }]
         }),
-        alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity' }),
     }
 })({
     canAutoUpdate() {
@@ -287,7 +286,7 @@ const OverpaintStructureRepresentation3DFromScript = PluginStateTransform.BuiltI
     },
     apply({ a, params }) {
         const structure = a.data.source.data;
-        const overpaint = Overpaint.ofScript(params.layers, params.alpha, structure);
+        const overpaint = Overpaint.ofScript(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { overpaint },
@@ -301,8 +300,8 @@ const OverpaintStructureRepresentation3DFromScript = PluginStateTransform.BuiltI
         const newStructure = a.data.source.data;
         if (newStructure !== oldStructure) return StateTransformer.UpdateResult.Recreate;
         const oldOverpaint = b.data.state.overpaint!;
-        const newOverpaint = Overpaint.ofScript(newParams.layers, newParams.alpha, newStructure);
-        if (oldParams.alpha === newParams.alpha && Overpaint.areEqual(oldOverpaint, newOverpaint)) return StateTransformer.UpdateResult.Unchanged;
+        const newOverpaint = Overpaint.ofScript(newParams.layers, newStructure);
+        if (Overpaint.areEqual(oldOverpaint, newOverpaint)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.overpaint = newOverpaint;
         b.data.source = a;
@@ -330,7 +329,6 @@ const OverpaintStructureRepresentation3DFromBundle = PluginStateTransform.BuiltI
             }],
             isHidden: true
         }),
-        alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity' }),
     }
 })({
     canAutoUpdate() {
@@ -338,7 +336,7 @@ const OverpaintStructureRepresentation3DFromBundle = PluginStateTransform.BuiltI
     },
     apply({ a, params }) {
         const structure = a.data.source.data;
-        const overpaint = Overpaint.ofBundle(params.layers, params.alpha, structure);
+        const overpaint = Overpaint.ofBundle(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { overpaint },
@@ -352,8 +350,8 @@ const OverpaintStructureRepresentation3DFromBundle = PluginStateTransform.BuiltI
         const newStructure = a.data.source.data;
         if (newStructure !== oldStructure) return StateTransformer.UpdateResult.Recreate;
         const oldOverpaint = b.data.state.overpaint!;
-        const newOverpaint = Overpaint.ofBundle(newParams.layers, newParams.alpha, newStructure);
-        if (oldParams.alpha === newParams.alpha && Overpaint.areEqual(oldOverpaint, newOverpaint)) return StateTransformer.UpdateResult.Unchanged;
+        const newOverpaint = Overpaint.ofBundle(newParams.layers, newStructure);
+        if (Overpaint.areEqual(oldOverpaint, newOverpaint)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.overpaint = newOverpaint;
         b.data.source = a;
@@ -369,9 +367,15 @@ const TransparencyStructureRepresentation3DFromScript = PluginStateTransform.Bui
     from: SO.Molecule.Structure.Representation3D,
     to: SO.Molecule.Structure.Representation3DState,
     params: {
-        script: PD.Script(Script('(sel.atom.all)', 'mol-script')),
-        value: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
-        variant: PD.Select('single', [['single', 'Single-layer'], ['multi', 'Multi-layer']] as ['single' | 'multi', string][])
+        layers: PD.ObjectList({
+            script: PD.Script(Script('(sel.atom.all)', 'mol-script')),
+            value: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
+        }, e => `Transparency (${e.value})`, {
+            defaultValue: [{
+                script: Script('(sel.atom.all)', 'mol-script'),
+                value: 0.5,
+            }]
+        })
     }
 })({
     canAutoUpdate() {
@@ -379,25 +383,25 @@ const TransparencyStructureRepresentation3DFromScript = PluginStateTransform.Bui
     },
     apply({ a, params }) {
         const structure = a.data.source.data;
-        const transparency = Transparency.ofScript(params.script, params.value, params.variant, structure);
+        const transparency = Transparency.ofScript(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { transparency },
             initialState: { transparency: Transparency.Empty },
             info: structure,
             source: a
-        }, { label: `Transparency (${transparency.value})` });
+        }, { label: `Transparency (${transparency.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 oldTransparency = b.data.state.transparency!;
-        const newTransparency = Transparency.ofScript(newParams.script, newParams.value, newParams.variant, structure);
+        const newTransparency = Transparency.ofScript(newParams.layers, structure);
         if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.transparency = newTransparency;
         b.data.source = a;
-        b.label = `Transparency (${newTransparency.value})`;
+        b.label = `Transparency (${newTransparency.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
 });
@@ -409,9 +413,16 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
     from: SO.Molecule.Structure.Representation3D,
     to: SO.Molecule.Structure.Representation3DState,
     params: {
-        bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
-        value: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
-        variant: PD.Select('single', [['single', 'Single-layer'], ['multi', 'Multi-layer']] as ['single' | 'multi', string][])
+        layers: PD.ObjectList({
+            bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
+            value: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
+        }, e => `Transparency (${e.value})`, {
+            defaultValue: [{
+                bundle: StructureElement.Bundle.Empty,
+                value: 0.5,
+            }],
+            isHidden: true
+        })
     }
 })({
     canAutoUpdate() {
@@ -419,25 +430,25 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
     },
     apply({ a, params }) {
         const structure = a.data.source.data;
-        const transparency = Transparency.ofBundle(params.bundle, params.value, params.variant, structure);
+        const transparency = Transparency.ofBundle(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { transparency },
             initialState: { transparency: Transparency.Empty },
             info: structure,
             source: a
-        }, { label: `Transparency (${transparency.value})` });
+        }, { label: `Transparency (${transparency.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 oldTransparency = b.data.state.transparency!;
-        const newTransparency = Transparency.ofBundle(newParams.bundle, newParams.value, newParams.variant, structure);
+        const newTransparency = Transparency.ofBundle(newParams.layers, structure);
         if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.transparency = newTransparency;
         b.data.source = a;
-        b.label = `Transparency (${newTransparency.value})`;
+        b.label = `Transparency (${newTransparency.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
 });

+ 18 - 4
src/mol-plugin-ui/controls/color.tsx

@@ -14,8 +14,11 @@ import { ParamProps } from './parameters';
 import { TextInput, Button, ControlRow } from './common';
 import { DefaultColorSwatch } from '../../mol-util/color/swatches';
 
-export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean }> {
-    state = { isExpanded: !!this.props.param.isExpanded }
+export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean, lightness: number }> {
+    state = {
+        isExpanded: !!this.props.param.isExpanded,
+        lightness: 0
+    }
 
     protected update(value: Color) {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
@@ -52,8 +55,15 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
         if (value !== this.props.value) this.update(value);
     }
 
+    onLighten = () => {
+        this.update(Color.lighten(this.props.value, 0.1));
+    }
+
+    onDarken = () => {
+        this.update(Color.darken(this.props.value, 0.1));
+    }
+
     swatch() {
-        // const def = this.props.param.defaultValue;
         return <div className='msp-combined-color-swatch'>
             {DefaultColorSwatch.map(c => <Button key={c[1]} inline data-color={c[1]} onClick={this.onClickSwatch} style={{ background: Color.toStyle(c[1]) }} />)}
         </div>;
@@ -68,11 +78,15 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
                 control={<Button onClick={this.toggleExpanded} inline className='msp-combined-color-button' style={{ background: Color.toStyle(this.props.value) }} />} />
             {this.state.isExpanded && <div className='msp-control-offset'>
                 {this.swatch()}
-                <ControlRow label='RGB' control={<div style={{ display: 'flex', textAlignLast: 'center' }}>
+                <ControlRow label='RGB' className='msp-control-label-short' control={<div style={{ display: 'flex', textAlignLast: 'center', left: '80px' }}>
                     <TextInput onChange={this.onR} numeric value={r} delayMs={250} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} />
                     <TextInput onChange={this.onG} numeric value={g} delayMs={250} style={{ order: 2, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} />
                     <TextInput onChange={this.onB} numeric value={b} delayMs={250} style={{ order: 3, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true} />
                 </div>}/>
+                <div style={{ display: 'flex', textAlignLast: 'center' }}>
+                    <Button  onClick={this.onLighten} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control'>Lighten</Button>
+                    <Button onClick={this.onDarken} style={{ order: 1, flex: '1 1 auto', minWidth: 0 }} className='msp-form-control'>Darken</Button>
+                </div>
             </div>}
         </>;
     }

Diff do ficheiro suprimidas por serem muito extensas
+ 3 - 3
src/mol-plugin-ui/controls/icons.tsx


+ 2 - 2
src/mol-plugin-ui/skin/base/components/controls.scss

@@ -56,11 +56,11 @@
 
 .msp-control-label-short {
     > span {
-        width: $control-label-short-width + $control-spacing;
+        width: $control-label-short-width + $control-spacing !important;
     }
 
     > div:nth-child(2) {
-        left: $control-label-short-width + $control-spacing;
+        left: $control-label-short-width + $control-spacing !important;
     }
 }
 

+ 2 - 2
src/mol-plugin-ui/state/snapshots.tsx

@@ -92,7 +92,7 @@ export class LocalStateSnapshotParams extends PluginUIComponent {
     }
 }
 
-class LocalStateSnapshots extends PluginUIComponent<
+export class LocalStateSnapshots extends PluginUIComponent<
 {},
 { params: PD.Values<typeof LocalStateSnapshots.Params> }> {
     state = { params: PD.getDefaultValues(LocalStateSnapshots.Params) };
@@ -130,7 +130,7 @@ class LocalStateSnapshots extends PluginUIComponent<
     }
 }
 
-class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
+export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
     componentDidMount() {
         this.subscribe(this.plugin.managers.snapshot.events.changed, () => this.forceUpdate());
     }

+ 1 - 1
src/mol-plugin-ui/state/update-transform.tsx

@@ -70,7 +70,7 @@ class UpdateTransformControl extends TransformControlBase<UpdateTransformControl
     }
 
     componentDidMount() {
-        if (super.componentDidMount) super.componentDidMount();
+        super.componentDidMount();
 
         if (this.props.toggleCollapsed) this.subscribe(this.props.toggleCollapsed, () => this.setState({ isCollapsed: !this.state.isCollapsed }));
 

+ 30 - 33
src/mol-plugin-ui/structure/components.tsx

@@ -2,6 +2,7 @@
  * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import * as React from 'react';
@@ -14,8 +15,8 @@ import { State } from '../../mol-state';
 import { ParamDefinition } from '../../mol-util/param-definition';
 import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
-import { Button, ExpandGroup, IconButton, ToggleButton } from '../controls/common';
-import { CubeSvg, IntersectSvg, SetSvg, SubtractSvg, UnionSvg, BookmarksOutlinedSvg, AddSvg, TuneSvg, RestoreSvg, DeleteSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg } from '../controls/icons';
+import { Button, ExpandGroup, IconButton, ToggleButton, ControlRow, TextInput } from '../controls/common';
+import { CubeOutlineSvg, IntersectSvg, SetSvg, SubtractSvg, UnionSvg, BookmarksOutlinedSvg, AddSvg, TuneSvg, RestoreSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg, CheckSvg } from '../controls/icons';
 import { ParameterControls } from '../controls/parameters';
 import { UpdateTransformControl } from '../state/update-transform';
 import { GenericEntryListControls } from './generic';
@@ -27,10 +28,10 @@ interface StructureComponentControlState extends CollapsableState {
 export class StructureComponentControls extends CollapsableControls<{}, StructureComponentControlState> {
     protected defaultState(): StructureComponentControlState {
         return {
-            header: 'Representation',
+            header: 'Components',
             isCollapsed: false,
             isDisabled: false,
-            brand: { accent: 'blue', svg: CubeSvg }
+            brand: { accent: 'blue', svg: CubeOutlineSvg }
         };
     }
 
@@ -147,9 +148,7 @@ interface AddComponentControlsProps {
 
 export class AddComponentControls extends PurePluginUIComponent<AddComponentControlsProps, AddComponentControlsState> {
     createState(): AddComponentControlsState {
-        const params = StructureComponentManager.getAddParams(this.plugin, this.props.forSelection
-            ? { allowNone: false, hideSelection: true, checkExisting: this.props.forSelection }
-            : void 0);
+        const params = StructureComponentManager.getAddParams(this.plugin);
         return { params, values: ParamDefinition.getDefaultValues(params) };
     }
 
@@ -159,8 +158,12 @@ export class AddComponentControls extends PurePluginUIComponent<AddComponentCont
         return this.plugin.managers.structure.component.currentStructures;
     }
 
+    get currentStructures() {
+        return this.plugin.managers.structure.hierarchy.current.structures;
+    }
+
     apply = () => {
-        const structures = this.props.forSelection ? this.plugin.managers.structure.hierarchy.getStructuresWithSelection() : this.selectedStructures;
+        const structures = this.props.forSelection ? this.currentStructures : this.selectedStructures;
         this.props.onApply();
         this.plugin.managers.structure.component.add(this.state.values, structures);
     }
@@ -206,7 +209,7 @@ class ComponentListControls extends PurePluginUIComponent {
     }
 }
 
-type StructureComponentEntryActions = 'action' | 'remove'
+type StructureComponentEntryActions = 'action' | 'label'
 
 class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureComponentRef[] }, { action?: StructureComponentEntryActions }> {
     state = { action: void 0 as StructureComponentEntryActions | undefined }
@@ -266,38 +269,25 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
 
         ret.push(ActionMenu.Item('Select This', () => mng.selectThis(this.props.group), { icon: SetSvg }));
 
-        return ret;
-    }
-
-    selectAction: ActionMenu.OnSelect = item => {
-        if (!item) return;
-        this.setState({ action: void 0 });
-        (item?.value as any)();
-    }
-
-    get removeActions(): ActionMenu.Items {
-        const ret = [
-            ActionMenu.Item('Remove', () => this.plugin.managers.structure.hierarchy.remove(this.props.group, true), { icon: DeleteSvg })
-        ];
-
-        const reprs = this.pivot.representations;
-        if (reprs.length === 0) {
-            return ret;
+        if (mng.canBeModified(this.props.group[0])) {
+            ret.push(
+                ActionMenu.Item('Edit Label', this.toggleLabel)
+            );
         }
 
-        ret.push(ActionMenu.Item(`Remove Representation${reprs.length > 1 ? 's' : ''}`, () => this.plugin.managers.structure.component.removeRepresentations(this.props.group), { icon: DeleteSvg }));
-
         return ret;
     }
 
-    selectRemoveAction: ActionMenu.OnSelect = item => {
+    selectAction: ActionMenu.OnSelect = item => {
         if (!item) return;
         this.setState({ action: void 0 });
         (item?.value as any)();
     }
 
+    remove = () => this.plugin.managers.structure.hierarchy.remove(this.props.group, true);
+
     toggleAction = () => this.setState({ action: this.state.action === 'action' ? void 0 : 'action' });
-    toggleRemove = () => this.setState({ action: this.state.action === 'remove' ? void 0 : 'remove' });
+    toggleLabel = () => this.setState({ action: this.state.action === 'label' ? void 0 : 'label' });
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
@@ -337,6 +327,10 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
         return `${pivot.representations.length} reprs`;
     }
 
+    private updateLabel = (v: string) => {
+        this.plugin.managers.structure.component.updateLabel(this.pivot, v);
+    }
+
     render() {
         const component = this.pivot;
         const cell = component.cell;
@@ -349,11 +343,14 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
                     <small className='msp-25-lower-contrast-text' style={{ float: 'right' }}>{reprLabel}</small>
                 </Button>
                 <IconButton svg={cell.state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} onClick={this.toggleVisible} title={`${cell.state.isHidden ? 'Show' : 'Hide'} component`} small className='msp-form-control' flex />
-                <IconButton svg={DeleteOutlinedSvg} onClick={this.toggleRemove} title='Remove' small toggleState={this.state.action === 'remove'} className='msp-form-control' flex />
+                <IconButton svg={DeleteOutlinedSvg} toggleState={false} onClick={this.remove} title='Remove' small className='msp-form-control' flex />
                 <IconButton svg={MoreHorizSvg} onClick={this.toggleAction} title='Actions' toggleState={this.state.action === 'action'} className='msp-form-control' flex />
             </div>
-            {this.state.action === 'remove' && <div style={{ marginBottom: '6px' }}>
-                <ActionMenu items={this.removeActions} onSelect={this.selectRemoveAction} />
+            {this.state.action === 'label' && <div className='msp-control-offset' style={{ marginBottom: '6px' }}>
+                <ControlRow label='Label' control={<div style={{ display: 'flex', textAlignLast: 'center' }}>
+                    <TextInput onChange={this.updateLabel} value={label} style={{ flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' blurOnEnter={true} blurOnEscape={true} />
+                    <IconButton svg={CheckSvg} onClick={this.toggleLabel}className='msp-form-control msp-control-button-label' flex />
+                </div>}/>
             </div>}
             {this.state.action === 'action' && <div className='msp-accent-offset'>
                 <div style={{ marginBottom: '6px' }}>

+ 3 - 2
src/mol-plugin-ui/structure/focus.tsx

@@ -187,11 +187,12 @@ export class StructureFocusControls extends PluginUIComponent<{}, StructureFocus
         } else {
             this.plugin.managers.structure.focus.set(f);
         }
+        this.focusCamera();
     }
 
     toggleAction = () => this.setState({ showAction: !this.state.showAction })
 
-    focus = () => {
+    focusCamera = () => {
         const { current } = this.plugin.managers.structure.focus;
         if (current) this.plugin.managers.camera.focusLoci(current.loci);
     }
@@ -233,7 +234,7 @@ export class StructureFocusControls extends PluginUIComponent<{}, StructureFocus
 
         return <>
             <div className='msp-flex-row'>
-                <Button noOverflow onClick={this.focus} title={title} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}
+                <Button noOverflow onClick={this.focusCamera} title={title} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}
                     style={{ textAlignLast: current ? 'left' : void 0 }}>
                     {label}
                 </Button>

+ 2 - 2
src/mol-plugin-ui/structure/measurements.tsx

@@ -17,7 +17,7 @@ import { FiniteArray } from '../../mol-util/type-helpers';
 import { CollapsableControls, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
 import { Button, ExpandGroup, IconButton, ToggleButton } from '../controls/common';
-import { Icon, RulerSvg, SetSvg, ArrowUpwardSvg, ArrowDownwardSvg, DeleteOutlinedSvg, HelpOutlineSvg, AddSvg, TuneSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, MoreHorizSvg } from '../controls/icons';
+import { Icon, PencilRulerSvg, SetSvg, ArrowUpwardSvg, ArrowDownwardSvg, DeleteOutlinedSvg, HelpOutlineSvg, AddSvg, TuneSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, MoreHorizSvg } from '../controls/icons';
 import { ParameterControls } from '../controls/parameters';
 import { UpdateTransformControl } from '../state/update-transform';
 import { ToggleSelectionModeButton } from './selection';
@@ -29,7 +29,7 @@ export class StructureMeasurementsControls extends CollapsableControls {
         return {
             isCollapsed: false,
             header: 'Measurements',
-            brand: { accent: 'gray' as const, svg: RulerSvg }
+            brand: { accent: 'gray' as const, svg: PencilRulerSvg }
         };
     }
 

+ 14 - 14
src/mol-plugin-ui/structure/selection.tsx

@@ -18,7 +18,7 @@ import { PluginUIComponent, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
 import { Button, ControlGroup, IconButton, ToggleButton } from '../controls/common';
 import { ParameterControls, ParamOnChange, PureSelectControl } from '../controls/parameters';
-import { UnionSvg, SubtractSvg, IntersectSvg, SetSvg, CubeSvg, Icon, SelectionModeSvg, RemoveSvg, RestoreSvg, HelpOutlineSvg, CancelOutlinedSvg, BrushSvg, CloseSvg } from '../controls/icons';
+import { UnionSvg, SubtractSvg, IntersectSvg, SetSvg, CubeOutlineSvg, Icon, SelectionModeSvg, RemoveSvg, RestoreSvg, HelpOutlineSvg, CancelOutlinedSvg, BrushSvg, CloseSvg } from '../controls/icons';
 import { AddComponentControls } from './components';
 import Structure from '../../mol-model/structure/structure/structure';
 import { ViewportHelpContent, HelpGroup, HelpText } from '../viewport/help';
@@ -52,7 +52,7 @@ interface StructureSelectionActionsControlsState {
     isBusy: boolean,
     canUndo: boolean,
 
-    action?: StructureSelectionModifier | 'theme' | 'add-repr' | 'help'
+    action?: StructureSelectionModifier | 'theme' | 'add-component' | 'help'
 }
 
 const ActionHeader = new Map<StructureSelectionModifier, string>([
@@ -159,7 +159,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
     toggleIntersect = this.showAction('intersect')
     toggleSet = this.showAction('set')
     toggleTheme = this.showAction('theme')
-    toggleAddRepr = this.showAction('add-repr')
+    toggleAddComponent = this.showAction('add-component')
     toggleHelp = this.showAction('help')
 
     setGranuality: ParamOnChange = ({ value }) => {
@@ -196,14 +196,14 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                 <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='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} />
+                <ToggleButton icon={CubeOutlineSvg} title='Create Component of Selection with Representation' toggle={this.toggleAddComponent} isSelected={this.state.action === 'add-component'} disabled={this.isDisabled} />
+                <IconButton svg={RemoveSvg} title='Remove/subtract Selection from all Components' onClick={this.subtract} disabled={this.isDisabled} />
                 <IconButton svg={RestoreSvg} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} title={undoTitle} />
 
                 <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 !== 'theme' && 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-component' && 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 === 'theme' && <div className='msp-selection-viewport-controls-actions'>
@@ -211,9 +211,9 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                     <ApplyThemeControls onApply={this.toggleTheme} />
                 </ControlGroup>
             </div>}
-            {this.state.action === 'add-repr' && <div className='msp-selection-viewport-controls-actions'>
-                <ControlGroup header='Add Representation' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddRepr} topRightIcon={CloseSvg}>
-                    <AddComponentControls onApply={this.toggleAddRepr} forSelection />
+            {this.state.action === 'add-component' && <div className='msp-selection-viewport-controls-actions'>
+                <ControlGroup header='Add Component' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddComponent} topRightIcon={CloseSvg}>
+                    <AddComponentControls onApply={this.toggleAddComponent} forSelection />
                 </ControlGroup>
             </div>}
             {this.state.action === 'help' && <div className='msp-selection-viewport-controls-actions'>
@@ -222,7 +222,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                         <HelpText>Use <Icon svg={UnionSvg} inline /> <Icon svg={SubtractSvg} inline /> <Icon svg={IntersectSvg} inline /> <Icon svg={SetSvg} inline /> to modify the selection.</HelpText>
                     </HelpGroup>
                     <HelpGroup header='Representation Operations'>
-                        <HelpText>Use <Icon svg={BrushSvg} inline /> <Icon svg={CubeSvg} inline /> <Icon svg={RemoveSvg} inline /> <Icon svg={RestoreSvg} inline /> to color, create selection/representation, subtract it, or undo actions.</HelpText>
+                        <HelpText>Use <Icon svg={BrushSvg} inline /> <Icon svg={CubeOutlineSvg} inline /> <Icon svg={RemoveSvg} inline /> <Icon svg={RestoreSvg} inline /> to color, create components, remove from components, or undo actions.</HelpText>
                     </HelpGroup>
                     <ViewportHelpContent selectOnly={true} />
                 </ControlGroup>
@@ -304,22 +304,22 @@ export class StructureSelectionStatsControls extends PluginUIComponent<{ hideOnE
     }
 }
 
-interface ApplyColorControlsState {
+interface ApplyThemeControlsState {
     values: StructureComponentManager.ThemeParams
 }
 
-interface ApplyColorControlsProps {
+interface ApplyThemeControlsProps {
     onApply?: () => void
 }
 
-class ApplyThemeControls extends PurePluginUIComponent<ApplyColorControlsProps, ApplyColorControlsState> {
+class ApplyThemeControls extends PurePluginUIComponent<ApplyThemeControlsProps, ApplyThemeControlsState> {
     _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.applyTheme(this.state.values);
+        this.plugin.managers.structure.component.applyTheme(this.state.values, this.plugin.managers.structure.hierarchy.current.structures);
         this.props.onApply?.();
     }
 

+ 1 - 1
src/mol-plugin-ui/viewport.tsx

@@ -173,7 +173,7 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> {
     }
 
     componentWillUnmount() {
-        if (super.componentWillUnmount) super.componentWillUnmount();
+        super.componentWillUnmount();
         // TODO viewer cleanup
     }
 

+ 6 - 11
src/mol-plugin/behavior/dynamic/camera.ts

@@ -11,7 +11,6 @@ import { PluginBehavior } from '../behavior';
 import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
 import { Binding } from '../../../mol-util/binding';
 import { PluginCommands } from '../../commands';
-import { Structure, StructureElement, Bond } from '../../../mol-model/structure';
 
 const B = ButtonsType;
 const M = ModifiersKeys;
@@ -19,11 +18,13 @@ const Trigger = Binding.Trigger;
 
 const DefaultFocusLociBindings = {
     clickCenterFocus: Binding([
-        Trigger(B.Flag.Primary, M.create())
+        Trigger(B.Flag.Primary, M.create()),
+        Trigger(B.Flag.Secondary, M.create()),
+        Trigger(B.Flag.Primary, M.create({ control: true }))
     ], 'Camera center and focus', 'Click element using ${triggers}'),
     clickCenterFocusSelectMode: Binding([
-        Trigger(B.Flag.Auxilary, M.create()),
-        Trigger(B.Flag.Primary, M.create({ alt: true }))
+        Trigger(B.Flag.Secondary, M.create()),
+        Trigger(B.Flag.Primary, M.create({ control: true }))
     ], 'Camera center and focus', 'Click element using ${triggers}'),
 };
 const FocusLociParams = {
@@ -53,14 +54,8 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
                         return;
                     }
 
-                    // The focus is handled in structure-focus-representation
-                    // TODO: is there a better solution for structure-based loci?
-
                     const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
-
-                    if (!Structure.isLoci(loci) && !StructureElement.Loci.is(loci) && !Bond.isLoci(loci)) {
-                        this.ctx.managers.camera.focusLoci(loci, this.params);
-                    }
+                    this.ctx.managers.camera.focusLoci(loci, this.params);
                 }
             });
         }

+ 20 - 20
src/mol-plugin/behavior/dynamic/representation.ts

@@ -15,7 +15,7 @@ import { StateSelection } from '../../../mol-state';
 import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
 import { Binding } from '../../../mol-util/binding';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { EmptyLoci, Loci, isEmptyLoci } from '../../../mol-model/loci';
+import { EmptyLoci, Loci } from '../../../mol-model/loci';
 import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
 import { arrayMax } from '../../../mol-util/array';
 import { Representation } from '../../../mol-repr/representation';
@@ -206,12 +206,10 @@ const DefaultFocusLociBindings = {
         Trigger(B.Flag.Primary, M.create({ shift: true })),
     ], 'Representation Focus Add', 'Click element using ${triggers}'),
     clickFocusSelectMode: Binding([
-        Trigger(B.Flag.Secondary, M.create()),
-        Trigger(B.Flag.Primary, M.create({ control: true }))
+        // default is empty
     ], 'Representation Focus', 'Click element using ${triggers}'),
     clickFocusAddSelectMode: Binding([
-        Trigger(B.Flag.Secondary, M.create({ shift: true })),
-        Trigger(B.Flag.Primary, M.create({ control: true, shift: true }))
+        // default is empty
     ], 'Representation Focus Add', 'Click element using ${triggers}'),
 };
 const FocusLociParams = {
@@ -227,28 +225,30 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
             this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
                 const { clickFocus, clickFocusAdd, clickFocusSelectMode, clickFocusAddSelectMode } = this.params.bindings;
 
+                // only apply structure focus for appropriate granularity
+                const { granularity } = this.ctx.managers.interactivity.props;
+                if (granularity !== 'residue' && granularity !== 'element') return;
+
                 const binding = this.ctx.selectionMode ? clickFocusSelectMode : clickFocus;
                 const matched = Binding.match(binding, button, modifiers);
-
                 const bindingAdd = this.ctx.selectionMode ? clickFocusAddSelectMode : clickFocusAdd;
                 const matchedAdd = Binding.match(bindingAdd, button, modifiers);
+                if (!matched && !matchedAdd) return;
 
-                if (matched || matchedAdd) {
-                    const loci = Loci.normalize(current.loci, 'residue');
-                    const entry = this.ctx.managers.structure.focus.current;
-                    if (entry && Loci.areEqual(entry.loci, loci)) {
-                        this.ctx.managers.structure.focus.clear();
+                const loci = Loci.normalize(current.loci, 'residue');
+                const entry = this.ctx.managers.structure.focus.current;
+                if (entry && Loci.areEqual(entry.loci, loci)) {
+                    this.ctx.managers.structure.focus.clear();
+                } else {
+                    if (matched) {
+                        this.ctx.managers.structure.focus.setFromLoci(loci);
                     } else {
-                        if (matched) {
-                            this.ctx.managers.structure.focus.setFromLoci(loci);
-                        } else {
-                            this.ctx.managers.structure.focus.addFromLoci(loci);
-                        }
-                        if (isEmptyLoci(loci)) {
-                            this.ctx.managers.camera.reset();
-                        }
+                        this.ctx.managers.structure.focus.addFromLoci(loci);
+
+                        // focus-add is not handled in camera behavior, doing it here
+                        const current = this.ctx.managers.structure.focus.current?.loci;
+                        if (current) this.ctx.managers.camera.focusLoci(current);
                     }
-                    return;
                 }
             });
         }

+ 0 - 2
src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts

@@ -142,8 +142,6 @@ export class StructureFocusRepresentationBehavior extends PluginBehavior.WithSub
         builder.to(refs[StructureFocusRepresentationTags.SurrSel]!).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression: surroundings, label: this.surrLabel }));
 
         await PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
-
-        this.plugin.managers.camera.focusLoci(loci);
     }
 
     register(ref: string): void {

+ 1 - 6
src/mol-plugin/behavior/static/representation.ts

@@ -26,7 +26,6 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
     events.object.updated.subscribe(e => {
         if (e.oldObj && SO.isRepresentation3D(e.oldObj)) {
             ctx.canvas3d?.remove(e.oldObj.data.repr);
-            ctx.canvas3d?.requestDraw(true);
             e.oldObj.data.repr.destroy();
         }
 
@@ -43,7 +42,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
     events.object.removed.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
         ctx.canvas3d?.remove(e.obj.data.repr);
-        ctx.canvas3d?.requestDraw(true);
+
         e.obj.data.repr.destroy();
     });
 }
@@ -57,21 +56,18 @@ export function SyncStructureRepresentation3DState(ctx: PluginContext) {
         const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
         data.source.data.repr.setState(data.state);
         ctx.canvas3d?.update(data.source.data.repr);
-        ctx.canvas3d?.requestDraw(true);
     });
     events.object.updated.subscribe(e => {
         if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
         const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
         data.source.data.repr.setState(data.state);
         ctx.canvas3d?.update(data.source.data.repr);
-        ctx.canvas3d?.requestDraw(true);
     });
     events.object.removed.subscribe(e => {
         if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
         const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
         data.source.data.repr.setState(data.initialState);
         ctx.canvas3d?.update(data.source.data.repr);
-        ctx.canvas3d?.requestDraw(true);
     });
 }
 
@@ -82,7 +78,6 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) {
         if (!SO.isRepresentation3D(cell.obj)) return;
         updateVisibility(cell, cell.obj.data.repr);
         ctx.canvas3d?.syncVisibility();
-        ctx.canvas3d?.requestDraw(true);
     });
 }
 

+ 2 - 2
src/mol-plugin/context.ts

@@ -30,7 +30,7 @@ import { Representation } from '../mol-repr/representation';
 import { StructureRepresentationRegistry } from '../mol-repr/structure/registry';
 import { VolumeRepresentationRegistry } from '../mol-repr/volume/registry';
 import { StateTransform } from '../mol-state';
-import { Progress, Task } from '../mol-task';
+import { Progress, Task, RuntimeContext } from '../mol-task';
 import { ColorTheme } from '../mol-theme/color';
 import { SizeTheme } from '../mol-theme/size';
 import { ThemeRegistryContext } from '../mol-theme/theme';
@@ -222,7 +222,7 @@ export class PluginContext {
         this.behaviors.interaction.selectionMode.next(mode);
     }
 
-    dataTransaction(f: () => Promise<void> | void, options?: { canUndo?: string | boolean }) {
+    dataTransaction(f: (ctx: RuntimeContext) => Promise<void> | void, options?: { canUndo?: string | boolean }) {
         return this.runTask(this.state.data.transaction(f, options));
     }
 

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

@@ -19,6 +19,7 @@ import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
 import { Overpaint } from '../../mol-theme/overpaint';
 import { StructureParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
+import { Transparency } from '../../mol-theme/transparency';
 
 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,7 +95,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
             const remappedOverpaint = Overpaint.remap(state.overpaint, _structure);
             visual.setOverpaint(remappedOverpaint);
         }
-        if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency);
+        if (state.transparency !== undefined && visual) {
+            // Remap loci from equivalent structure to the current structure
+            const remappedTransparency = Transparency.remap(state.transparency, _structure);
+            visual.setTransparency(remappedTransparency);
+        }
         if (state.clipping !== undefined && visual) {
             // Remap loci from equivalent structure to the current structure
             const remappedClipping = Clipping.remap(state.clipping, _structure);

+ 3 - 1
src/mol-repr/structure/units-representation.ts

@@ -224,7 +224,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
             }
         }
         if (transparency !== undefined && !Transparency.areEqual(transparency, _state.transparency)) {
-            newState.transparency = transparency;
+            if (_structure) {
+                newState.transparency = Transparency.remap(transparency, _structure);
+            }
         }
         if (clipping !== undefined && !Clipping.areEqual(clipping, _state.clipping)) {
             if (_structure) {

+ 16 - 18
src/mol-repr/structure/visual/util/polymer/trace-iterator.ts

@@ -78,8 +78,6 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     private cyclicPolymerMap: Map<ResidueIndex, ResidueIndex>
     private secondaryStructureType: SecondaryStructure['type']
     private secondaryStructureGetIndex: SecondaryStructure['getIndex']
-    private residueSegmentBeg: ResidueIndex
-    private residueSegmentEnd: ResidueIndex
     private residueSegmentMin: ResidueIndex
     private residueSegmentMax: ResidueIndex
     private prevSecStrucType: SecondaryStructureType
@@ -122,8 +120,6 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
 
     private updateResidueSegmentRange(polymerSegment: Segmentation.Segment<number>) {
         const { index } = this.residueAtomSegments;
-        this.residueSegmentBeg = index[this.unit.elements[polymerSegment.start]];
-        this.residueSegmentEnd = index[this.unit.elements[polymerSegment.end - 1]];
         this.residueSegmentMin = index[this.polymerRanges[polymerSegment.index * 2]];
         this.residueSegmentMax = index[this.polymerRanges[polymerSegment.index * 2 + 1] - 1];
     }
@@ -186,10 +182,12 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
                 this.updateResidueSegmentRange(this.polymerSegment);
                 if (residueIt.hasNext) {
                     this.state = AtomicPolymerTraceIteratorState.nextResidue;
-                    this.currSecStrucType = SecStrucTypeNA;
-                    this.nextSecStrucType = this.getSecStruc(this.residueSegmentBeg);
+                    const residueIndexBeg = this.residueAtomSegments.index[this.unit.elements[this.polymerSegment.start]];
+                    const residueIndexBegPrev = this.getResidueIndex(residueIndexBeg - 1);
+                    this.currSecStrucType = residueIndexBeg === residueIndexBegPrev ? SecStrucTypeNA : this.getSecStruc(residueIndexBegPrev);
+                    this.nextSecStrucType = this.getSecStruc(residueIndexBeg);
                     this.currCoarseBackbone = false;
-                    this.nextCoarseBackbone = this.directionFromElementIndex[this.residueSegmentBeg] === -1 || this.directionToElementIndex[this.residueSegmentBeg] === -1;
+                    this.nextCoarseBackbone = this.directionFromElementIndex[residueIndexBeg] === -1 || this.directionToElementIndex[residueIndexBeg] === -1;
                     break;
                 }
             }
@@ -197,13 +195,20 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
 
         if (this.state === AtomicPolymerTraceIteratorState.nextResidue) {
             const { index: residueIndex } = residueIt.move();
+            const residueIndexPrev3 = this.getResidueIndex(residueIndex - 3);
+            const residueIndexPrev2 = this.getResidueIndex(residueIndex - 2);
+            const residueIndexPrev1 = this.getResidueIndex(residueIndex - 1);
+            const residueIndexNext1 = this.getResidueIndex(residueIndex + 1);
+            const residueIndexNext2 = this.getResidueIndex(residueIndex + 2);
+            const residueIndexNext3 = this.getResidueIndex(residueIndex + 3);
+
             this.prevSecStrucType = this.currSecStrucType;
             this.currSecStrucType = this.nextSecStrucType;
-            this.nextSecStrucType = residueIt.hasNext ? this.getSecStruc(residueIndex + 1) : SecStrucTypeNA;
+            this.nextSecStrucType = residueIndex === residueIndexNext1 ? SecStrucTypeNA : this.getSecStruc(residueIndexNext1);
 
             this.prevCoarseBackbone = this.currCoarseBackbone;
             this.currCoarseBackbone = this.nextCoarseBackbone;
-            this.nextCoarseBackbone = residueIt.hasNext ? (this.directionFromElementIndex[residueIndex + 1] === -1 || this.directionToElementIndex[residueIndex + 1] === -1) : false;
+            this.nextCoarseBackbone = residueIndex === residueIndexNext1 ? false : (this.directionFromElementIndex[residueIndexNext1] === -1 || this.directionToElementIndex[residueIndexNext1] === -1);
 
             value.secStrucType = this.currSecStrucType;
             value.secStrucFirst = this.prevSecStrucType !== this.currSecStrucType;
@@ -211,18 +216,11 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
             value.isCoarseBackbone = this.currCoarseBackbone;
             value.coarseBackboneFirst = this.prevCoarseBackbone !== this.currCoarseBackbone;
             value.coarseBackboneLast = this.currCoarseBackbone !== this.nextCoarseBackbone;
-            value.first = residueIndex === this.residueSegmentBeg;
-            value.last = residueIndex === this.residueSegmentEnd;
+            value.first = residueIndex === this.residueSegmentMin;
+            value.last = residueIndex === this.residueSegmentMax;
             value.moleculeType = this.moleculeType[residueIndex];
             value.isCoarseBackbone = this.directionFromElementIndex[residueIndex] === -1 || this.directionToElementIndex[residueIndex] === -1;
 
-            const residueIndexPrev3 = this.getResidueIndex(residueIndex - 3);
-            const residueIndexPrev2 = this.getResidueIndex(residueIndex - 2);
-            const residueIndexPrev1 = this.getResidueIndex(residueIndex - 1);
-            const residueIndexNext1 = this.getResidueIndex(residueIndex + 1);
-            const residueIndexNext2 = this.getResidueIndex(residueIndex + 2);
-            const residueIndexNext3 = this.getResidueIndex(residueIndex + 3);
-
             value.initial = residueIndex === residueIndexPrev1;
             value.final = residueIndex === residueIndexNext1;
 

+ 11 - 11
src/mol-repr/visual.ts

@@ -99,7 +99,7 @@ namespace Visual {
                 const end = Interval.end(interval);
                 return clear
                     ? clearOverpaint(array, start, end)
-                    : applyOverpaintColor(array, start, end, color, overpaint.alpha);
+                    : applyOverpaintColor(array, start, end, color);
             };
             lociApply(loci, apply, false);
         }
@@ -112,22 +112,22 @@ namespace Visual {
         const { tTransparency, uGroupCount, instanceCount } = renderObject.values;
         const count = uGroupCount.ref.value * instanceCount.ref.value;
 
-        const { loci, value, variant } = transparency;
-
         // ensure texture has right size and variant
-        createTransparency(value && !isEmptyLoci(loci) ? count : 0, variant, renderObject.values);
+        createTransparency(transparency.layers.length ? count : 0, renderObject.values);
         const { array } = tTransparency.ref.value;
 
         // clear if requested
         if (clear) clearTransparency(array, 0, count);
 
-        const apply = (interval: Interval) => {
-            const start = Interval.start(interval);
-            const end = Interval.end(interval);
-            return applyTransparencyValue(array, start, end, value);
-        };
-        lociApply(loci, apply, false);
-
+        for (let i = 0, il = transparency.layers.length; i < il; ++i) {
+            const { loci, value } = transparency.layers[i];
+            const apply = (interval: Interval) => {
+                const start = Interval.start(interval);
+                const end = Interval.end(interval);
+                return applyTransparencyValue(array, start, end, value);
+            };
+            lociApply(loci, apply, false);
+        }
         ValueCell.update(tTransparency, tTransparency.ref.value);
     }
 

+ 2 - 2
src/mol-state/state.ts

@@ -178,7 +178,7 @@ class State {
     private inTransactionError = false;
 
     /** Apply series of updates to the state. If any of them fail, revert to the original state. */
-    transaction(edits: () => Promise<void> | void, options?: { canUndo?: string | boolean }) {
+    transaction(edits: (ctx: RuntimeContext) => Promise<void> | void, options?: { canUndo?: string | boolean }) {
         return Task.create('State Transaction', async ctx => {
             const isNested = this.inTransaction;
 
@@ -191,7 +191,7 @@ class State {
 
                 this.inTransaction = true;
                 this.inTransactionError = false;
-                await edits();
+                await edits(ctx);
 
                 if (this.inTransactionError) {
                     restored = true;

+ 14 - 15
src/mol-theme/overpaint.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>
  */
@@ -11,20 +11,19 @@ import { Script } from '../mol-script/script';
 
 export { Overpaint };
 
-type Overpaint = { readonly layers: ReadonlyArray<Overpaint.Layer>, readonly alpha: number }
+type Overpaint = { readonly layers: ReadonlyArray<Overpaint.Layer> }
 
-function Overpaint(layers: ReadonlyArray<Overpaint.Layer>, alpha: number): Overpaint {
-    return { layers, alpha };
+function Overpaint(layers: ReadonlyArray<Overpaint.Layer>): Overpaint {
+    return { layers };
 }
 
 namespace Overpaint {
     export type Layer = { readonly loci: StructureElement.Loci, readonly color: Color, readonly clear: boolean }
-    export const Empty: Overpaint = { layers: [], alpha: 1 };
+    export const Empty: Overpaint = { layers: [] };
 
     export function areEqual(oA: Overpaint, oB: Overpaint) {
         if (oA.layers.length === 0 && oB.layers.length === 0) return true;
         if (oA.layers.length !== oB.layers.length) return false;
-        if (oA.alpha !== oB.alpha) return false;
         for (let i = 0, il = oA.layers.length; i < il; ++i) {
             if (oA.layers[i].clear !== oB.layers[i].clear) return false;
             if (oA.layers[i].color !== oB.layers[i].color) return false;
@@ -46,7 +45,7 @@ namespace Overpaint {
                 layers.push({ loci, color, clear });
             }
         }
-        return { layers, alpha: overpaint.alpha };
+        return { layers };
     }
 
     export function merge(overpaint: Overpaint): Overpaint {
@@ -72,7 +71,7 @@ namespace Overpaint {
             const color = colorOrClear === -1 ? Color(0) : colorOrClear;
             layers.push({ loci, color, clear });
         });
-        return { layers, alpha: overpaint.alpha };
+        return { layers };
     }
 
     export function filter(overpaint: Overpaint, filter: Structure): Overpaint {
@@ -89,11 +88,11 @@ namespace Overpaint {
                 layers.push({ loci, color, clear });
             }
         }
-        return { layers, alpha: overpaint.alpha };
+        return { layers };
     }
 
     export type ScriptLayer = { script: Script, color: Color, clear: boolean }
-    export function ofScript(scriptLayers: ScriptLayer[], alpha: number, structure: Structure): Overpaint {
+    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Overpaint {
         const layers: Overpaint.Layer[] = [];
         for (let i = 0, il = scriptLayers.length; i < il; ++i) {
             const { script, color, clear } = scriptLayers[i];
@@ -102,27 +101,27 @@ namespace Overpaint {
                 layers.push({ loci, color, clear });
             }
         }
-        return { layers, alpha };
+        return { layers };
     }
 
     export type BundleLayer = { bundle: StructureElement.Bundle, color: Color, clear: boolean }
-    export function ofBundle(bundleLayers: BundleLayer[], alpha: number, structure: Structure): Overpaint {
+    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Overpaint {
         const layers: Overpaint.Layer[] = [];
         for (let i = 0, il = bundleLayers.length; i < il; ++i) {
             const { bundle, color, clear } = bundleLayers[i];
             const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
             layers.push({ loci, color, clear });
         }
-        return { layers, alpha };
+        return { layers };
     }
 
-    export function toBundle(overpaint: Overpaint, alpha: number) {
+    export function toBundle(overpaint: Overpaint) {
         const layers: BundleLayer[] = [];
         for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
             let { loci, color, clear } = overpaint.layers[i];
             const bundle = StructureElement.Bundle.fromLoci(loci);
             layers.push({ bundle, color, clear });
         }
-        return { layers, alpha };
+        return { layers };
     }
 }

+ 101 - 14
src/mol-theme/transparency.ts

@@ -1,37 +1,124 @@
 /**
- * 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>
  */
 
-import { Loci, EmptyLoci } from '../mol-model/loci';
+import { Loci } from '../mol-model/loci';
 import { StructureElement, Structure } from '../mol-model/structure';
 import { Script } from '../mol-script/script';
 
 export { Transparency };
 
-interface Transparency {
-    readonly loci: Loci
-    readonly value: number
-    readonly variant: Transparency.Variant
+type Transparency = { readonly layers: ReadonlyArray<Transparency.Layer> }
+
+function Transparency(layers: ReadonlyArray<Transparency.Layer>): Transparency {
+    return { layers };
 }
 
 namespace Transparency {
+    export type Layer = { readonly loci: StructureElement.Loci, readonly value: number }
+    export const Empty: Transparency = { layers: [] };
+
     export type Variant = 'single' | 'multi'
-    export const Empty: Transparency = { loci: EmptyLoci, value: 0, variant: 'single' };
 
     export function areEqual(tA: Transparency, tB: Transparency) {
-        if (tA.value !== tB.value) return false;
-        if (tA.variant !== tB.variant) return false;
-        if (!Loci.areEqual(tA.loci, tB.loci)) return false;
+        if (tA.layers.length === 0 && tB.layers.length === 0) return true;
+        if (tA.layers.length !== tB.layers.length) return false;
+        for (let i = 0, il = tA.layers.length; i < il; ++i) {
+            if (tA.layers[i].value !== tB.layers[i].value) return false;
+            if (!Loci.areEqual(tA.layers[i].loci, tB.layers[i].loci)) return false;
+        }
         return true;
     }
 
-    export function ofScript(script: Script, value: number, variant: Variant, structure: Structure): Transparency {
-        return { loci: Script.toLoci(script, structure), value, variant };
+    export function isEmpty(transparency: Transparency) {
+        return transparency.layers.length === 0;
+    }
+
+    export function remap(transparency: Transparency, structure: Structure) {
+        const layers: Transparency.Layer[] = [];
+        for (const layer of transparency.layers) {
+            let { loci, value } = layer;
+            loci = StructureElement.Loci.remap(loci, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, value });
+            }
+        }
+        return { layers };
+    }
+
+    export function merge(transparency: Transparency): Transparency {
+        if (isEmpty(transparency)) return transparency;
+        const { structure } = transparency.layers[0].loci;
+        const map = new Map<number, StructureElement.Loci>();
+        let shadowed = StructureElement.Loci.none(structure);
+        for (let i = 0, il = transparency.layers.length; i < il; ++i) {
+            let { loci, value } = transparency.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(value)) {
+                    loci = StructureElement.Loci.union(loci, map.get(value)!);
+                }
+                map.set(value, loci);
+            }
+        }
+        const layers: Transparency.Layer[] = [];
+        map.forEach((loci, value) => {
+            layers.push({ loci, value });
+        });
+        return { layers };
+    }
+
+    export function filter(transparency: Transparency, filter: Structure): Transparency {
+        if (isEmpty(transparency)) return transparency;
+        const { structure } = transparency.layers[0].loci;
+        const layers: Transparency.Layer[] = [];
+        for (const layer of transparency.layers) {
+            let { loci, value } = layer;
+            // filter by first map to the `filter` structure and
+            // then map back to the original structure of the transparency loci
+            const filtered = StructureElement.Loci.remap(loci, filter);
+            loci = StructureElement.Loci.remap(filtered, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, value });
+            }
+        }
+        return { layers };
+    }
+
+    export type ScriptLayer = { script: Script, value: number }
+    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Transparency {
+        const layers: Transparency.Layer[] = [];
+        for (let i = 0, il = scriptLayers.length; i < il; ++i) {
+            const { script, value } = scriptLayers[i];
+            const loci = Script.toLoci(script, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, value });
+            }
+        }
+        return { layers };
+    }
+
+    export type BundleLayer = { bundle: StructureElement.Bundle, value: number }
+    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Transparency {
+        const layers: Transparency.Layer[] = [];
+        for (let i = 0, il = bundleLayers.length; i < il; ++i) {
+            const { bundle, value } = bundleLayers[i];
+            const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
+            layers.push({ loci, value });
+        }
+        return { layers };
     }
 
-    export function ofBundle(bundle: StructureElement.Bundle, value: number, variant: Variant, structure: Structure): Transparency {
-        return { loci: StructureElement.Bundle.toLoci(bundle, structure), value, variant };
+    export function toBundle(transparency: Transparency) {
+        const layers: BundleLayer[] = [];
+        for (let i = 0, il = transparency.layers.length; i < il; ++i) {
+            let { loci, value } = transparency.layers[i];
+            const bundle = StructureElement.Bundle.fromLoci(loci);
+            layers.push({ bundle, value });
+        }
+        return { layers };
     }
 }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff