Browse Source

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

Alexander Rose 3 years ago
parent
commit
1e4d1e45f9

+ 0 - 18
.github/workflows/lint.yml

@@ -1,18 +0,0 @@
-on:
-  push:
-  pull_request:
-
-jobs:
-  eslint:
-    name: eslint
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v1
-    - name: install node v14
-      uses: actions/setup-node@v1
-      with:
-        node-version: 14
-    - name: yarn install
-      run: yarn install
-    - name: eslint
-      uses: icrawl/action-eslint@v1

+ 20 - 0
.github/workflows/node.yml

@@ -0,0 +1,20 @@
+on:
+  push:
+  pull_request:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - uses: actions/setup-node@v2
+      with:
+        node-version: 14
+    - run: npm ci
+    - run: sudo apt-get install xvfb
+    - name: Lint
+      run: npm run lint
+    - name: Test
+      run: xvfb-run --auto-servernum npm run jest
+    - name: Build
+      run: npm run build

+ 8 - 0
CHANGELOG.md

@@ -6,6 +6,13 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Added ``ViewerOptions.collapseRightPanel``
+- Added ``Viewer.loadTrajectory`` to support loading "composed" trajectories (e.g. from gro + xtc)
+- Fix: handle parent in Structure.remapModel
+- Add ``rounded`` and ``square`` helix profile options to Cartoon representation (in addition to the default ``elliptical``)
+
+## [v2.3.6] - 2021-11-8
+
 - Add additional measurement controls: orientation (box, axes, ellipsoid) & plane (best fit)
 - Improve aromatic bond visuals (add ``aromaticScale``, ``aromaticSpacing``, ``aromaticDashCount`` params)
 - [Breaking] Change ``adjustCylinderLength`` default to ``false`` (set to true for focus representation)
@@ -14,6 +21,7 @@ Note that since we don't clearly distinguish between a public and private interf
     - add binary model support
     - add compartment (including membrane) geometry support
     - add latest mycoplasma model example
+- Prefer WebGL1 in Safari 15.1.
 
 ## [v2.3.5] - 2021-10-19
 

File diff suppressed because it is too large
+ 14614 - 27
package-lock.json


+ 6 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "2.3.5",
+  "version": "2.3.6",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -96,6 +96,8 @@
     "@graphql-codegen/typescript-graphql-request": "^4.1.4",
     "@graphql-codegen/typescript-operations": "^2.1.6",
     "@types/cors": "^2.8.12",
+    "@types/gl": "^4.1.0",
+    "@types/jest": "^27.0.2",
     "@typescript-eslint/eslint-plugin": "^4.32.0",
     "@typescript-eslint/parser": "^4.32.0",
     "benchmark": "^2.1.4",
@@ -129,7 +131,6 @@
     "@types/benchmark": "^2.1.1",
     "@types/compression": "1.7.2",
     "@types/express": "^4.17.13",
-    "@types/jest": "^27.0.2",
     "@types/node": "^16.10.2",
     "@types/node-fetch": "^2.5.12",
     "@types/react": "^17.0.27",
@@ -151,5 +152,8 @@
     "tslib": "^2.3.1",
     "util.promisify": "^1.1.1",
     "xhr2": "^0.2.1"
+  },
+  "optionalDependencies": {
+    "gl": "^4.9.2"
   }
 }

+ 1 - 1
src/apps/viewer/embedded.html

@@ -36,7 +36,7 @@
                 emdbProvider: 'rcsb',
             });
             viewer.loadPdb('7bv2');
-            viewer.loadEmdb('EMD-30210', { detail: 6 });
+            viewer.loadEmdb('EMD-30210', { detail: 6 });            
 
             // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
         </script>

+ 69 - 3
src/apps/viewer/index.ts

@@ -9,25 +9,28 @@ import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
 import { CellPack } from '../../extensions/cellpack';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
 import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
-import { Mp4Export } from '../../extensions/mp4-export';
 import { GeometryExport } from '../../extensions/geo-export';
+import { Mp4Export } from '../../extensions/mp4-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
 import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
 import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
+import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
 import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
 import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
+import { BuildInStructureFormat } from '../../mol-plugin-state/formats/structure';
 import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
 import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
 import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
 import { createPlugin } from '../../mol-plugin-ui';
 import { PluginUIContext } from '../../mol-plugin-ui/context';
-import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginConfig } from '../../mol-plugin/config';
+import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 import { PluginSpec } from '../../mol-plugin/spec';
 import { PluginState } from '../../mol-plugin/state';
 import { StateObjectSelector } from '../../mol-state';
