Browse Source

Merge commit '2faa821c50a6dfce700eb8072a61d01d937c18e5' into stubs2

Alexander Rose 4 years ago
parent
commit
83dcdfdc4b
42 changed files with 1009 additions and 162 deletions
  1. 4 11
      README.md
  2. 2 2
      package-lock.json
  3. 1 1
      package.json
  4. 20 0
      src/mol-canvas3d/camera.ts
  5. 20 4
      src/mol-canvas3d/canvas3d.ts
  6. 15 1
      src/mol-canvas3d/controls/trackball.ts
  7. 106 18
      src/mol-canvas3d/helper/camera-helper.ts
  8. 8 2
      src/mol-canvas3d/passes/pick.ts
  9. 4 4
      src/mol-canvas3d/util.ts
  10. 10 1
      src/mol-io/reader/common/text/tokenizer.ts
  11. 1 1
      src/mol-io/reader/xyz/parser.ts
  12. 24 16
      src/mol-math/geometry/spacegroup/construction.ts
  13. 3 0
      src/mol-math/linear-algebra/3d/vec3.ts
  14. 1 1
      src/mol-model-props/common/custom-element-property.ts
  15. 4 3
      src/mol-model/loci.ts
  16. 107 0
      src/mol-model/structure/model/properties/utils/residue-set.ts
  17. 253 1
      src/mol-model/structure/query/queries/modifiers.ts
  18. 4 1
      src/mol-model/structure/structure/element/loci.ts
  19. 2 2
      src/mol-model/structure/structure/properties.ts
  20. 20 26
      src/mol-model/structure/structure/structure.ts
  21. 30 0
      src/mol-model/structure/structure/util/lookup3d.ts
  22. 9 2
      src/mol-plugin-state/builder/structure/representation-preset.ts
  23. 13 0
      src/mol-plugin-state/helpers/structure-selection-query.ts
  24. 3 1
      src/mol-plugin-state/transforms.ts
  25. 39 0
      src/mol-plugin-state/transforms/representation.ts
  26. 69 0
      src/mol-plugin-state/transforms/shape.ts
  27. 4 6
      src/mol-plugin-ui/sequence/polymer.ts
  28. 5 0
      src/mol-plugin-ui/spec.ts
  29. 17 2
      src/mol-plugin-ui/structure/source.tsx
  30. 66 1
      src/mol-plugin/behavior/dynamic/camera.ts
  31. 2 0
      src/mol-plugin/spec.ts
  32. 6 0
      src/mol-script/language/symbol-table/structure-query.ts
  33. 7 0
      src/mol-script/runtime/query/table.ts
  34. 1 0
      src/mol-script/script/mol-script/symbols.ts
  35. 73 37
      src/mol-util/marker-action.ts
  36. 6 2
      src/mol-util/type-helpers.ts
  37. 1 0
      src/servers/common/swagger-ui/indexTemplate.ts
  38. 3 0
      src/servers/model/CHANGELOG.md
  39. 29 1
      src/servers/model/server/api.ts
  40. 1 0
      src/servers/model/server/query.ts
  41. 1 1
      src/servers/model/version.ts
  42. 15 14
      src/servers/plugin-state/index.ts

+ 4 - 11
README.md

@@ -5,15 +5,12 @@
 
 # Mol*
 