@@ -71,6 +74,7 @@ const DefaultViewerOptions = {
     layoutShowLog: true,
     layoutShowLeftPanel: true,
     collapseLeftPanel: false,
+    collapseRightPanel: false,
     disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
     pixelScale: PluginConfig.General.PixelScale.defaultValue,
     pickScale: PluginConfig.General.PickScale.defaultValue,
@@ -114,7 +118,7 @@ export class Viewer {
                     regionState: {
                         bottom: 'full',
                         left: o.collapseLeftPanel ? 'collapsed' : 'full',
-                        right: 'full',
+                        right: o.collapseRightPanel ? 'hidden' : 'full',
                         top: 'full',
                     }
                 },
@@ -328,6 +332,56 @@ export class Viewer {
         });
     }
 
+    /**
+     * @example
+     *  viewer.loadTrajectory({
+     *      model: { kind: 'model-url', url: 'villin.gro', format: 'gro' },
+     *      coordinates: { kind: 'coordinates-url', url: 'villin.xtc', format: 'xtc', isBinary: true },
+     *      preset: 'all-models' // or 'default'
+     *  });
+     */
+    async loadTrajectory(params: LoadTrajectoryParams) {
+        const plugin = this.plugin;
+
+        let model: StateObjectSelector, coords: StateObjectSelector;
+
+        if (params.model.kind === 'model-data' || params.model.kind === 'model-url') {
+            const data = params.model.kind === 'model-data'
+                ? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
+                : await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
+
+            const trajectory = await plugin.builders.structure.parseTrajectory(data, params.model.format ?? 'mmcif');
+            model = await plugin.builders.structure.createModel(trajectory);
+        } else {
+            const data = params.model.kind === 'topology-data'
+                ? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
+                : await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
+
+            const provider = plugin.dataFormats.get(params.model.format);
+            model = await provider!.parse(plugin, data);
+        }
+
+        {
+            const data = params.coordinates.kind === 'coordinates-data'
+                ? await plugin.builders.data.rawData({ data: params.coordinates.data, label: params.coordinatesLabel })
+                : await plugin.builders.data.download({ url: params.coordinates.url, isBinary: params.coordinates.isBinary, label: params.coordinatesLabel });
+
+            const provider = plugin.dataFormats.get(params.coordinates.format);
+            coords = await provider!.parse(plugin, data);
+        }
+
+        const trajectory = await plugin.build().toRoot()
+            .apply(TrajectoryFromModelAndCoordinates, {
+                modelRef: model.ref,
+                coordinatesRef: coords.ref
+            }, { dependsOn: [model.ref, coords.ref] })
+            .commit();
+
+        const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, params.preset ?? 'default');
+
+        return { model, coords, preset };
+    }
+
     handleResize() {
         this.plugin.layout.events.updated.next(void 0);
     }
@@ -343,4 +397,16 @@ export interface VolumeIsovalueInfo {
     color: Color,
     alpha?: number,
     volumeIndex?: number
+}
+
+export interface LoadTrajectoryParams {
+    model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
+    | { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
+    | { kind: 'topology-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
+    | { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
+    modelLabel?: string,
+    coordinates: { kind: 'coordinates-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
+    | { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
+    coordinatesLabel?: string,
+    preset?: keyof PresetTrajectoryHierarchy
 }

+ 18 - 11
src/mol-geo/geometry/mesh/builder/sheet.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -40,7 +40,7 @@ const v3set = Vec3.set;
 const caAdd3 = ChunkedArray.add3;
 const caAdd = ChunkedArray.add;
 
-function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
+function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number, flip: boolean) {
     const { vertices, normals, indices } = state;
     const vertexCount = vertices.elementCount;
 
@@ -74,11 +74,19 @@ function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLi
         v3copy(verticalVector, verticalLeftVector);
     }
 
-    for (let i = 0; i < 4; ++i) {
-        caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
+    if (flip) {
+        for (let i = 0; i < 4; ++i) {
+            caAdd3(normals, -normalVector[0], -normalVector[1], -normalVector[2]);
+        }
+        caAdd3(indices, vertexCount, vertexCount + 1, vertexCount + 2);
+        caAdd3(indices, vertexCount + 2, vertexCount + 3, vertexCount);
+    } else {
+        for (let i = 0; i < 4; ++i) {
+            caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
+        }
+        caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+        caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
     }
-    caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
-    caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
 }
 
 /** set arrowHeight = 0 for no arrow */
@@ -193,19 +201,18 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
         const width = widthValues[0];
         const height = heightValues[0];
         const h = arrowHeight === 0 ? height : arrowHeight;
-        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h);
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h, false);
     } else if (arrowHeight > 0) {
         const width = widthValues[0];
         const height = heightValues[0];
-        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height);
-        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height);
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height, false);
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height, false);
     }
 
     if (endCap && arrowHeight === 0) {
         const width = widthValues[linearSegments];
         const height = heightValues[linearSegments];
-        // use negative height to flip the direction the cap's triangles are facing
-        addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, -height, -height);
+        addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, height, height, true);
     }
 
     const addedVertexCount = (linearSegments + 1) * 8 +