-The goal of **Mol\*** (*/'mol-star/*) is to provide a technology stack that will serve as a basis for the next-generation data delivery and analysis tools for macromolecular structure data. This is a collaboration between PDBe and RCSB PDB teams and the development will be open-source and available to anyone who wants to use it for developing visualization tools for macromolecular structure data available from [PDB](https://www.wwpdb.org/) and other institutions.
+The goal of **Mol\*** (*/'mol-star/*) is to provide a technology stack that serves as a basis for the next-generation data delivery and analysis tools for (not only) macromolecular structure data. Mol* development was jointly initiated by PDBe and RCSB PDB to combine and build on the strengths of [LiteMol](https://litemol.org) (developed by PDBe) and [NGL](https://nglviewer.org) (developed by RCSB PDB) viewers.
 
-This particular project is the implementation of this technology (still under development).
 
-*If you are looking for the "MOLeculAR structure annoTator", that package is now available on NPM as [MolArt](https://www.npmjs.com/package/molart).*
+## Project Structure Overview
 
-## Project Overview
-
-The core of Mol* currently consists of these modules (see under `src/`):
+The core of Mol* consists of these modules (see under `src/`):
 
 - `mol-task` Computation abstraction with progress tracking and cancellation support.
 - `mol-data` Collections (integer-based sets, interface to columns/tables, etc.)
@@ -29,7 +26,6 @@ The core of Mol* currently consists of these modules (see under `src/`):
 - `mol-gl` A wrapper around WebGL.
 - `mol-canvas3d` A low-level 3d view component. Uses `mol-geo` to generate geometries.
 - `mol-state` State representation tree with state saving and automatic updates.
-- `mol-app` Components for building UIs.
 - `mol-plugin` Allow to define modular Mol* plugin instances utilizing `mol-state` and `mol-canvas3d`.
 - `mol-plugin-state` State transformations, builders, and managers.
 - `mol-plugin-ui` React-based user interface for the Mol* plugin. Some components of the UI are usable outside the main plugin and can be integrated into 3rd party solutions.
@@ -41,7 +37,7 @@ Moreover, the project contains the implementation of `servers`, including
 - `servers/volume` A tool for accessing volumetric experimental data related to molecular structures.
 - `servers/plugin-state` A basic server to store Mol* Plugin states.
 
-The project also contains performance tests (`perf-tests`), `examples`, and basic proof of concept `cli` apps (CIF to BinaryCIF converter and JSON domain annotation to CIF converter).
+The project also contains performance tests (`perf-tests`), `examples`, and `cli` apps (CIF to BinaryCIF converter and JSON domain annotation to CIF converter).
 
 ## Previous Work
 This project builds on experience from previous solutions:
@@ -169,9 +165,6 @@ To get syntax highlighting for shader and graphql files add the following to Vis
 ## Contributing
 Just open an issue or make a pull request. All contributions are welcome.
 
-## Roadmap
-Continually develop this prototype project. As individual modules become stable, make them into standalone libraries.
-
 ## Funding
 Funding sources include but are not limited to:
 * [RCSB PDB](https://www.rcsb.org) funding by a grant [DBI-1338415; PI: SK Burley] from the NSF, the NIH, and the US DoE

+ 2 - 2
package-lock.json

@@ -1,11 +1,11 @@
 {
   "name": "molstar",
-  "version": "2.0.0-dev.9",
+  "version": "2.0.0-dev.11",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "version": "2.0.0-dev.8",
+      "version": "2.0.0-dev.11",
       "license": "MIT",
       "dependencies": {
         "@types/argparse": "^1.0.38",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "2.0.0-dev.9",
+  "version": "2.0.0-dev.11",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {

+ 20 - 0
src/mol-canvas3d/camera.ts

@@ -9,6 +9,7 @@ import { Mat4, Vec3, Vec4, EPSILON } from '../mol-math/linear-algebra';
 import { Viewport, cameraProject, cameraUnproject } from './camera/util';
 import { CameraTransitionManager } from './camera/transition';
 import { BehaviorSubject } from 'rxjs';
+import { Scene } from '../mol-gl/scene';
 
 export { ICamera, Camera };
 
@@ -126,6 +127,23 @@ class Camera implements ICamera {
         return state;
     }
 
+    getInvariantFocus(target: Vec3, radius: number, up: Vec3, dir: Vec3): Partial<Camera.Snapshot> {
+        const r = Math.max(radius, 0.01);
+        const targetDistance = this.getTargetDistance(r);
+
+        Vec3.copy(this.deltaDirection, dir);
+        Vec3.setMagnitude(this.deltaDirection, this.deltaDirection, targetDistance);
+        Vec3.sub(this.newPosition, target, this.deltaDirection);
+
+        const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
+        state.target = Vec3.clone(target);
+        state.radius = r;
+        state.position = Vec3.clone(this.newPosition);
+        Vec3.copy(state.up, up);
+
+        return state;
+    }
+
     focus(target: Vec3, radius: number, durationMs?: number, up?: Vec3, dir?: Vec3) {
         if (radius > 0) {
             this.setState(this.getFocus(target, radius, up, dir), durationMs);
@@ -150,6 +168,8 @@ class Camera implements ICamera {
 namespace Camera {
     export type Mode = 'perspective' | 'orthographic'
 
+    export type SnapshotProvider = Partial<Snapshot> | ((scene: Scene, camera: Camera) => Partial<Snapshot>)
+
     /**
      * Sets an offseted view in a larger frustum. This is useful for
      * - multi-window or multi-monitor/multi-machine setups

+ 20 - 4
src/mol-canvas3d/canvas3d.ts

@@ -229,7 +229,7 @@ interface Canvas3D {
     /** performs handleResize on the next animation frame */
     requestResize(): void
     /** Focuses camera on scene's bounding sphere, centered and zoomed. */
-    requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
+    requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider }): void
     readonly camera: Camera
     readonly boundingSphere: Readonly<Sphere3D>
     setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
@@ -296,7 +296,7 @@ namespace Canvas3D {
         let drawPending = false;
         let cameraResetRequested = false;
         let nextCameraResetDuration: number | undefined = void 0;
-        let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0;
+        let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
         let resizeRequested = false;
 
         let notifyDidDraw = true;
@@ -305,7 +305,11 @@ namespace Canvas3D {
             let loci: Loci = EmptyLoci;
             let repr: Representation.Any = Representation.Empty;
             if (pickingId) {
+                const cameraHelperLoci = helper.camera.getLoci(pickingId);
+                if (cameraHelperLoci !== EmptyLoci) return { loci: cameraHelperLoci, repr };
+
                 loci = helper.handle.getLoci(pickingId);
+
                 reprRenderObjects.forEach((_, _repr) => {
                     const _loci = _repr.getLoci(pickingId);
                     if (!isEmptyLoci(_loci)) {
@@ -327,11 +331,13 @@ namespace Canvas3D {
                 changed = repr.mark(loci, action);
             } else {
                 changed = helper.handle.mark(loci, action);
+                changed = helper.camera.mark(loci, action) || changed;
                 reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
             }
             if (changed) {
                 scene.update(void 0, true);
                 helper.handle.scene.update(void 0, true);
+                helper.camera.scene.update(void 0, true);
                 const prevPickDirty = pickHelper.dirty;
                 draw(true);
                 pickHelper.dirty = prevPickDirty; // marking does not change picking buffers
@@ -453,11 +459,21 @@ namespace Canvas3D {
         function resolveCameraReset() {
             if (!cameraResetRequested) return;
 
-            const { center, radius } = scene.boundingSphereVisible;
+            const boundingSphere = scene.boundingSphereVisible;
+            const { center, radius } = boundingSphere;
+
+            const autoAdjustControls = controls.props.autoAdjustMinMaxDistance;
+            if (autoAdjustControls.name === 'on') {
+                const minDistance = autoAdjustControls.params.minDistanceFactor * radius + autoAdjustControls.params.minDistancePadding;
+                const maxDistance = Math.max(autoAdjustControls.params.maxDistanceFactor * radius, autoAdjustControls.params.maxDistanceMin);
+                controls.setProps({ minDistance, maxDistance });
+            }
+
             if (radius > 0) {
                 const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration;
                 const focus = camera.getFocus(center, radius);
-                const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus;
+                const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
+                const snapshot = next ? { ...focus, ...next } : focus;
                 camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
             }
 

+ 15 - 1
src/mol-canvas3d/controls/trackball.ts

@@ -49,7 +49,21 @@ export const TrackballControlsParams = {
     minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
     maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
 
-    bindings: PD.Value(DefaultTrackballBindings, { isHidden: true })
+    bindings: PD.Value(DefaultTrackballBindings, { isHidden: true }),
+
+    /**
+     * minDistance = minDistanceFactor * boundingSphere.radius + minDistancePadding
+     * maxDistance = max(maxDistanceFactor * boundingSphere.radius, maxDistanceMin)
+     */
+    autoAdjustMinMaxDistance: PD.MappedStatic('on', {
+        off: PD.EmptyGroup(),
+        on: PD.Group({
+            minDistanceFactor: PD.Numeric(0),
+            minDistancePadding: PD.Numeric(5),
+            maxDistanceFactor: PD.Numeric(10),
+            maxDistanceMin: PD.Numeric(20)
+        })
+    }, { isHidden: true })
 };
 export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
 

+ 106 - 18
src/mol-canvas3d/helper/camera-helper.ts

@@ -1,30 +1,35 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLContext } from '../../mol-gl/webgl/context';
-import { Scene } from '../../mol-gl/scene';
-import { Camera, ICamera } from '../camera';
-import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
-import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
+import produce from 'immer';
+import { Interval } from '../../mol-data/int/interval';
+import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
 import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
-import { GraphicsRenderObject } from '../../mol-gl/render-object';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
-import { ColorNames } from '../../mol-util/color/names';
-import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
-import { Viewport } from '../camera/util';
+import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
+import { PickingId } from '../../mol-geo/geometry/picking';
+import { GraphicsRenderObject } from '../../mol-gl/render-object';
+import { Scene } from '../../mol-gl/scene';
+import { WebGLContext } from '../../mol-gl/webgl/context';
 import { Sphere3D } from '../../mol-math/geometry';
-import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import produce from 'immer';
+import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
+import { DataLoci, EmptyLoci, Loci } from '../../mol-model/loci';
 import { Shape } from '../../mol-model/shape';
+import { Visual } from '../../mol-repr/visual';
+import { ColorNames } from '../../mol-util/color/names';
+import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Camera, ICamera } from '../camera';
+import { Viewport } from '../camera/util';
 
 // TODO add scale line/grid
 
 const AxesParams = {
     ...Mesh.Params,
-    alpha: { ...Mesh.Params.alpha, defaultValue: 0.33 },
+    alpha: { ...Mesh.Params.alpha, defaultValue: 0.51 },
     ignoreLight: { ...Mesh.Params.ignoreLight, defaultValue: true },
     colorX: PD.Color(ColorNames.red, { isEssential: true }),
     colorY: PD.Color(ColorNames.green, { isEssential: true }),
@@ -87,6 +92,32 @@ export class CameraHelper {
         return this.props.axes.name === 'on';
     }
 
+    getLoci(pickingId: PickingId) {
+        const { objectId, groupId, instanceId } = pickingId;
+        if (!this.renderObject || objectId !== this.renderObject.id || groupId === CameraHelperAxis.None) return EmptyLoci;
+        return CameraAxesLoci(this, groupId, instanceId);
+    }
+
+    private eachGroup = (loci: Loci, apply: (interval: Interval) => boolean): boolean => {
+        if (!this.renderObject) return false;
+        if (!isCameraAxesLoci(loci)) return false;
+        let changed = false;
+        const groupCount = this.renderObject.values.uGroupCount.ref.value;
+        const { elements } = loci;
+        for (const { groupId, instanceId } of elements) {
+            const idx = instanceId * groupCount + groupId;
+            if (apply(Interval.ofSingleton(idx))) changed = true;
+        }
+        return changed;
+    }
+
+    mark(loci: Loci, action: MarkerAction) {
+        if (!MarkerActions.is(MarkerActions.Highlighting, action)) return false;
+        if (!isCameraAxesLoci(loci)) return false;
+        if (loci.data !== this) return false;
+        return Visual.mark(this.renderObject, loci, action, this.eachGroup);
+    }
+
     update(camera: ICamera) {
         if (!this.renderObject) return;
 
@@ -102,6 +133,38 @@ export class CameraHelper {
     }
 }
 
+export const enum CameraHelperAxis {
+    None = 0,
+    X,
+    Y,
+    Z,
+    XY,
+    XZ,
+    YZ
+}
+
+function getAxisLabel(axis: number) {
+    switch (axis) {
+        case CameraHelperAxis.X: return 'X Axis';
+        case CameraHelperAxis.Y: return 'Y Axis';
+        case CameraHelperAxis.Z: return 'Z Axis';
+        case CameraHelperAxis.XY: return 'XY Plane';
+        case CameraHelperAxis.XZ: return 'XZ Plane';
+        case CameraHelperAxis.YZ: return 'YZ Plane';
+        default: return 'Axes';
+    }
+}
+
+function CameraAxesLoci(cameraHelper: CameraHelper, groupId: number, instanceId: number) {
+    return DataLoci('camera-axes', cameraHelper, [{ groupId, instanceId }],
+        void 0 /** bounding sphere */,
+        () => getAxisLabel(groupId));
+}
+export type CameraAxesLoci = ReturnType<typeof CameraAxesLoci>
+export function isCameraAxesLoci(x: Loci): x is CameraAxesLoci {
+    return x.kind === 'data-loci' && x.tag === 'camera-axes';
+}
+
 function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.ViewOffset) {
     const { near, far } = camera;
 
@@ -134,27 +197,52 @@ function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.Vie
 
 function createAxesMesh(scale: number, mesh?: Mesh) {
     const state = MeshBuilder.createState(512, 256, mesh);
-    const radius = 0.05 * scale;
+    const radius = 0.075 * scale;
     const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
     const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
     const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
     const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
 
-    state.currentGroup = 0;
+    state.currentGroup = CameraHelperAxis.None;
     addSphere(state, Vec3.origin, radius, 2);
 
-    state.currentGroup = 1;
+    state.currentGroup = CameraHelperAxis.X;
     addSphere(state, x, radius, 2);
     addCylinder(state, Vec3.origin, x, 1, cylinderProps);
 
-    state.currentGroup = 2;
+    state.currentGroup = CameraHelperAxis.Y;
     addSphere(state, y, radius, 2);
     addCylinder(state, Vec3.origin, y, 1, cylinderProps);
 
-    state.currentGroup = 3;
+    state.currentGroup = CameraHelperAxis.Z;
     addSphere(state, z, radius, 2);
     addCylinder(state, Vec3.origin, z, 1, cylinderProps);
 
+    Vec3.scale(x, x, 0.5);
+    Vec3.scale(y, y, 0.5);
+    Vec3.scale(z, z, 0.5);
+
+    state.currentGroup = CameraHelperAxis.XY;
+    MeshBuilder.addTriangle(state, Vec3.origin, x, y);
+    MeshBuilder.addTriangle(state, Vec3.origin, y, x);
+    const xy = Vec3.add(Vec3(), x, y);
+    MeshBuilder.addTriangle(state, xy, x, y);
+    MeshBuilder.addTriangle(state, xy, y, x);
+
+    state.currentGroup = CameraHelperAxis.XZ;
+    MeshBuilder.addTriangle(state, Vec3.origin, x, z);
+    MeshBuilder.addTriangle(state, Vec3.origin, z, x);
+    const xz = Vec3.add(Vec3(), x, z);
+    MeshBuilder.addTriangle(state, xz, x, z);
+    MeshBuilder.addTriangle(state, xz, z, x);
+
+    state.currentGroup = CameraHelperAxis.YZ;
+    MeshBuilder.addTriangle(state, Vec3.origin, y, z);
+    MeshBuilder.addTriangle(state, Vec3.origin, z, y);
+    const yz = Vec3.add(Vec3(), y, z);
+    MeshBuilder.addTriangle(state, yz, y, z);
+    MeshBuilder.addTriangle(state, yz, z, y);
+
     return MeshBuilder.getMesh(state);
 }
 

+ 8 - 2
src/mol-canvas3d/passes/pick.ts

@@ -66,14 +66,20 @@ export class PickPass {
     private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: GraphicsRenderVariant) {
         const depth = this.drawPass.depthTexturePrimitives;
         renderer.clear(false);
+
+        renderer.update(camera);
         renderer.renderPick(scene.primitives, camera, variant, null);
         renderer.renderPick(scene.volumes, camera, variant, depth);
         renderer.renderPick(helper.handle.scene, camera, variant, null);
+
+        if (helper.camera.isEnabled) {
+            helper.camera.update(camera);
+            renderer.update(helper.camera.camera);
+            renderer.renderPick(helper.camera.scene, helper.camera.camera, variant, null);
+        }
     }
 
     render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper) {
-        renderer.update(camera);
-
         this.objectPickTarget.bind();
         this.renderVariant(renderer, camera, scene, helper, 'pickObject');
 

+ 4 - 4
src/mol-canvas3d/util.ts

@@ -12,13 +12,13 @@ export function setCanvasSize(canvas: HTMLCanvasElement, width: number, height:
 }
 
 /** Resize canvas to container element taking `devicePixelRatio` into account */
-export function resizeCanvas (canvas: HTMLCanvasElement, container: Element, scale = 1) {
+export function resizeCanvas (canvas: HTMLCanvasElement, container: HTMLElement, scale = 1) {
     let width = window.innerWidth;
     let height = window.innerHeight;
     if (container !== document.body) {
-        let bounds = container.getBoundingClientRect();
-        width = bounds.right - bounds.left;
-        height = bounds.bottom - bounds.top;
+        // fixes issue #molstar/molstar#147, offsetWidth/offsetHeight is correct size when css transform:scale is used
+        width = container.offsetWidth;
+        height = container.offsetHeight;
     }
     setCanvasSize(canvas, width, height, scale);
 }

+ 10 - 1
src/mol-io/reader/common/text/tokenizer.ts

@@ -91,12 +91,21 @@ namespace Tokenizer {
         return eatLine(state);
     }
 
-    /** Advance the state by the given number of lines and return line as string. */
+    /** Advance the state and return line as string. */
     export function readLine(state: Tokenizer): string {
         markLine(state);
         return getTokenString(state);
     }
 
+    /** Advance the state and return trimmed line as string. */
+    export function readLineTrim(state: Tokenizer): string {
+        markLine(state);
+        const position = state.position;
+        trim(state, state.tokenStart, state.tokenEnd);
+        state.position = position;
+        return getTokenString(state);
+    }
+
     function readLinesChunk(state: Tokenizer, count: number, tokens: Tokens) {
         let read = 0;
         for (let i = 0; i < count; i++) {

+ 1 - 1
src/mol-io/reader/xyz/parser.ts

@@ -32,7 +32,7 @@ function handleMolecule(tokenizer: Tokenizer): XyzFile['molecules'][number] {
     const type_symbol = new Array<string>(count);
 
     for (let i = 0; i < count; ++i) {
-        const line = Tokenizer.readLine(tokenizer);
+        const line = Tokenizer.readLineTrim(tokenizer);
         const fields = line.split(/\s+/g);
         type_symbol[i] = fields[0];
         x[i] = +fields[1];

+ 24 - 16
src/mol-math/geometry/spacegroup/construction.ts

@@ -111,33 +111,41 @@ namespace Spacegroup {
 
     const _translationRef = Vec3();
     const _translationRefSymop = Vec3();
+    const _translationRefOffset = Vec3();
     const _translationSymop = Vec3();
-    export function setOperatorMatrixRef(spacegroup: Spacegroup, index: number, i: number, j: number, k: number, ref: Vec3, target: Mat4) {
+
+    /**
+     * Get Symmetry operator for transformation around the given
+     * reference point `ref` in fractional coordinates
+     */
+    export function getSymmetryOperatorRef(spacegroup: Spacegroup, spgrOp: number, i: number, j: number, k: number, ref: Vec3) {
+
+        const operator =  Mat4.zero();
+
         Vec3.set(_ijkVec, i, j, k);
         Vec3.floor(_translationRef, ref);
 
-        Mat4.copy(target, spacegroup.operators[index]);
+        Mat4.copy(operator, spacegroup.operators[spgrOp]);
 
-        Vec3.floor(_translationRefSymop, Vec3.transformMat4(_translationRefSymop, ref, target));
+        Vec3.floor(_translationRefSymop, Vec3.transformMat4(_translationRefSymop, ref, operator));
 
-        Mat4.getTranslation(_translationSymop, target);
+        Mat4.getTranslation(_translationSymop, operator);
         Vec3.sub(_translationSymop, _translationSymop, _translationRefSymop);
         Vec3.add(_translationSymop, _translationSymop, _translationRef);
         Vec3.add(_translationSymop, _translationSymop, _ijkVec);
 
-        Mat4.setTranslation(target, _translationSymop);
-        Mat4.mul(target, spacegroup.cell.fromFractional, target);
-        Mat4.mul(target, target, spacegroup.cell.toFractional);
-        return target;
-    }
+        Mat4.setTranslation(operator, _translationSymop);
+        Mat4.mul(operator, spacegroup.cell.fromFractional, operator);
+        Mat4.mul(operator, operator, spacegroup.cell.toFractional);
 
-    /**
-     * Get Symmetry operator for transformation around the given
-     * reference point `ref` in fractional coordinates
-     */
-    export function getSymmetryOperatorRef(spacegroup: Spacegroup, spgrOp: number, i: number, j: number, k: number, ref: Vec3) {
-        const operator = setOperatorMatrixRef(spacegroup, spgrOp, i, j, k, ref, Mat4.zero());
-        return SymmetryOperator.create(`${spgrOp + 1}_${5 + i}${5 + j}${5 + k}`, operator, { hkl: Vec3.create(i, j, k), spgrOp });
+        Vec3.sub(_translationRefOffset, _translationRefSymop, _translationRef);
+
+        const _i = i - _translationRefOffset[0];
+        const _j = j - _translationRefOffset[1];
+        const _k = k - _translationRefOffset[2];
+
+        // const operator = setOperatorMatrixRef(spacegroup, spgrOp, i, j, k, ref, Mat4.zero());
+        return SymmetryOperator.create(`${spgrOp + 1}_${5 + _i}${5 + _j}${5 + _k}`, operator, { hkl: Vec3.create(_i, _j, _k), spgrOp });
     }
 
     function getOperatorMatrix(ids: number[]) {

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

@@ -590,6 +590,9 @@ namespace Vec3 {
     export const unitX: ReadonlyVec3 = create(1, 0, 0);
     export const unitY: ReadonlyVec3 = create(0, 1, 0);
     export const unitZ: ReadonlyVec3 = create(0, 0, 1);
+    export const negUnitX: ReadonlyVec3 = create(-1, 0, 0);
+    export const negUnitY: ReadonlyVec3 = create(0, -1, 0);
+    export const negUnitZ: ReadonlyVec3 = create(0, 0, -1);
 }
 
 export { Vec3 };

+ 1 - 1
src/mol-model-props/common/custom-element-property.ts

@@ -61,7 +61,7 @@ namespace CustomElementProperty {
             type: builder.type || 'dynamic',
             defaultParams: {},
             getParams: (data: Model) => ({}),
-            isApplicable: (data: Model) => !!builder.isApplicable?.(data),
+            isApplicable: (data: Model) => !builder.isApplicable || !!builder.isApplicable(data),
             obtain: async (ctx: CustomProperty.Context, data: Model) => {
                 return await builder.getData(data, ctx);
             }

+ 4 - 3
src/mol-model/loci.ts

@@ -37,9 +37,10 @@ export interface DataLoci<T = unknown, E = unknown> {
     readonly kind: 'data-loci',
     readonly tag: string
     readonly data: T,
-    readonly elements: ReadonlyArray<E>
+    readonly elements: ReadonlyArray<E>,
 
-    getBoundingSphere(boundingSphere: Sphere3D): Sphere3D
+    /** if undefined, won't zoom */
+    getBoundingSphere?(boundingSphere: Sphere3D): Sphere3D
     getLabel(): string
 }
 export function isDataLoci(x?: Loci): x is DataLoci {
@@ -159,7 +160,7 @@ namespace Loci {
         } else if (loci.kind === 'group-loci') {
             return ShapeGroup.getBoundingSphere(loci, boundingSphere);
         } else if (loci.kind === 'data-loci') {
-            return loci.getBoundingSphere(boundingSphere);
+            return loci.getBoundingSphere?.(boundingSphere);
         } else if (loci.kind === 'volume-loci') {
             return Volume.getBoundingSphere(loci.volume, boundingSphere);
         } else if (loci.kind === 'isosurface-loci') {

+ 107 - 0
src/mol-model/structure/model/properties/utils/residue-set.ts

@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureElement } from '../../../structure/element';
+import { StructureProperties } from '../../../structure/properties';
+
+export interface ResidueSetEntry {
+    label_asym_id: string,
+    label_comp_id: string,
+    label_seq_id: number,
+    label_alt_id: string,
+    ins_code: string,
+    // 1_555 by default
+    operator_name?: string
+}
+
+export class ResidueSet {
+    private index = new Map<string, Map<number, ResidueSetEntry[]>>();
+    private checkOperator: boolean = false;
+
+    add(entry: ResidueSetEntry) {
+        let root = this.index.get(entry.label_asym_id);
+        if (!root) {
+            root = new Map();
+            this.index.set(entry.label_asym_id, root);
+        }
+
+        let entries = root.get(entry.label_seq_id);
+        if (!entries) {
+            entries = [];
+            root.set(entry.label_seq_id, entries);
+        }
+
+        const exists = this._find(entry, entries);
+        if (!exists) {
+            entries.push(entry);
+            return true;
+        }
+
+        return false;
+    }
+
+    hasLabelAsymId(asym_id: string) {
+        return this.index.has(asym_id);
+    }
+
+    has(loc: StructureElement.Location) {
+        const asym_id = _asym_id(loc);
+        if (!this.index.has(asym_id)) return;
+        const root = this.index.get(asym_id)!;
+        const seq_id = _seq_id(loc);
+        if (!root.has(seq_id)) return;
+        const entries = root.get(seq_id)!;
+
+        const comp_id = _comp_id(loc);
+        const alt_id = _alt_id(loc);
+        const ins_code = _ins_code(loc);
+        const op_name = _op_name(loc) ?? '1_555';
+
+        for (const e of entries) {
+            if (e.label_comp_id !== comp_id || e.label_alt_id !== alt_id || e.ins_code !== ins_code) continue;
+            if (this.checkOperator && (e.operator_name ?? '1_555') !== op_name) continue;
+
+            return e;
+        }
+    }
+
+    static getLabel(entry: ResidueSetEntry, checkOperator = false) {
+        return `${entry.label_asym_id} ${entry.label_comp_id} ${entry.label_seq_id}:${entry.ins_code}:${entry.label_alt_id}${checkOperator ? ' ' + (entry.operator_name ?? '1_555') : ''}`;
+    }
+
+    static getEntryFromLocation(loc: StructureElement.Location): ResidueSetEntry {
+        return {
+            label_asym_id: _asym_id(loc),
+            label_comp_id: _comp_id(loc),
+            label_seq_id: _seq_id(loc),
+            label_alt_id: _alt_id(loc),
+            ins_code: _ins_code(loc),
+            operator_name: _op_name(loc) ?? '1_555'
+        };
+    }
+
+    private _find(entry: ResidueSetEntry, xs: ResidueSetEntry[]) {
+        for (const e of xs) {
+            if (e.label_comp_id !== entry.label_comp_id || e.label_alt_id !== entry.label_alt_id || e.ins_code !== entry.ins_code) continue;
+            if (this.checkOperator && (e.operator_name ?? '1_555') !== (entry.operator_name ?? '1_555')) continue;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    constructor(options?: { checkOperator?: boolean }) {
+        this.checkOperator = options?.checkOperator ?? false;
+    }
+}
+
+const _asym_id = StructureProperties.chain.label_asym_id;
+const _seq_id = StructureProperties.residue.label_seq_id;
+const _comp_id = StructureProperties.atom.label_comp_id;
+const _alt_id = StructureProperties.atom.label_alt_id;
+const _ins_code = StructureProperties.residue.pdbx_PDB_ins_code;
+const _op_name = StructureProperties.unit.operator_name;

+ 253 - 1
src/mol-model/structure/query/queries/modifiers.ts

@@ -11,10 +11,14 @@ import { StructureSelection } from '../selection';
 import { UniqueStructuresBuilder } from '../utils/builders';
 import { StructureUniqueSubsetBuilder } from '../../structure/util/unique-subset-builder';
 import { QueryContext, QueryFn } from '../context';
-import { structureIntersect, structureSubtract } from '../utils/structure-set';
+import { structureIntersect, structureSubtract, structureUnion } from '../utils/structure-set';
 import { UniqueArray } from '../../../../mol-data/generic';
 import { StructureSubsetBuilder } from '../../structure/util/subset-builder';
 import { StructureElement } from '../../structure/element';
+import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
+import { ResidueSet, ResidueSetEntry } from '../../model/properties/utils/residue-set';
+import { StructureProperties } from '../../structure/properties';
+import { arraySetAdd } from '../../../../mol-util/array';
 
 function getWholeResidues(ctx: QueryContext, source: Structure, structure: Structure) {
     const builder = source.subsetBuilder(true);
@@ -435,4 +439,252 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
     return builder.getStructure();
 }
 
+export interface SurroundingLigandsParams {
+    query: StructureQuery,
+    radius: number,
+    includeWater: boolean
+}
+
+/**
+ * Includes expanded surrounding ligands based on radius from the source, struct_conn entries & pdbx_molecule entries.
+ */
+export function surroundingLigands({ query, radius, includeWater }: SurroundingLigandsParams): StructureQuery {
+    return function query_surroundingLigands(ctx) {
+
+        const inner = StructureSelection.unionStructure(query(ctx));
+        const surroundings = getWholeResidues(ctx, ctx.inputStructure, getIncludeSurroundings(ctx, ctx.inputStructure, inner, { radius }));
+
+        const prd = getPrdAsymIdx(ctx.inputStructure);
+        const graph = getStructConnInfo(ctx.inputStructure);
+
+        const l = StructureElement.Location.create(surroundings);
+
+        const includedPrdChains = new Map<string, string[]>();
+
+        const componentResidues = new ResidueSet({ checkOperator: true });
+
+        for (const unit of surroundings.units) {
+            if (unit.kind !== Unit.Kind.Atomic) continue;
+
+            l.unit = unit;
+
+            const { elements } = unit;
+            const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
+            const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements);
+
+            while (chainsIt.hasNext) {
+                const chainSegment = chainsIt.move();
+                l.element = elements[chainSegment.start];
+
+                const asym_id = StructureProperties.chain.label_asym_id(l);
+                const op_name = StructureProperties.unit.operator_name(l);
+
+                // check for PRD molecules
+                if (prd.has(asym_id)) {
+                    if (includedPrdChains.has(asym_id)) {
+                        arraySetAdd(includedPrdChains.get(asym_id)!, op_name);
+                    } else {
+                        includedPrdChains.set(asym_id, [op_name]);
+                    }
+                    continue;
+                }
+
+                const entityType = StructureProperties.entity.type(l);
+
+                // test entity and chain
+                if (entityType === 'water' || entityType === 'polymer') continue;
+
+                residuesIt.setSegment(chainSegment);
+                while (residuesIt.hasNext) {
+                    const residueSegment = residuesIt.move();
+                    l.element = elements[residueSegment.start];
+                    graph.addComponent(ResidueSet.getEntryFromLocation(l), componentResidues);
+                }
+            }
+
+            ctx.throwIfTimedOut();
+        }
+
+        // assemble the core structure
+
+        const builder = ctx.inputStructure.subsetBuilder(true);
+        for (const unit of ctx.inputStructure.units) {
+            if (unit.kind !== Unit.Kind.Atomic) continue;
+
+            l.unit = unit;
+            const { elements } = unit;
+            const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
+            const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements);
+
+            builder.beginUnit(unit.id);
+
+            while (chainsIt.hasNext) {
+                const chainSegment = chainsIt.move();
+                l.element = elements[chainSegment.start];
+
+                const asym_id = StructureProperties.chain.label_asym_id(l);
+                const op_name = StructureProperties.unit.operator_name(l);
+
+                if (includedPrdChains.has(asym_id) && includedPrdChains.get(asym_id)!.indexOf(op_name) >= 0) {
+                    builder.addElementRange(elements, chainSegment.start, chainSegment.end);
+                    continue;
+                }
+
+                if (!componentResidues.hasLabelAsymId(asym_id)) {
+                    continue;
+                }
+
+                residuesIt.setSegment(chainSegment);
+                while (residuesIt.hasNext) {
+                    const residueSegment = residuesIt.move();
+                    l.element = elements[residueSegment.start];
+
+                    if (!componentResidues.has(l)) continue;
+                    builder.addElementRange(elements, residueSegment.start, residueSegment.end);
+                }
+            }
+            builder.commitUnit();
+
+            ctx.throwIfTimedOut();
+        }
+
+        const components = structureUnion(ctx.inputStructure, [builder.getStructure(), inner]);
+
+        // add water
+        if (includeWater) {
+            const finalBuilder = new StructureUniqueSubsetBuilder(ctx.inputStructure);
+            const lookup = ctx.inputStructure.lookup3d;
+            for (const unit of components.units) {
+                const { x, y, z } = unit.conformation;
+                const elements = unit.elements;
+                for (let i = 0, _i = elements.length; i < _i; i++) {
+                    const e = elements[i];
+                    lookup.findIntoBuilderIf(x(e), y(e), z(e), radius, finalBuilder, testIsWater);
+                    finalBuilder.addToUnit(unit.id, e);
+                }
+
+                ctx.throwIfTimedOut();
+            }
+
+            return StructureSelection.Sequence(ctx.inputStructure, [finalBuilder.getStructure()]);
+        } else {
+            return StructureSelection.Sequence(ctx.inputStructure, [components]);
+        }
+    };
+}
+
+const _entity_type = StructureProperties.entity.type;
+function testIsWater(l: StructureElement.Location) {
+    return _entity_type(l) === 'water';
+}
+
+function getPrdAsymIdx(structure: Structure) {
+    const model = structure.models[0];
+    const ids = new Set<string>();
+    if (!MmcifFormat.is(model.sourceData)) return ids;
+    const { _rowCount, asym_id } = model.sourceData.data.db.pdbx_molecule;
+    for (let i = 0; i < _rowCount; i++) {
+        ids.add(asym_id.value(i));
+    }
+    return ids;
+}
+
+function getStructConnInfo(structure: Structure) {
+    const model = structure.models[0];
+    const graph = new StructConnGraph();
+
+    if (!MmcifFormat.is(model.sourceData)) return graph;
+
+    const struct_conn = model.sourceData.data.db.struct_conn;
+    const { conn_type_id } = struct_conn;
+    const { ptnr1_label_asym_id, ptnr1_label_comp_id, ptnr1_label_seq_id, ptnr1_symmetry, pdbx_ptnr1_label_alt_id, pdbx_ptnr1_PDB_ins_code } = struct_conn;
+    const { ptnr2_label_asym_id, ptnr2_label_comp_id, ptnr2_label_seq_id, ptnr2_symmetry, pdbx_ptnr2_label_alt_id, pdbx_ptnr2_PDB_ins_code } = struct_conn;
+
+    for (let i = 0; i < struct_conn._rowCount; i++) {
+        const bondType = conn_type_id.value(i);
+        if (bondType !== 'covale' && bondType !== 'metalc') continue;
+
+        const a: ResidueSetEntry = {
+            label_asym_id: ptnr1_label_asym_id.value(i),
+            label_comp_id: ptnr1_label_comp_id.value(i),
+            label_seq_id: ptnr1_label_seq_id.value(i),
+            label_alt_id: pdbx_ptnr1_label_alt_id.value(i),
+            ins_code: pdbx_ptnr1_PDB_ins_code.value(i),
+            operator_name: ptnr1_symmetry.value(i) ?? '1_555'
+        };
+
+        const b: ResidueSetEntry = {
+            label_asym_id: ptnr2_label_asym_id.value(i),
+            label_comp_id: ptnr2_label_comp_id.value(i),
+            label_seq_id: ptnr2_label_seq_id.value(i),
+            label_alt_id: pdbx_ptnr2_label_alt_id.value(i),
+            ins_code: pdbx_ptnr2_PDB_ins_code.value(i),
+            operator_name: ptnr2_symmetry.value(i) ?? '1_555'
+        };
+
+        graph.addEdge(a, b);
+    }
+
+    return graph;
+}
+
+class StructConnGraph {
+    vertices = new Map<string, ResidueSetEntry>();
+    edges = new Map<string, string[]>();
+
+    private addVertex(e: ResidueSetEntry, label: string) {
+        if (this.vertices.has(label)) return;
+        this.vertices.set(label, e);
+        this.edges.set(label, []);
+    }
+
+    addEdge(a: ResidueSetEntry, b: ResidueSetEntry) {
+        const al = ResidueSet.getLabel(a);
+        const bl = ResidueSet.getLabel(b);
+        this.addVertex(a, al);
+        this.addVertex(b, bl);
+        arraySetAdd(this.edges.get(al)!, bl);
+        arraySetAdd(this.edges.get(bl)!, al);
+    }
+
+    addComponent(start: ResidueSetEntry, set: ResidueSet) {
+        const startLabel = ResidueSet.getLabel(start);
+
+        if (!this.vertices.has(startLabel)) {
+            set.add(start);
+            return;
+        }
+
+        const visited = new Set<string>();
+        const added = new Set<string>();
+        const stack = [startLabel];
+
+        added.add(startLabel);
+        set.add(start);
+
+        while (stack.length > 0) {
+            const a = stack.pop()!;
+            visited.add(a);
+
+            const u = this.vertices.get(a)!;
+
+            for (const b of this.edges.get(a)!) {
+                if (visited.has(b)) continue;
+                stack.push(b);
+
+                if (added.has(b)) continue;
+                added.add(b);
+
+                const v = this.vertices.get(b)!;
+                if (u.operator_name === v.operator_name) {
+                    set.add({ ...v, operator_name: start.operator_name });
+                } else {
+                    set.add(v);
+                }
+
+            }
+        }
+    }
+}
+
 // TODO: unionBy (skip this one?), cluster

+ 4 - 1
src/mol-model/structure/structure/element/loci.ts

@@ -23,6 +23,9 @@ import { StructureProperties } from '../properties';
 import { BoundaryHelper } from '../../../../mol-math/geometry/boundary-helper';
 import { Boundary } from '../../../../mol-math/geometry/boundary';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const osSize = OrderedSet.size;
+
 /** Represents multiple structure element index locations */
 export interface Loci {
     readonly kind: 'element-loci',
@@ -71,7 +74,7 @@ export namespace Loci {
 
     export function size(loci: Loci) {
         let s = 0;
-        for (const u of loci.elements) s += OrderedSet.size(u.indices);
+        for (const u of loci.elements) s += osSize(u.indices);
         return s;
     }
 

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

@@ -98,12 +98,12 @@ const residue = {
     microheterogeneityCompIds: p(microheterogeneityCompIds),
     secondary_structure_type: p(l => {
         if (!Unit.isAtomic(l.unit)) notAtomic();
-        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id);
+        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.invariantId);
         return secStruc?.type[l.unit.residueIndex[l.element]] ?? SecondaryStructureType.Flag.NA;
     }),
     secondary_structure_key: p(l => {
         if (!Unit.isAtomic(l.unit)) notAtomic();
-        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id);
+        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.invariantId);
         return secStruc?.key[l.unit.residueIndex[l.element]] ?? -1;
     }),
     chem_comp_type: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.chemicalComponentMap.get(compId(l))!.type),

+ 20 - 26
src/mol-model/structure/structure/structure.ts

@@ -23,7 +23,7 @@ import { Carbohydrates } from './carbohydrates/data';
 import { computeCarbohydrates } from './carbohydrates/compute';
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { idFactory } from '../../../mol-util/id-factory';
-import { Box3D, GridLookup3D } from '../../../mol-math/geometry';
+import { GridLookup3D } from '../../../mol-math/geometry';
 import { UUID } from '../../../mol-util';
 import { CustomProperties } from '../../custom-property';
 import { AtomicHierarchy } from '../model/properties/atomic';
@@ -61,6 +61,7 @@ type State = {
     uniqueElementCount: number,
     atomicResidueCount: number,
     polymerResidueCount: number,
+    polymerGapCount: number,
     polymerUnitCount: number,
     coordinateSystem: SymmetryOperator,
     label: string,
@@ -118,6 +119,14 @@ class Structure {
         return this.state.polymerResidueCount;
     }
 
+    /** Count of all polymer gaps in the structure */
+    get polymerGapCount() {
+        if (this.state.polymerGapCount === -1) {
+            this.state.polymerGapCount = getPolymerGapCount(this);
+        }
+        return this.state.polymerGapCount;
+    }
+
     get polymerUnitCount() {
         if (this.state.polymerUnitCount === -1) {
             this.state.polymerUnitCount = getPolymerUnitCount(this);
@@ -238,13 +247,6 @@ class Structure {
         return this.state.unitSymmetryGroupsIndexMap;
     }
 
-    /** Array of all units in the structure, sorted by their boundary volume */
-    get unitsSortedByVolume(): ReadonlyArray<Unit> {
-        if (this.state.unitsSortedByVolume) return this.state.unitsSortedByVolume;
-        this.state.unitsSortedByVolume = getUnitsSortedByVolume(this);
-        return this.state.unitsSortedByVolume;
-    }
-
     get carbohydrates(): Carbohydrates {
         if (this.state.carbohydrates) return this.state.carbohydrates;
         this.state.carbohydrates = computeCarbohydrates(this);
@@ -399,24 +401,6 @@ function cmpUnits(units: ArrayLike<Unit>, i: number, j: number) {
     return units[i].id - units[j].id;
 
 }
-function cmpUnitGroupVolume(units: ArrayLike<[index: number, volume: number]>, i: number, j: number) {
-    const d = units[i][1] - units[j][1];
-    if (d === 0) return units[i][0] - units[j][0];
-    return d;
-}
-
-function getUnitsSortedByVolume(structure: Structure) {
-    const { unitSymmetryGroups } = structure;
-    const groups = unitSymmetryGroups.map((g, i) => [i, Box3D.volume(g.units[0].lookup3d.boundary.box)] as [number, number]);
-    sort(groups, 0, groups.length, cmpUnitGroupVolume, arraySwap);
-    const ret: Unit[] = [];
-    for (const [i] of groups) {
-        for (const u of unitSymmetryGroups[i].units) {
-            ret.push(u);
-        }
-    }
-    return ret;
-}
 
 function getModels(s: Structure) {
     const { units } = s;
@@ -538,6 +522,15 @@ function getPolymerResidueCount(structure: Structure): number {
     return polymerResidueCount;
 }
 
+function getPolymerGapCount(structure: Structure): number {
+    const { units } = structure;
+    let polymerGapCount = 0;
+    for (let i = 0, _i = units.length; i < _i; i++) {
+        polymerGapCount += units[i].gapElements.length / 2;
+    }
+    return polymerGapCount;
+}
+
 function getPolymerUnitCount(structure: Structure): number {
     const { units } = structure;
     let polymerUnitCount = 0;
@@ -684,6 +677,7 @@ namespace Structure {
             uniqueElementCount: -1,
             atomicResidueCount: -1,
             polymerResidueCount: -1,
+            polymerGapCount: -1,
             polymerUnitCount: -1,
             coordinateSystem: SymmetryOperator.Default,
             label: ''

+ 30 - 0
src/mol-model/structure/structure/util/lookup3d.ts

@@ -94,6 +94,36 @@ export class StructureLookup3D {
         }
     }
 
+    findIntoBuilderIf(x: number, y: number, z: number, radius: number, builder: StructureUniqueSubsetBuilder, test: (l: StructureElement.Location) => boolean) {
+        const { units } = this.structure;
+        const closeUnits = this.unitLookup.find(x, y, z, radius);
+        if (closeUnits.count === 0) return;
+
+        const loc = StructureElement.Location.create(this.structure);
+
+        for (let t = 0, _t = closeUnits.count; t < _t; t++) {
+            const unit = units[closeUnits.indices[t]];
+            Vec3.set(this.pivot, x, y, z);
+            if (!unit.conformation.operator.isIdentity) {
+                Vec3.transformMat4(this.pivot, this.pivot, unit.conformation.operator.inverse);
+            }
+            const unitLookup = unit.lookup3d;
+            const groupResult = unitLookup.find(this.pivot[0], this.pivot[1], this.pivot[2], radius);
+            if (groupResult.count === 0) continue;
+
+            const elements = unit.elements;
+            loc.unit = unit;
+            builder.beginUnit(unit.id);
+            for (let j = 0, _j = groupResult.count; j < _j; j++) {
+                loc.element = elements[groupResult.indices[j]];
+                if (test(loc)) {
+                    builder.addElement(loc.element);
+                }
+            }
+            builder.commitUnit();
+        }
+    }
+
     findIntoBuilderWithRadius(x: number, y: number, z: number, pivotR: number, maxRadius: number, radius: number, eRadius: StructureElement.Property<number>, builder: StructureUniqueSubsetBuilder) {
         const { units } = this.structure;
         const closeUnits = this.unitLookup.find(x, y, z, radius);

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

@@ -23,6 +23,7 @@ import { OperatorNameColorThemeProvider } from '../../../mol-theme/color/operato
 import { IndexPairBonds } from '../../../mol-model-formats/structure/property/bonds/index-pair';
 import { StructConn } from '../../../mol-model-formats/structure/property/bonds/struct_conn';
 import { StructureRepresentationRegistry } from '../../../mol-repr/structure/registry';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
 
 export interface StructureRepresentationPresetProvider<P = any, S extends _Result = _Result> extends PresetProvider<PluginStateObject.Molecule.Structure, P, S> { }
 export function StructureRepresentationPresetProvider<P, S extends _Result>(repr: StructureRepresentationPresetProvider<P, S>) { return repr; }
@@ -111,6 +112,8 @@ const auto = StructureRepresentationPresetProvider({
         const thresholds = plugin.config.get(PluginConfig.Structure.SizeThresholds) || Structure.DefaultSizeThresholds;
         const size = Structure.getSize(structure, thresholds);
 
+        const gapFraction = structure.polymerResidueCount / structure.polymerGapCount;
+
         switch (size) {
             case Structure.Size.Gigantic:
             case Structure.Size.Huge:
@@ -118,10 +121,14 @@ const auto = StructureRepresentationPresetProvider({
             case Structure.Size.Large:
                 return polymerCartoon.apply(ref, params, plugin);
             case Structure.Size.Medium:
-                return polymerAndLigand.apply(ref, params, plugin);
+                if (gapFraction > 3) {
+                    return polymerAndLigand.apply(ref, params, plugin);
+                } // else fall through
             case Structure.Size.Small:
-                // `showCarbohydrateSymbol: true` is nice e.g. for PDB 1aga
+                // `showCarbohydrateSymbol: true` is nice, e.g., for PDB 1aga
                 return atomicDetail.apply(ref, { ...params, showCarbohydrateSymbol: true }, plugin);
+            default:
+                assertUnreachable(size);
         }
     }
 });

+ 13 - 0
src/mol-plugin-state/helpers/structure-selection-query.ts

@@ -425,6 +425,18 @@ const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of
     referencesCurrent: true
 });
 
+const surroundingLigands = StructureSelectionQuery('Surrounding Ligands (5 \u212B) of Selection', MS.struct.modifier.union([
+    MS.struct.modifier.surroundingLigands({
+        0: MS.internal.generator.current(),
+        radius: 5,
+        'include-water': true
+    })
+]), {
+    description: 'Select ligand components within 5 \u212B of the current selection.',
+    category: StructureSelectionCategory.Manipulate,
+    referencesCurrent: true
+});
+
 const complement = StructureSelectionQuery('Inverse / Complement of Selection', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
         0: MS.struct.generator.all(),
@@ -645,6 +657,7 @@ export const StructureSelectionQueries = {
     ring,
     aromaticRing,
     surroundings,
+    surroundingLigands,
     complement,
     covalentlyBonded,
     covalentlyOrMetallicBonded,

+ 3 - 1
src/mol-plugin-state/transforms.ts

@@ -9,13 +9,15 @@ import * as Misc from './transforms/misc';
 import * as Model from './transforms/model';
 import * as Volume from './transforms/volume';
 import * as Representation from './transforms/representation';
+import * as Shape from './transforms/shape';
 
 export const StateTransforms = {
     Data,
     Misc,
     Model,
     Volume,
-    Representation
+    Representation,
+    Shape
 };
 
 export type StateTransforms = typeof StateTransforms

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

@@ -36,6 +36,10 @@ import { DihedralParams, DihedralRepresentation } from '../../mol-repr/shape/loc
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
 import { Clipping } from '../../mol-theme/clipping';
 import { ObjectKeys } from '../../mol-util/type-helpers';
+import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
+import { getBoxMesh } from './shape';
+import { Shape } from '../../mol-model/shape';
+import { Box3D } from '../../mol-math/geometry';
 
 export { StructureRepresentation3D };
 export { ExplodeStructureRepresentation3D };
@@ -741,6 +745,41 @@ const ModelUnitcell3D = PluginStateTransform.BuiltIn({
     }
 });
 
+export { StructureBoundingBox3D };
+type StructureBoundingBox3D = typeof StructureBoundingBox3D
+const StructureBoundingBox3D = PluginStateTransform.BuiltIn({
+    name: 'structure-bounding-box-3d',
+    display: 'Bounding Box',
+    from: SO.Molecule.Structure,
+    to: SO.Shape.Representation3D,
+    params: {
+        radius: PD.Numeric(0.05, { min: 0.01, max: 4, step: 0.01 }, { isEssential: true }),
+        color: PD.Color(ColorNames.red, { isEssential: true }),
+        ...Mesh.Params,
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Bounding Box', async ctx => {
+            const repr = ShapeRepresentation((_, data: { box: Box3D, radius: number, color: Color }, __, shape) => {
+                const mesh = getBoxMesh(data.box, data.radius, shape?.geometry);
+                return Shape.create('Bouding Box', data, mesh, () => data.color, () => 1, () => 'Bounding Box');
+            }, Mesh.Utils);
+            await repr.createOrUpdate(params, { box: a.data.boundary.box, radius: params.radius, color: params.color }).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, sourceData: a.data }, { label: `Bounding Box` });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Bounding Box', async ctx => {
+            await b.data.repr.createOrUpdate(newParams, { box: a.data.boundary.box, radius: newParams.radius, color: newParams.color }).runInContext(ctx);
+            b.data.sourceData = a.data;
+            return StateTransformer.UpdateResult.Updated;
+        });
+    }
+});
+
 export { StructureSelectionsDistance3D };
 type StructureSelectionsDistance3D = typeof StructureSelectionsDistance3D
 const StructureSelectionsDistance3D = PluginStateTransform.BuiltIn({

+ 69 - 0
src/mol-plugin-state/transforms/shape.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
+import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
+import { BoxCage } from '../../mol-geo/primitive/box';
+import { Box3D, Sphere3D } from '../../mol-math/geometry';
+import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
+import { Shape } from '../../mol-model/shape';
+import { Task } from '../../mol-task';
+import { ColorNames } from '../../mol-util/color/names';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+
+export { BoxShape3D };
+type BoxShape3D = typeof BoxShape3D
+const BoxShape3D = PluginStateTransform.BuiltIn({
+    name: 'box-shape-3d',
+    display: 'Box Shape',
+    from: SO.Root,
+    to: SO.Shape.Provider,
+    params: {
+        bottomLeft: PD.Vec3(Vec3()),
+        topRight: PD.Vec3(Vec3.create(1, 1, 1)),
+        radius: PD.Numeric(0.15, { min: 0.01, max: 4, step: 0.01 }),
+        color: PD.Color(ColorNames.red)
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ params }) {
+        return Task.create('Shape Representation', async ctx => {
+            return new SO.Shape.Provider({
+                label: 'Box',
+                data: params,
+                params: Mesh.Params,
+                getShape: (_, data: typeof params) => {
+                    const mesh = getBoxMesh(Box3D.create(params.bottomLeft, params.topRight), params.radius);
+                    return Shape.create('Box', data, mesh, () => data.color, () => 1, () => 'Box');
+                },
+                geometryUtils: Mesh.Utils
+            }, { label: 'Box' });
+        });
+    }
+});
+
+export function getBoxMesh(box: Box3D, radius: number, oldMesh?: Mesh) {
+    const diag = Vec3.sub(Vec3(), box.max, box.min);
+    const translateUnit = Mat4.fromTranslation(Mat4(), Vec3.create(0.5, 0.5, 0.5));
+    const scale = Mat4.fromScaling(Mat4(), diag);
+    const translate = Mat4.fromTranslation(Mat4(), box.min);
+    const transform = Mat4.mul3(Mat4(), translate, scale, translateUnit);
+
+    // TODO: optimize?
+    const state = MeshBuilder.createState(256, 128, oldMesh);
+    state.currentGroup = 1;
+    MeshBuilder.addCage(state, transform, BoxCage(), radius, 2, 20);
+    const mesh = MeshBuilder.getMesh(state);
+
+    const center = Vec3.scaleAndAdd(Vec3(), box.min, diag, 0.5);
+    const sphereRadius = Vec3.distance(box.min, center);
+    mesh.setBoundingSphere(Sphere3D.create(center, sphereRadius));
+
+    return mesh;
+}

+ 4 - 6
src/mol-plugin-ui/sequence/polymer.ts

@@ -122,12 +122,11 @@ function applyMarkerAtomic(e: StructureElement.Loci.Element, action: MarkerActio
     const { index: residueIndex } = model.atomicHierarchy.residueAtomSegments;
     const { label_seq_id } = model.atomicHierarchy.residues;
 
-    let changed = false;
     OrderedSet.forEachSegment(e.indices, i => residueIndex[elements[i]], rI => {
         const seqId = label_seq_id.value(rI);
-        changed = applyMarkerActionAtPosition(markerArray, index(seqId), action) || changed;
+        applyMarkerActionAtPosition(markerArray, index(seqId), action);
     });
-    return changed;
+    return true;
 }
 
 function applyMarkerCoarse(e: StructureElement.Loci.Element, action: MarkerAction, markerArray: Uint8Array, index: (seqId: number) => number) {
@@ -135,12 +134,11 @@ function applyMarkerCoarse(e: StructureElement.Loci.Element, action: MarkerActio
     const begin = Unit.isSpheres(e.unit) ? model.coarseHierarchy.spheres.seq_id_begin : model.coarseHierarchy.gaussians.seq_id_begin;
     const end = Unit.isSpheres(e.unit) ? model.coarseHierarchy.spheres.seq_id_end : model.coarseHierarchy.gaussians.seq_id_end;
 
-    let changed = false;
     OrderedSet.forEach(e.indices, i => {
         const eI = elements[i];
         for (let s = index(begin.value(eI)), e = index(end.value(eI)); s <= e; s++) {
-            changed = applyMarkerActionAtPosition(markerArray, s, action) || changed;
+            applyMarkerActionAtPosition(markerArray, s, action);
         }
     });
-    return changed;
+    return true;
 }

+ 5 - 0
src/mol-plugin-ui/spec.ts

@@ -7,8 +7,10 @@
 
 
 import { StateTransformParameters } from '../mol-plugin-ui/state/common';
+import { CreateVolumeStreamingBehavior } from '../mol-plugin/behavior/dynamic/volume-streaming/transformers';
 import { DefaultPluginSpec, PluginSpec } from '../mol-plugin/spec';
 import { StateAction, StateTransformer } from '../mol-state';
+import { VolumeStreamingCustomControls } from './custom/volume';
 
 export { PluginUISpec };
 
@@ -37,4 +39,7 @@ namespace PluginUISpec {
 
 export const DefaultPluginUISpec = (): PluginUISpec => ({
     ...DefaultPluginSpec(),
+    customParamEditors: [
+        [CreateVolumeStreamingBehavior, VolumeStreamingCustomControls]
+    ],
 });

+ 17 - 2
src/mol-plugin-ui/structure/source.tsx

@@ -195,12 +195,27 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
     get presetActions() {
         const actions: ActionMenu.Item[] = [];
         const { trajectories } = this.plugin.managers.structure.hierarchy.selection;
-        if (trajectories.length !== 1) return actions;
+        if (trajectories.length === 0) return actions;
+
+        let providers = this.plugin.builders.structure.hierarchy.getPresets(trajectories[0].cell.obj);
+
+        if (trajectories.length > 1) {
+            const providerSet = new Set(providers);
+            for (let i = 1; i < trajectories.length; i++) {
+                const providers = this.plugin.builders.structure.hierarchy.getPresets(trajectories[i].cell.obj);
+                const current = new Set(providers);
+
+                for (const p of providers) {
+                    if (!current.has(p)) providerSet.delete(p);
+                }
+            }
+            providers = providers.filter(p => providerSet.has(p));
+        }
 
-        const providers = this.plugin.builders.structure.hierarchy.getPresets(trajectories[0].cell.obj);
         for (const p of providers) {
             actions.push(ActionMenu.Item(p.display.name, p, { description: p.display.description }));
         }
+
         return actions;
     }
 

+ 66 - 1
src/mol-plugin/behavior/dynamic/camera.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 David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -11,6 +11,8 @@ import { PluginBehavior } from '../behavior';
 import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
 import { Binding } from '../../../mol-util/binding';
 import { PluginCommands } from '../../commands';
+import { CameraHelperAxis, isCameraAxesLoci } from '../../../mol-canvas3d/helper/camera-helper';
+import { Vec3 } from '../../../mol-math/linear-algebra';
 
 const B = ButtonsType;
 const M = ModifiersKeys;
@@ -62,4 +64,67 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
     },
     params: () => FocusLociParams,
     display: { name: 'Camera Focus Loci on Canvas' }
+});
+
+export const CameraAxisHelper = PluginBehavior.create<{}>({
+    name: 'camera-axis-helper',
+    category: 'interaction',
+    ctor: class extends PluginBehavior.Handler<{}> {
+        register(): void {
+
+            let lastPlane = CameraHelperAxis.None;
+            let state = 0;
+
+            this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current }) => {
+                if (!this.ctx.canvas3d || !isCameraAxesLoci(current.loci)) return;
+
+                const axis = current.loci.elements[0].groupId;
+                if (axis === CameraHelperAxis.None) {
+                    lastPlane = CameraHelperAxis.None;
+                    state = 0;
+                    return;
+                }
+
+                const { camera } = this.ctx.canvas3d;
+                let dir: Vec3, up: Vec3;
+
+                if (axis >= CameraHelperAxis.X && axis <= CameraHelperAxis.Z) {
+                    lastPlane = CameraHelperAxis.None;
+                    state = 0;
+
+                    const d = Vec3.sub(Vec3(), camera.target, camera.position);
+                    const c = Vec3.cross(Vec3(), d, camera.up);
+
+                    up = Vec3();
+                    up[axis - 1] = 1;
+                    dir = Vec3.cross(Vec3(), up, c);
+                    if (Vec3.magnitude(dir) === 0) dir = d;
+                } else {
+                    if (lastPlane === axis) {
+                        state = (state + 1) % 2;
+                    } else {
+                        lastPlane = axis;
+                        state = 0;
+                    }
+
+                    if (axis === CameraHelperAxis.XY) {
+                        up = state ? Vec3.unitX : Vec3.unitY;
+                        dir = Vec3.negUnitZ;
+                    } else if (axis === CameraHelperAxis.XZ) {
+                        up = state ? Vec3.unitX : Vec3.unitZ;
+                        dir = Vec3.negUnitY;
+                    } else {
+                        up = state ? Vec3.unitY : Vec3.unitZ;
+                        dir = Vec3.negUnitX;
+                    }
+                }
+
+                this.ctx.canvas3d.requestCameraReset({
+                    snapshot: (scene, camera) => camera.getInvariantFocus(scene.boundingSphereVisible.center, scene.boundingSphereVisible.radius, up, dir)
+                });
+            });
+        }
+    },
+    params: () => ({}),
+    display: { name: 'Camera Axis Helper' }
 });

+ 2 - 0
src/mol-plugin/spec.ts

@@ -93,6 +93,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsLabel3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsOrientation3D),
         PluginSpec.Action(StateTransforms.Representation.ModelUnitcell3D),
+        PluginSpec.Action(StateTransforms.Representation.StructureBoundingBox3D),
         PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3DFromScript),
@@ -111,6 +112,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Representation.FocusLoci),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
+        PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
         PluginSpec.Behavior(StructureFocusRepresentation),
 
         PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo),

+ 6 - 0
src/mol-script/language/symbol-table/structure-query.ts

@@ -155,6 +155,12 @@ const modifier = {
         'as-whole-residues': Argument(Type.Bool, { isOptional: true })
     }), Types.ElementSelectionQuery, 'For each atom set in the selection, include all surrouding atoms/residues that are within the specified radius.'),
 
+    surroundingLigands: symbol(Arguments.Dictionary({
+        0: Argument(Types.ElementSelectionQuery),
+        radius: Argument(Type.Num),
+        'include-water': Argument(Type.Bool, { isOptional: true, defaultValue: true })
+    }), Types.ElementSelectionQuery, 'Find all ligands components around the source query.'),
+
     includeConnected: symbol(Arguments.Dictionary({
         0: Argument(Types.ElementSelectionQuery),
         'bond-test': Argument(Type.Bool, { isOptional: true, defaultValue: 'true for covalent bonds' as any }),

+ 7 - 0
src/mol-script/runtime/query/table.ts

@@ -258,6 +258,13 @@ const symbols = [
             elementRadius: xs['atom-radius']
         })(ctx);
     }),
+    D(MolScript.structureQuery.modifier.surroundingLigands, function structureQuery_modifier_includeSurroundingLigands(ctx, xs) {
+        return Queries.modifiers.surroundingLigands({
+            query: xs[0] as any,
+            radius: xs['radius'](ctx),
+            includeWater: !!(xs['include-water'] && xs['include-water'](ctx)),
+        })(ctx);
+    }),
     D(MolScript.structureQuery.modifier.wholeResidues, function structureQuery_modifier_wholeResidues(ctx, xs) { return Queries.modifiers.wholeResidues(xs[0] as any)(ctx); }),
     D(MolScript.structureQuery.modifier.union, function structureQuery_modifier_union(ctx, xs) { return Queries.modifiers.union(xs[0] as any)(ctx); }),
     D(MolScript.structureQuery.modifier.expandProperty, function structureQuery_modifier_expandProperty(ctx, xs) { return Queries.modifiers.expandProperty(xs[0] as any, xs['property'])(ctx); }),

+ 1 - 0
src/mol-script/script/mol-script/symbols.ts

@@ -138,6 +138,7 @@ export const SymbolTable = [
             Alias(MolScript.structureQuery.modifier.union, 'sel.atom.union'),
             Alias(MolScript.structureQuery.modifier.cluster, 'sel.atom.cluster'),
             Alias(MolScript.structureQuery.modifier.includeSurroundings, 'sel.atom.include-surroundings'),
+            Alias(MolScript.structureQuery.modifier.surroundingLigands, 'sel.atom.surrounding-ligands'),
             Alias(MolScript.structureQuery.modifier.includeConnected, 'sel.atom.include-connected'),
             Alias(MolScript.structureQuery.modifier.expandProperty, 'sel.atom.expand-property'),
 

+ 73 - 37
src/mol-util/marker-action.ts

@@ -1,11 +1,12 @@
 /**
- * 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>
  */
 
 import { OrderedSet, Interval } from '../mol-data/int';
 import { BitFlags } from './bit-flags';
+import { assertUnreachable } from './type-helpers';
 
 export enum MarkerAction {
     None = 0x0,
@@ -37,50 +38,85 @@ export namespace MarkerActions {
 }
 
 export function applyMarkerActionAtPosition(array: Uint8Array, i: number, action: MarkerAction) {
-    let v = array[i];
     switch (action) {
-        case MarkerAction.Highlight:
-            if (v % 2 === 0) {
-                array[i] = v + 1;
-                return true;
-            }
-            return false;
-        case MarkerAction.RemoveHighlight:
-            if (v % 2 !== 0) {
-                array[i] = v - 1;
-                return true;
-            }
-            return false;
-        case MarkerAction.Select:
-            if (v < 2) {
-                array[i] = v + 2;
-                return true;
-            }
-            return false;
-        case MarkerAction.Deselect:
-            array[i] = v % 2;
-            return array[i] !== v;
-        case MarkerAction.Toggle:
-            if (v >= 2) array[i] = v - 2;
-            else array[i] = v + 2;
-            return true;
-        case MarkerAction.Clear:
-            array[i] = 0;
-            return v !== 0;
+        case MarkerAction.Highlight: array[i] |= 1; break;
+        case MarkerAction.RemoveHighlight: array[i] &= ~1; break;
+        case MarkerAction.Select: array[i] |= 2; break;
+        case MarkerAction.Deselect: array[i] &= ~2; break;
+        case MarkerAction.Toggle: array[i] ^= 2; break;
+        case MarkerAction.Clear: array[i] = 0; break;
     }
-    return false;
 }
 
 export function applyMarkerAction(array: Uint8Array, set: OrderedSet, action: MarkerAction) {
-    let changed = false;
+    if (action === MarkerAction.None) return false;
+
     if (Interval.is(set)) {
-        for (let i = Interval.start(set), _i = Interval.end(set); i < _i; i++) {
-            changed = applyMarkerActionAtPosition(array, i, action) || changed;
+        const start = Interval.start(set);
+        const end = Interval.end(set);
+        const view = new Uint32Array(array.buffer, 0, array.buffer.byteLength >> 2);
+
+        const viewStart = (start + 3) >> 2;
+        const viewEnd = viewStart + ((end - 4 * viewStart) >> 2);
+
+        const frontStart = start;
+        const frontEnd =  Math.min(4 * viewStart, end);
+        const backStart = Math.max(start, 4 * viewEnd);
+        const backEnd = end;
+
+        switch (action) {
+            case MarkerAction.Highlight:
+                for (let i = viewStart; i < viewEnd; ++i) view[i] |= 0x01010101;
+                break;
+            case MarkerAction.RemoveHighlight:
+                for (let i = viewStart; i < viewEnd; ++i) view[i] &= ~0x01010101;
+                break;
+            case MarkerAction.Select:
+                for (let i = viewStart; i < viewEnd; ++i) view[i] |= 0x02020202;
+                break;
+            case MarkerAction.Deselect:
+                for (let i = viewStart; i < viewEnd; ++i) view[i] &= ~0x02020202;
+                break;
+            case MarkerAction.Toggle:
+                for (let i = viewStart; i < viewEnd; ++i) view[i] ^= 0x02020202;
+                break;
+            case MarkerAction.Clear:
+                for (let i = viewStart; i < viewEnd; ++i) view[i] = 0;
+                break;
+            default:
+                assertUnreachable(action);
+        }
+
+        for (let i = frontStart; i < frontEnd; ++i) {
+            applyMarkerActionAtPosition(array, i, action);
+        }
+
+        for (let i = backStart; i < backEnd; ++i) {
+            applyMarkerActionAtPosition(array, i, action);
         }
     } else {
-        for (let i = 0, _i = set.length; i < _i; i++) {
-            changed = applyMarkerActionAtPosition(array, set[i], action) || changed;
+        switch (action) {
+            case MarkerAction.Highlight:
+                for (let i = 0, il = set.length; i < il; ++i) array[set[i]] |= 1;
+                break;
+            case MarkerAction.RemoveHighlight:
+                for (let i = 0, il = set.length; i < il; ++i) array[set[i]] &= ~1;
+                break;
+            case MarkerAction.Select:
+                for (let i = 0, il = set.length; i < il; ++i) array[set[i]] |= 2;
+                break;
+            case MarkerAction.Deselect:
+                for (let i = 0, il = set.length; i < il; ++i) array[set[i]] &= ~2;
+                break;
+            case MarkerAction.Toggle:
+                for (let i = 0, il = set.length; i < il; ++i) array[set[i]] ^= 2;
+                break;
+            case MarkerAction.Clear:
+                for (let i = 0, il = set.length; i < il; ++i) array[set[i]] = 0;
+                break;
+            default:
+                assertUnreachable(action);
         }
     }
-    return changed;
+    return true;
 }

+ 6 - 2
src/mol-util/type-helpers.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>
@@ -25,4 +25,8 @@ export type NonNullableArray<T extends any[] | ReadonlyArray<any>> = T extends a
 export function ObjectKeys<T extends object>(o: T) {
     return Object.keys(o) as (keyof T)[];
 }
-export interface FiniteArray<T, L extends number = number> extends ReadonlyArray<T> { length: L };
+export interface FiniteArray<T, L extends number = number> extends ReadonlyArray<T> { length: L };
+
+export function assertUnreachable(x: never): never {
+    throw new Error('unreachable');
+}

+ 1 - 0
src/servers/common/swagger-ui/indexTemplate.ts

@@ -53,6 +53,7 @@ export const indexTemplate = `<!DOCTYPE html>
                         SwaggerUIBundle.presets.apis,
                         SwaggerUIStandalonePreset
                     ],
+                    syntaxHighlight: { activated: false, theme: 'agate' },
                     plugins: [
                         SwaggerUIBundle.plugins.DownloadUrl,
                         HidePlugin

+ 3 - 0
src/servers/model/CHANGELOG.md

@@ -1,3 +1,6 @@
+# 0.9.7
+* add Surrounding Ligands query
+
 # 0.9.6
 * optional download parameter
 

+ 29 - 1
src/servers/model/server/api.ts

@@ -136,6 +136,13 @@ const AssemblyNameParam: QueryParamInfo = {
     description: 'Assembly name. If none is provided, crystal symmetry (where available) or deposited model is used.'
 };
 
+const OmitWaterParam: QueryParamInfo = {
+    name: 'omit_water',
+    type: QueryParamType.Boolean,
+    required: false,
+    defaultValue: false
+};
+
 function Q<Params = any>(definition: Partial<QueryDefinition<Params>>) {
     return definition;
 }
@@ -223,7 +230,28 @@ const QueryMap = {
         jsonParams: [ AtomSiteTestJsonParam, RadiusParam ],
         restParams: [ ...AtomSiteTestRestParams, RadiusParam ],
         filter: QuerySchemas.interaction
-    })
+    }),
+    'surroundingLigands': Q<{ atom_site: AtomSiteSchema, radius: number, assembly_name: string, omit_water: boolean }>({
+        niceName: 'Surrounding Ligands',
+        description: 'Identifies (complete) ligands within the given radius from the source atom set. Takes crystal symmetry into account.',
+        query(p) {
+            const tests = getAtomsTests(p.atom_site);
+            const center = Queries.combinators.merge(tests.map(test => Queries.generators.atoms({
+                ...test,
+                entityTest: test.entityTest
+                    ? ctx => test.entityTest!(ctx) && ctx.element.unit.conformation.operator.isIdentity
+                    : ctx => ctx.element.unit.conformation.operator.isIdentity
+            })));
+            return Queries.modifiers.surroundingLigands({ query: center, radius: p.radius !== void 0 ? p.radius : 5, includeWater: !p.omit_water });
+        },
+        structureTransform(p, s) {
+            if (p.assembly_name) return StructureSymmetry.buildAssembly(s, '' + p.assembly_name).run();
+            return StructureSymmetry.builderSymmetryMates(s, p.radius !== void 0 ? p.radius : 5).run();
+        },
+        jsonParams: [ AtomSiteTestJsonParam, RadiusParam, OmitWaterParam, AssemblyNameParam ],
+        restParams: [ ...AtomSiteTestRestParams, RadiusParam, OmitWaterParam, AssemblyNameParam ],
+        filter: QuerySchemas.interaction
+    }),
 };
 
 export type QueryName = keyof typeof QueryMap

+ 1 - 0
src/servers/model/server/query.ts

@@ -248,6 +248,7 @@ async function resolveJobEntry(entry: JobEntry, structure: StructureWrapper, enc
 
         if (!entry.copyAllCategories && entry.queryDefinition.filter) encoder.setFilter(entry.queryDefinition.filter);
         if (result.length > 0) encode_mmCIF_categories(encoder, result, { copyAllCategories: entry.copyAllCategories });
+        else ConsoleLogger.logId(entry.job.id, 'Warning', `Empty result for Query ${entry.key}/${entry.queryDefinition.name}`);
         if (entry.transform && !Mat4.isIdentity(entry.transform)) GlobalModelTransformInfo.writeMmCif(encoder, entry.transform);
         if (!entry.copyAllCategories && entry.queryDefinition.filter) encoder.setFilter();
         perf.end('encode');

+ 1 - 1
src/servers/model/version.ts

@@ -4,4 +4,4 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-export const VERSION = '0.9.6';
+export const VERSION = '0.9.7';

+ 15 - 14
src/servers/plugin-state/index.ts

@@ -75,6 +75,7 @@ function remove(id: string) {
             i++;
             continue;
         }
+        if (e.isSticky) return;
         try {
             for (let j = i + 1; j < index.length; j++) {
                 index[j - 1] = index[j];
@@ -89,15 +90,15 @@ function remove(id: string) {
     }
 }
 
-function clear() {
-    let index = readIndex();
-    for (const e of index) {
-        try {
-            fs.unlinkSync(path.join(Config.working_folder, e.id + '.json'));
-        } catch { }
-    }
-    writeIndex([]);
-}
+// function clear() {
+//     let index = readIndex();
+//     for (const e of index) {
+//         try {
+//             fs.unlinkSync(path.join(Config.working_folder, e.id + '.json'));
+//         } catch { }
+//     }
+//     writeIndex([]);
+// }
 
 function mapPath(path: string) {
     if (!Config.api_prefix) return path;
@@ -128,11 +129,11 @@ app.get(mapPath(`/get/:id`), (req, res) => {
     });
 });
 
-app.get(mapPath(`/clear`), (req, res) => {
-    clear();
-    res.status(200);
-    res.end();
-});
+// app.get(mapPath(`/clear`), (req, res) => {
+//     clear();
+//     res.status(200);
+//     res.end();
+// });
 
 app.get(mapPath(`/remove/:id`), (req, res) => {
     remove((req.params.id as string || '').toLowerCase());