+ 60 - 20
src/mol-geo/geometry/mesh/builder/tube.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -30,9 +30,10 @@ function add3AndScale2(out: Vec3, a: Vec3, b: Vec3, c: Vec3, sa: number, sb: num
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3fromArray = Vec3.fromArray;
 const v3normalize = Vec3.normalize;
-const v3negate = Vec3.negate;
-const v3copy = Vec3.copy;
+const v3scaleAndAdd = Vec3.scaleAndAdd;
 const v3cross = Vec3.cross;
+const v3dot = Vec3.dot;
+const v3unitX = Vec3.unitX;
 const caAdd3 = ChunkedArray.add3;
 
 const CosSinCache = new Map<number, { cos: number[], sin: number[] }>();
@@ -50,13 +51,16 @@ function getCosSin(radialSegments: number) {
     return CosSinCache.get(radialSegments)!;
 }
 
-export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean) {
+export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean, crossSection: 'elliptical' | 'rounded') {
     const { currentGroup, vertices, normals, indices, groups } = state;
 
     let vertexCount = vertices.elementCount;
 
     const { cos, sin } = getCosSin(radialSegments);
 
+    const q1 = radialSegments / 4;
+    const q3 = q1 * 3;
+
     for (let i = 0; i <= linearSegments; ++i) {
         const i3 = i * 3;
         v3fromArray(u, normalVectors, i3);
@@ -65,14 +69,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
 
         const width = widthValues[i];
         const height = heightValues[i];
+        const rounded = crossSection === 'rounded' && height > width;
 
         for (let j = 0; j < radialSegments; ++j) {
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
-            if (radialSegments === 2) {
-                v3copy(normalVector, v);
-                v3normalize(normalVector, normalVector);
-                if (j !== 0 || i % 2 === 0) v3negate(normalVector, normalVector);
+            if (rounded) {
+                add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[j], width * sin[j]);
+                const h = v3dot(v, v3unitX) < 0
+                    ? (j < q1 || j >= q3) ? height - width : -height + width
+                    : (j >= q1 && j < q3) ? -height + width : height - width;
+                v3scaleAndAdd(surfacePoint, surfacePoint, u, h);
+                add2AndScale2(normalVector, u, v, cos[j], sin[j]);
             } else {
+                add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
                 add2AndScale2(normalVector, u, v, width * cos[j], height * sin[j]);
             }
             v3normalize(normalVector, normalVector);
@@ -82,19 +90,37 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         }
     }
 
+    const radialSegmentsHalf = Math.round(radialSegments / 2);
+
     for (let i = 0; i < linearSegments; ++i) {
-        for (let j = 0; j < radialSegments; ++j) {
+        // the triangles are arranged such that opposing triangles of the sheet align
+        // which prevents triangle intersection within tight curves
+        for (let j = 0; j < radialSegmentsHalf; ++j) {
+            caAdd3(
+                indices,
+                vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
+                vertexCount + i * radialSegments + j // b
+            );
+            caAdd3(
+                indices,
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
+                vertexCount + (i + 1) * radialSegments + j, // d
+                vertexCount + i * radialSegments + j // b
+            );
+        }
+        for (let j = radialSegmentsHalf; j < radialSegments; ++j) {
             caAdd3(
                 indices,
-                vertexCount + i * radialSegments + (j + 1) % radialSegments,
-                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
-                vertexCount + i * radialSegments + j
+                vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
+                vertexCount + (i + 1) * radialSegments + j, // d
+                vertexCount + i * radialSegments + j // b
             );
             caAdd3(
                 indices,
-                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
-                vertexCount + (i + 1) * radialSegments + j,
-                vertexCount + i * radialSegments + j
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
+                vertexCount + (i + 1) * radialSegments + j, // d
+                vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
             );
         }
     }
@@ -111,11 +137,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
         const width = widthValues[0];
-        const height = heightValues[0];
+        let height = heightValues[0];
+        const rounded = crossSection === 'rounded' && height > width;
+        if (rounded) height -= width;
 
         vertexCount = vertices.elementCount;
         for (let i = 0; i < radialSegments; ++i) {
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            if (rounded) {
+                add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[i], width * sin[i]);
+                v3scaleAndAdd(surfacePoint, surfacePoint, u, (i < q1 || i >= q3) ? height : -height);
+            } else {
+                add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            }
 
             caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
             caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
@@ -141,11 +174,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
         const width = widthValues[linearSegments];
-        const height = heightValues[linearSegments];
+        let height = heightValues[linearSegments];
+        const rounded = crossSection === 'rounded' && height > width;
+        if (rounded) height -= width;
 
         vertexCount = vertices.elementCount;
         for (let i = 0; i < radialSegments; ++i) {
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            if (rounded) {
+                add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[i], width * sin[i]);
+                v3scaleAndAdd(surfacePoint, surfacePoint, u, (i < q1 || i >= q3) ? height : -height);
+            } else {
+                add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            }
 
             caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
             caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);

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

@@ -245,7 +245,7 @@ export namespace Text {
             ...BaseGeometry.createValues(props, counts),
             uSizeFactor: ValueCell.create(props.sizeFactor),
 
-            uBorderWidth: ValueCell.create(clamp(props.borderWidth / 2, 0, 0.5)),
+            uBorderWidth: ValueCell.create(clamp(props.borderWidth, 0, 0.5)),
             uBorderColor: ValueCell.create(Color.toArrayNormalized(props.borderColor, Vec3.zero(), 0)),
             uOffsetX: ValueCell.create(props.offsetX),
             uOffsetY: ValueCell.create(props.offsetY),

+ 36 - 0
src/mol-gl/_spec/cylinders.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Cylinders } from '../../mol-geo/geometry/cylinders/cylinders';
+
+export function createCylinders() {
+    const cylinders = Cylinders.createEmpty();
+    const props = PD.getDefaultValues(Cylinders.Params);
+    const values = Cylinders.Utils.createValuesSimple(cylinders, props, ColorNames.orange, 1);
+    const state = Cylinders.Utils.createRenderableState(props);
+    return createRenderObject('cylinders', values, state, -1);
+}
+
+describe('cylinders', () => {
+    const ctx = tryGetGLContext(32, 32, { fragDepth: true });
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const cylinders = createCylinders();
+        scene.add(cylinders);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 36 - 0
src/mol-gl/_spec/direct-volume.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
+
+export function createDirectVolume() {
+    const directVolume = DirectVolume.createEmpty();
+    const props = PD.getDefaultValues(DirectVolume.Params);
+    const values = DirectVolume.Utils.createValuesSimple(directVolume, props, ColorNames.orange, 1);
+    const state = DirectVolume.Utils.createRenderableState(props);
+    return createRenderObject('direct-volume', values, state, -1);
+}
+
+describe('direct-volume', () => {
+    const ctx = tryGetGLContext(32, 32);
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const directVolume = createDirectVolume();
+        scene.add(directVolume);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 2 - 2
src/mol-gl/_spec/gl.shim.ts

@@ -760,8 +760,8 @@ export function createGl(width: number, height: number, contextAttributes: WebGL
         validateProgram: function () { },
         generateMipmap: function () { },
         isContextLost: function () { return false; },
-        drawingBufferWidth: 1024,
-        drawingBufferHeight: 1024,
+        drawingBufferWidth: width,
+        drawingBufferHeight: height,
         blendColor: function () { },
         blendEquation: function () { },
         blendEquationSeparate: function () { },

+ 28 - 0
src/mol-gl/_spec/gl.ts

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createContext } from '../webgl/context';
+
+export function getGLContext(width: number, height: number) {
+    const gl = require('gl')(width, height, {
+        alpha: true,
+        depth: true,
+        premultipliedAlpha: true,
+        preserveDrawingBuffer: true,
+        antialias: true,
+    });
+    return createContext(gl);
+}
+
+export function tryGetGLContext(width: number, height: number, requiredExtensions?: { fragDepth?: boolean }) {
+    try {
+        const ctx = getGLContext(width, height);
+        if (requiredExtensions?.fragDepth && !ctx.extensions.fragDepth) return;
+        return ctx;
+    } catch (e) {
+        return;
+    }
+}

+ 36 - 0
src/mol-gl/_spec/image.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Image } from '../../mol-geo/geometry/image/image';
+
+export function createImage() {
+    const image = Image.createEmpty();
+    const props = PD.getDefaultValues(Image.Params);
+    const values = Image.Utils.createValuesSimple(image, props, ColorNames.orange, 1);
+    const state = Image.Utils.createRenderableState(props);
+    return createRenderObject('image', values, state, -1);
+}
+
+describe('image', () => {
+    const ctx = tryGetGLContext(32, 32);
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const image = createImage();
+        scene.add(image);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 36 - 0
src/mol-gl/_spec/lines.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Lines } from '../../mol-geo/geometry/lines/lines';
+
+export function createLines() {
+    const lines = Lines.createEmpty();
+    const props = PD.getDefaultValues(Lines.Params);
+    const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.orange, 1);
+    const state = Lines.Utils.createRenderableState(props);
+    return createRenderObject('lines', values, state, -1);
+}
+
+describe('lines', () => {
+    const ctx = tryGetGLContext(32, 32);
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const lines = createLines();
+        scene.add(lines);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 36 - 0
src/mol-gl/_spec/mesh.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
+
+export function createMesh() {
+    const mesh = Mesh.createEmpty();
+    const props = PD.getDefaultValues(Mesh.Params);
+    const values = Mesh.Utils.createValuesSimple(mesh, props, ColorNames.orange, 1);
+    const state = Mesh.Utils.createRenderableState(props);
+    return createRenderObject('mesh', values, state, -1);
+}
+
+describe('mesh', () => {
+    const ctx = tryGetGLContext(32, 32);
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const mesh = createMesh();
+        scene.add(mesh);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 36 - 0
src/mol-gl/_spec/points.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Points } from '../../mol-geo/geometry/points/points';
+
+export function createPoints() {
+    const points = Points.createEmpty();
+    const props = PD.getDefaultValues(Points.Params);
+    const values = Points.Utils.createValuesSimple(points, props, ColorNames.orange, 1);
+    const state = Points.Utils.createRenderableState(props);
+    return createRenderObject('points', values, state, -1);
+}
+
+describe('points', () => {
+    const ctx = tryGetGLContext(32, 32);
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const points = createPoints();
+        scene.add(points);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

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

@@ -5,28 +5,14 @@
  */
 
 import { createGl } from './gl.shim';
-
 import { Camera } from '../../mol-canvas3d/camera';
-import { Vec3, Mat4, Vec4 } from '../../mol-math/linear-algebra';
-import { ValueCell } from '../../mol-util';
-
+import { Vec3 } from '../../mol-math/linear-algebra';
 import { Renderer } from '../renderer';
-import { createValueColor } from '../../mol-geo/geometry/color-data';
-import { createValueSize } from '../../mol-geo/geometry/size-data';
 import { createContext } from '../webgl/context';
-import { RenderableState } from '../renderable';
-import { createRenderObject } from '../render-object';
-import { PointsValues } from '../renderable/points';
 import { Scene } from '../scene';
-import { createEmptyMarkers } from '../../mol-geo/geometry/marker-data';
-import { fillSerial } from '../../mol-util/array';
-import { Color } from '../../mol-util/color';
-import { Sphere3D } from '../../mol-math/geometry';
-import { createEmptyOverpaint } from '../../mol-geo/geometry/overpaint-data';
-import { createEmptyTransparency } from '../../mol-geo/geometry/transparency-data';
-import { createEmptyClipping } from '../../mol-geo/geometry/clipping-data';
+import { createPoints } from './points.spec';
 
-function createRenderer(gl: WebGLRenderingContext) {
+export function createRenderer(gl: WebGLRenderingContext) {
     const ctx = createContext(gl);
     const camera = new Camera({
         position: Vec3.create(0, 0, 50)
@@ -35,85 +21,14 @@ function createRenderer(gl: WebGLRenderingContext) {
     return { ctx, camera, renderer };
 }
 
-function createPoints() {
-    const aPosition = ValueCell.create(new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]));
-    const aGroup = ValueCell.create(fillSerial(new Float32Array(3)));
-    const aInstance = ValueCell.create(fillSerial(new Float32Array(1)));
-    const color = createValueColor(Color(0xFF0000));
-    const size = createValueSize(1);
-    const marker = createEmptyMarkers();
-    const overpaint = createEmptyOverpaint();
-    const transparency = createEmptyTransparency();
-    const clipping = createEmptyClipping();
-
-    const aTransform = ValueCell.create(new Float32Array(16));
-    const m4 = Mat4.identity();
-    Mat4.toArray(m4, aTransform.ref.value, 0);
-    const transform = ValueCell.create(new Float32Array(aTransform.ref.value));
-    const extraTransform = ValueCell.create(new Float32Array(aTransform.ref.value));
-
-    const boundingSphere = ValueCell.create(Sphere3D.create(Vec3.zero(), 2));
-    const invariantBoundingSphere = ValueCell.create(Sphere3D.create(Vec3.zero(), 2));
-
-    const values: PointsValues = {
-        aPosition,
-        aGroup,
-        aTransform,
-        aInstance,
-        ...color,
-        ...marker,
-        ...size,
-        ...overpaint,
-        ...transparency,
-        ...clipping,
-
-        uAlpha: ValueCell.create(1.0),
-        uVertexCount: ValueCell.create(3),
-        uInstanceCount: ValueCell.create(1),
-        uGroupCount: ValueCell.create(3),
-        uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere.ref.value)),
-
-        uMetalness: ValueCell.create(0.0),
-        uRoughness: ValueCell.create(1.0),
-
-        alpha: ValueCell.create(1.0),
-        drawCount: ValueCell.create(3),
-        instanceCount: ValueCell.create(1),
-        matrix: ValueCell.create(m4),
-        transform,
-        extraTransform,
-        hasReflection: ValueCell.create(false),
-        boundingSphere,
-        invariantBoundingSphere,
-
-        uSizeFactor: ValueCell.create(1),
-        dPointSizeAttenuation: ValueCell.create(true),
-        dPointStyle: ValueCell.create('square'),
-
-        dLightCount: ValueCell.create(1),
-    };
-    const state: RenderableState = {
-        disposed: false,
-        visible: true,
-        alphaFactor: 1,
-        pickable: true,
-        colorOnly: false,
-        opaque: true,
-        writeDepth: true,
-        noClip: false,
-    };
-
-    return createRenderObject('points', values, state, -1);
-}
-
 describe('renderer', () => {
     it('basic', () => {
         const [width, height] = [32, 32];
         const gl = createGl(width, height, { preserveDrawingBuffer: true });
         const { ctx, renderer } = createRenderer(gl);
 
-        expect(ctx.gl.canvas.width).toBe(32);
-        expect(ctx.gl.canvas.height).toBe(32);
+        expect(ctx.gl.drawingBufferWidth).toBe(32);
+        expect(ctx.gl.drawingBufferHeight).toBe(32);
 
         expect(ctx.stats.resourceCounts.attribute).toBe(0);
         expect(ctx.stats.resourceCounts.texture).toBe(0);

+ 36 - 0
src/mol-gl/_spec/spheres.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
+
+export function createSpheres() {
+    const spheres = Spheres.createEmpty();
+    const props = PD.getDefaultValues(Spheres.Params);
+    const values = Spheres.Utils.createValuesSimple(spheres, props, ColorNames.orange, 1);
+    const state = Spheres.Utils.createRenderableState(props);
+    return createRenderObject('spheres', values, state, -1);
+}
+
+describe('spheres', () => {
+    const ctx = tryGetGLContext(32, 32, { fragDepth: true });
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const spheres = createSpheres();
+        scene.add(spheres);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 36 - 0
src/mol-gl/_spec/text.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Text } from '../../mol-geo/geometry/text/text';
+
+export function createText() {
+    const text = Text.createEmpty();
+    const props = PD.getDefaultValues(Text.Params);
+    const values = Text.Utils.createValuesSimple(text, props, ColorNames.orange, 1);
+    const state = Text.Utils.createRenderableState(props);
+    return createRenderObject('text', values, state, -1);
+}
+
+describe('text', () => {
+    const ctx = tryGetGLContext(32, 32);
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const text = createText();
+        scene.add(text);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 36 - 0
src/mol-gl/_spec/texture-mesh.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createRenderObject } from '../render-object';
+import { Scene } from '../scene';
+import { getGLContext, tryGetGLContext } from './gl';
+import { setDebugMode } from '../../mol-util/debug';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
+
+export function createTextureMesh() {
+    const textureMesh = TextureMesh.createEmpty();
+    const props = PD.getDefaultValues(TextureMesh.Params);
+    const values = TextureMesh.Utils.createValuesSimple(textureMesh, props, ColorNames.orange, 1);
+    const state = TextureMesh.Utils.createRenderableState(props);
+    return createRenderObject('texture-mesh', values, state, -1);
+}
+
+describe('texture-mesh', () => {
+    const ctx = tryGetGLContext(32, 32);
+
+    (ctx ? it : it.skip)('basic', async () => {
+        const ctx = getGLContext(32, 32);
+        const scene = Scene.create(ctx);
+        const textureMesh = createTextureMesh();
+        scene.add(textureMesh);
+        setDebugMode(true);
+        expect(() => scene.commit()).not.toThrow();
+        setDebugMode(false);
+        ctx.destroy();
+    });
+});

+ 4 - 1
src/mol-gl/webgl/context.ts

@@ -358,7 +358,10 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
             unbindResources(gl);
 
             // to aid GC
-            if (!options?.doNotForceWebGLContextLoss) gl.getExtension('WEBGL_lose_context')?.loseContext();
+            if (!options?.doNotForceWebGLContextLoss) {
+                gl.getExtension('WEBGL_lose_context')?.loseContext();
+                gl.getExtension('STACKGL_destroy_context')?.destroy();
+            }
         }
     };
 }

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

@@ -355,8 +355,8 @@ class Structure {
         return this.models.indexOf(m);
     }
 
-    remapModel(m: Model) {
-        const { dynamicBonds, interUnitBonds } = this.state;
+    remapModel(m: Model): Structure {
+        const { dynamicBonds, interUnitBonds, parent } = this.state;
         const units: Unit[] = [];
         for (const ug of this.unitSymmetryGroups) {
             const unit = ug.units[0].remapModel(m, dynamicBonds);
@@ -367,6 +367,7 @@ class Structure {
             }
         }
         return Structure.create(units, {
+            parent: parent?.remapModel(m),
             label: this.label,
             interUnitBonds: dynamicBonds ? undefined : interUnitBonds,
             dynamicBonds

+ 2 - 0
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -379,6 +379,8 @@ const atomicDetail = StructureRepresentationPresetProvider({
         }
 
         await update.commit({ revertOnError: true });
+        await updateFocusRepr(plugin, structure, params.theme?.focus?.name ?? color, params.theme?.focus?.params ?? colorParams);
+
         return { components, representations };
     }
 });

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

@@ -17,10 +17,12 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common';
 import { LineParams } from '../../../mol-repr/structure/representation/line';
 import { Expression } from '../../../mol-script/language/expression';
+import { Color } from '../../../mol-util/color';
 
 export { StructureMeasurementManager };
 
 export const MeasurementGroupTag = 'measurement-group';
+export const MeasurementOrderLabelTag = 'measurement-order-label';
 
 export type StructureMeasurementCell = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Structure.Selections, PluginStateObject.Shape.Representation3D, any>>>
 
@@ -281,6 +283,42 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
     }
 
+    async addOrderLabels(locis: StructureElement.Loci[]) {
+        const update = this.getGroup();
+
+        const current = this.plugin.state.data.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Selections).withTag(MeasurementOrderLabelTag));
+        for (const obj of current)
+            update.delete(obj);
+
+        let order = 1;
+        for (const loci of locis) {
+            const cell = this.plugin.helpers.substructureParent.get(loci.structure);
+            if (!cell) continue;
+
+            const dependsOn = [cell.transform.ref];
+
+            update
+                .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                    selections: [
+                        { key: 'a', ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(loci) },
+                    ],
+                    isTransitive: true,
+                    label: 'Order'
+                }, { dependsOn, tags: MeasurementOrderLabelTag })
+                .apply(StateTransforms.Representation.StructureSelectionsLabel3D, {
+                    textColor: Color.fromRgb(255, 255, 255),
+                    borderColor: Color.fromRgb(0, 0, 0),
+                    textSize: 0.33,
+                    borderWidth: 0.3,
+                    offsetZ: 0.75,
+                    customText: `${order++}`
+                }, { tags: MeasurementOrderLabelTag });
+        }
+
+        const state = this.plugin.state.data;
+        await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+    }
+
     private _empty: any[] = [];
     private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) {
         const state = this.plugin.state.data;
@@ -291,8 +329,15 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
     }
 
     private sync() {
+        const labels = [];
+        for (const cell of this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D) as StructureMeasurementCell[]) {
+            const tags = (cell.obj as any)['tags'] as string[];
+            if (!tags || !tags.includes(MeasurementOrderLabelTag))
+                labels.push(cell);
+        }
+
         const updated = this.updateState({
-            labels: this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D),
+            labels,
             distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D),
             angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D),
             dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D),

+ 30 - 3
src/mol-plugin-ui/structure/measurements.tsx

@@ -62,7 +62,6 @@ export class MeasurementList extends PurePluginUIComponent {
 
     render() {
         const measurements = this.plugin.managers.structure.measurement.state;
-
         return <div style={{ marginTop: '6px' }}>
             {this.renderGroup(measurements.labels, 'Labels')}
             {this.renderGroup(measurements.distances, 'Distances')}
@@ -80,6 +79,7 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
     componentDidMount() {
         this.subscribe(this.selection.events.additionsHistoryUpdated, () => {
             this.forceUpdate();
+            this.updateOrderLabels();
         });
 
         this.subscribe(this.plugin.behaviors.state.isBusy, v => {
@@ -87,6 +87,33 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
         });
     }
 
+    componentWillUnmount() {
+        this.clearOrderLabels();
+        super.componentWillUnmount();
+    }
+
+    componentDidUpdate(prevProps: {}, prevState: { isBusy: boolean, action?: 'add' | 'options' }) {
+        if (this.state.action !== prevState.action)
+            this.updateOrderLabels();
+    }
+
+    clearOrderLabels() {
+        this.plugin.managers.structure.measurement.addOrderLabels([]);
+    }
+
+    updateOrderLabels() {
+        if (this.state.action !== 'add') {
+            this.clearOrderLabels();
+            return;
+        }
+
+        const locis = [];
+        const history = this.selection.additionsHistory;
+        for (let idx = 0; idx < history.length && idx < 4; idx++)
+            locis.push(history[idx].loci);
+        this.plugin.managers.structure.measurement.addOrderLabels(locis);
+    }
+
     get selection() {
         return this.plugin.managers.structure.selection;
     }
@@ -163,8 +190,8 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
 
     historyEntry(e: StructureSelectionHistoryEntry, idx: number) {
         const history = this.plugin.managers.structure.selection.additionsHistory;
-        return <div className='msp-flex-row' key={e.id}>
-            <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}>
+        return <div className='msp-flex-row' key={e.id} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}>
+            <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }}>
                 {idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} />
             </Button>
             {history.length > 1 && <IconButton svg={ArrowUpwardSvg} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} flex='20px' title={'Move up'} />}

+ 13 - 3
src/mol-plugin/config.ts

@@ -20,9 +20,19 @@ export class PluginConfigItem<T = any> {
 function item<T>(key: string, defaultValue?: T) { return new PluginConfigItem(key, defaultValue); }
 
 
-// adapted from https://stackoverflow.com/questions/9038625/detect-if-device-is-ios
-function is_iOS() {
+function preferWebGl1() {
     if (typeof navigator === 'undefined' || typeof window === 'undefined') return false;
+
+    // WebGL2 isn't working in MacOS 12.0.1 Safari 15.1 (but is working in Safari tech preview)
+    // prefer webgl 1 based on the userAgent substring
+    if (navigator.userAgent.indexOf('Version/15.1 Safari') > 0) {
+        return true;
+    }
+
+    // Check for iOS device which enabled WebGL2 recently but it doesn't seem
+    // to be full up to speed yet.
+
+    // adapted from https://stackoverflow.com/questions/9038625/detect-if-device-is-ios
     const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
     const isAppleDevice = navigator.userAgent.includes('Macintosh');
     const isTouchScreen = navigator.maxTouchPoints >= 4; // true for iOS 13 (and hopefully beyond)
@@ -41,7 +51,7 @@ export const PluginConfig = {
         EnableWboit: item('plugin-config.enable-wboit', true),
         // as of Oct 1 2021, WebGL 2 doesn't work on iOS 15.
         // TODO: check back in a few weeks to see if it was fixed
-        PreferWebGl1: item('plugin-config.prefer-webgl1', is_iOS()),
+        PreferWebGl1: item('plugin-config.prefer-webgl1', preferWebGl1()),
     },
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),

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

@@ -27,8 +27,9 @@ import { StructureGroup } from './util/common';
 export const PolymerTraceMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
     aspectRatio: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }),
-    arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }),
-    tubularHelices: PD.Boolean(false),
+    arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Size factor for sheet arrows' }),
+    tubularHelices: PD.Boolean(false, { description: 'Draw alpha helices as tubes' }),
+    helixProfile: PD.Select('elliptical', PD.arrayToOptions(['elliptical', 'rounded', 'square'] as const), { description: 'Protein and nucleic helix trace profile' }),
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo)
@@ -42,7 +43,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
     const polymerElementCount = unit.polymerElements.length;
 
     if (!polymerElementCount) return Mesh.createEmpty(mesh);
-    const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices } = props;
+    const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices, helixProfile } = props;
 
     const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2;
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
@@ -131,9 +132,6 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
                 h0 = w0 * aspectRatio;
                 h1 = w1 * aspectRatio;
                 h2 = w2 * aspectRatio;
-                [w0, h0] = [h0, w0];
-                [w1, h1] = [h1, w1];
-                [w2, h2] = [h2, w2];
             } else {
                 h0 = w0;
                 h1 = w1;
@@ -142,18 +140,26 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
 
             interpolateSizes(state, w0, w1, w2, h0, h1, h2, shift);
 
+            const [normals, binormals] = isNucleicType && !v.isCoarseBackbone ? [binormalVectors, normalVectors] : [normalVectors, binormalVectors];
+            if (isNucleicType && !v.isCoarseBackbone) {
+                // TODO: find a cleaner way to swap normal and binormal for nucleic types
+                for (let i = 0, il = normals.length; i < il; i++) normals[i] *= -1;
+            }
+
             if (radialSegments === 2) {
                 if (isNucleicType && !v.isCoarseBackbone) {
-                    // TODO find a cleaner way to swap normal and binormal for nucleic types
-                    for (let i = 0, il = binormalVectors.length; i < il; i++) binormalVectors[i] *= -1;
-                    addRibbon(builderState, curvePoints, binormalVectors, normalVectors, segmentCount, heightValues, widthValues, 0);
+                    addRibbon(builderState, curvePoints, normals, binormals, segmentCount, heightValues, widthValues, 0);
                 } else {
-                    addRibbon(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0);
+                    addRibbon(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0);
                 }
             } else if (radialSegments === 4) {
-                addSheet(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0, startCap, endCap);
+                addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
+            } else if (h1 === w1) {
+                addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, 'elliptical');
+            } else if (helixProfile === 'square') {
+                addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
             } else {
-                addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap);
+                addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, helixProfile);
             }
         }
 
@@ -189,7 +195,8 @@ export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTrace
                 newProps.linearSegments !== currentProps.linearSegments ||
                 newProps.radialSegments !== currentProps.radialSegments ||
                 newProps.aspectRatio !== currentProps.aspectRatio ||
-                newProps.arrowFactor !== currentProps.arrowFactor
+                newProps.arrowFactor !== currentProps.arrowFactor ||
+                newProps.helixProfile !== currentProps.helixProfile
             );
 
             const secondaryStructureHash = SecondaryStructureProvider.get(newStructureGroup.structure).version;

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

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

+ 1 - 0
tsconfig.commonjs.json

@@ -14,6 +14,7 @@
         "moduleResolution": "node",
         "importHelpers": true,
         "noEmitHelpers": true,
+        "allowSyntheticDefaultImports": true,
         "jsx": "react-jsx",
         "lib": [ "es6", "dom", "esnext.asynciterable", "es2016" ],
         "rootDir": "src",

+ 1 - 0
tsconfig.json

@@ -14,6 +14,7 @@
         "moduleResolution": "node",
         "importHelpers": true,
         "noEmitHelpers": true,
+        "allowSyntheticDefaultImports": true,
         "jsx": "react-jsx",
         "lib": [ "es6", "dom", "esnext.asynciterable", "es2016" ],
         "rootDir": "src",

Some files were not shown because too many files changed in this diff