Browse Source

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

dsehnal 3 years ago
parent
commit
5330df87e1
40 changed files with 476 additions and 131 deletions
  1. 7 0
      CHANGELOG.md
  2. 1 1
      package-lock.json
  3. 1 1
      package.json
  4. 2 2
      src/extensions/dnatco/confal-pyramids/representation.ts
  5. 11 5
      src/extensions/geo-export/controls.ts
  6. 2 2
      src/extensions/geo-export/glb-exporter.ts
  7. 67 1
      src/extensions/geo-export/mesh-exporter.ts
  8. 2 67
      src/extensions/geo-export/obj-exporter.ts
  9. 2 2
      src/extensions/geo-export/render-object-exporter.ts
  10. 2 2
      src/extensions/geo-export/stl-exporter.ts
  11. 30 1
      src/extensions/geo-export/ui.tsx
  12. 262 0
      src/extensions/geo-export/usdz-exporter.ts
  13. 2 1
      src/extensions/rcsb/validation-report/representation.ts
  14. 0 3
      src/mol-geo/geometry/mesh/mesh.ts
  15. 0 3
      src/mol-geo/geometry/spheres/spheres.ts
  16. 0 3
      src/mol-geo/geometry/text/text.ts
  17. 17 10
      src/mol-model-formats/structure/common/component.ts
  18. 2 1
      src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
  19. 5 0
      src/mol-model/loci.ts
  20. 21 0
      src/mol-model/structure/structure/element/loci.ts
  21. 3 0
      src/mol-plugin-ui/controls/icons.tsx
  22. 1 0
      src/mol-repr/structure/params.ts
  23. 1 0
      src/mol-repr/structure/representation/ball-and-stick.ts
  24. 1 0
      src/mol-repr/structure/representation/ellipsoid.ts
  25. 1 0
      src/mol-repr/structure/representation/line.ts
  26. 1 1
      src/mol-repr/structure/units-representation.ts
  27. 5 7
      src/mol-repr/structure/units-visual.ts
  28. 2 1
      src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts
  29. 2 1
      src/mol-repr/structure/visual/bond-intra-unit-line.ts
  30. 2 1
      src/mol-repr/structure/visual/element-sphere.ts
  31. 2 2
      src/mol-repr/structure/visual/gaussian-surface-mesh.ts
  32. 2 1
      src/mol-repr/structure/visual/orientation-ellipsoid-mesh.ts
  33. 2 1
      src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
  34. 2 1
      src/mol-repr/structure/visual/polymer-backbone-sphere.ts
  35. 2 1
      src/mol-repr/structure/visual/polymer-trace-mesh.ts
  36. 1 2
      src/mol-repr/structure/visual/util/bond.ts
  37. 7 1
      src/mol-repr/structure/visual/util/common.ts
  38. 1 2
      src/mol-repr/structure/visual/util/element.ts
  39. 1 2
      src/mol-repr/structure/visual/util/nucleotide.ts
  40. 1 2
      src/mol-repr/structure/visual/util/polymer.ts

+ 7 - 0
CHANGELOG.md

@@ -8,6 +8,13 @@ Note that since we don't clearly distinguish between a public and private interf
 
 - Add `tubularHelices` parameter to Cartoon representation
 - Add `SdfFormat` and update SDF parser to be able to parse data headers according to spec (hopefully :)) #230
+- Fix mononucleotides detected as polymer components (#229)
+- Set default outline scale back to 1
+- Improved DCD reader cell angle handling (interpret near 0 angles as 90 deg)
+- Handle more residue/atom names commonly used in force-fields
+- Add USDZ support to ``geo-export`` extension.
+- Fix `includeParent` support for multi-instance bond visuals.
+- Add `operator` Loci granularity, selecting everything with the same operator name.
 
 ## [v2.1.0] - 2021-07-05
 

+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "2.1.0",
+  "version": "2.2.0-dev.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {

+ 1 - 1
package.json

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

+ 2 - 2
src/extensions/dnatco/confal-pyramids/representation.ts

@@ -20,10 +20,10 @@ import { Structure, StructureProperties, Unit } from '../../../mol-model/structu
 import { CustomProperty } from '../../../mol-model-props/common/custom-property';
 import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
 import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
-import { StructureGroup, UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
+import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { VisualContext } from '../../../mol-repr/visual';
-import { getAltResidueLociFromId } from '../../../mol-repr/structure/visual/util/common';
+import { getAltResidueLociFromId, StructureGroup } from '../../../mol-repr/structure/visual/util/common';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
 import { NullLocation } from '../../../mol-model/location';

+ 11 - 5
src/extensions/geo-export/controls.ts

@@ -13,15 +13,17 @@ import { PluginStateObject } from '../../mol-plugin-state/objects';
 import { StateSelection } from '../../mol-state';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { SetUtils } from '../../mol-util/set';
-import { ObjExporter } from './obj-exporter';
 import { GlbExporter } from './glb-exporter';
+import { ObjExporter } from './obj-exporter';
 import { StlExporter } from './stl-exporter';
+import { UsdzExporter } from './usdz-exporter';
 
 export const GeometryParams = {
     format: PD.Select('glb', [
         ['glb', 'glTF 2.0 Binary (.glb)'],
         ['stl', 'Stl (.stl)'],
-        ['obj', 'Wavefront (.obj)']
+        ['obj', 'Wavefront (.obj)'],
+        ['usdz', 'Universal Scene Description (.usdz)']
     ])
 };
 
@@ -44,11 +46,12 @@ export class GeometryControls extends PluginComponent {
                 const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
                 const filename = this.getFilename();
 
-                const boundingBox = Box3D.fromSphere3D(Box3D(), this.plugin.canvas3d?.boundingSphereVisible!);
-                let renderObjectExporter: GlbExporter | ObjExporter | StlExporter;
+                const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
+                const boundingSphere = this.plugin.canvas3d?.boundingSphereVisible!;
+                const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
+                let renderObjectExporter: GlbExporter | ObjExporter | StlExporter | UsdzExporter;
                 switch (this.behaviors.params.value.format) {
                     case 'glb':
-                        const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
                         renderObjectExporter = new GlbExporter(style, boundingBox);
                         break;
                     case 'obj':
@@ -57,6 +60,9 @@ export class GeometryControls extends PluginComponent {
                     case 'stl':
                         renderObjectExporter = new StlExporter(boundingBox);
                         break;
+                    case 'usdz':
+                        renderObjectExporter = new UsdzExporter(style, boundingBox, boundingSphere.radius);
+                        break;
                     default: throw new Error('Unsupported format.');
                 }
 

+ 2 - 2
src/extensions/geo-export/glb-exporter.ts

@@ -264,7 +264,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
         }
     }
 
-    getData() {
+    async getData() {
         const binaryBufferLength = this.byteOffset;
 
         const gltf = {
@@ -334,7 +334,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
     }
 
     async getBlob(ctx: RuntimeContext) {
-        return new Blob([this.getData().glb], { type: 'model/gltf-binary' });
+        return new Blob([(await this.getData()).glb], { type: 'model/gltf-binary' });
     }
 
     constructor(private style: Style, boundingBox: Box3D) {

+ 67 - 1
src/extensions/geo-export/mesh-exporter.ts

@@ -4,6 +4,7 @@
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  */
 
+import { sort, arraySwap } from '../../mol-data/util';
 import { GraphicsRenderObject } from '../../mol-gl/render-object';
 import { MeshValues } from '../../mol-gl/renderable/mesh';
 import { LinesValues } from '../../mol-gl/renderable/lines';
@@ -22,6 +23,7 @@ import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
 import { sizeDataFactor } from '../../mol-geo/geometry/size-data';
 import { Vec3 } from '../../mol-math/linear-algebra';
 import { RuntimeContext } from '../../mol-task';
+import { Color } from '../../mol-util/color/color';
 import { decodeFloatRGB } from '../../mol-util/float-packing';
 import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
 
@@ -111,6 +113,70 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         return interpolated.array;
     }
 
+    protected static quantizeColors(colorArray: Uint8Array, vertexCount: number) {
+        if (vertexCount <= 1024) return;
+        const rgb = Vec3();
+        const min = Vec3();
+        const max = Vec3();
+        const sum = Vec3();
+        const colorMap = new Map<Color, Color>();
+        const colorComparers = [
+            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]),
+            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]),
+            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]),
+        ];
+
+        const medianCut = (colors: Color[], l: number, r: number, depth: number) => {
+            if (l > r) return;
+            if (l === r || depth >= 10) {
+                // Find the average color.
+                Vec3.set(sum, 0, 0, 0);
+                for (let i = l; i <= r; ++i) {
+                    Color.toVec3(rgb, colors[i]);
+                    Vec3.add(sum, sum, rgb);
+                }
+                Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1)));
+                const averageColor = Color.fromArray(rgb, 0);
+                for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor);
+                return;
+            }
+
+            // Find the color channel with the greatest range.
+            Vec3.set(min, 255, 255, 255);
+            Vec3.set(max, 0, 0, 0);
+            for (let i = l; i <= r; ++i) {
+                Color.toVec3(rgb, colors[i]);
+                for (let j = 0; j < 3; ++j) {
+                    Vec3.min(min, min, rgb);
+                    Vec3.max(max, max, rgb);
+                }
+            }
+            let k = 0;
+            if (max[1] - min[1] > max[k] - min[k]) k = 1;
+            if (max[2] - min[2] > max[k] - min[k]) k = 2;
+
+            sort(colors, l, r + 1, colorComparers[k], arraySwap);
+
+            const m = (l + r) >> 1;
+            medianCut(colors, l, m, depth + 1);
+            medianCut(colors, m + 1, r, depth + 1);
+        };
+
+        // Create an array of unique colors and use the median cut algorithm.
+        const colorSet = new Set<Color>();
+        for (let i = 0; i < vertexCount; ++i) {
+            colorSet.add(Color.fromArray(colorArray, i * 3));
+        }
+        const colors = Array.from(colorSet);
+        medianCut(colors, 0, colors.length - 1, 0);
+
+        // Map actual colors to quantized colors.
+        for (let i = 0; i < vertexCount; ++i) {
+            const color = colorMap.get(Color.fromArray(colorArray, i * 3));
+            Color.toArray(color!, colorArray, i * 3);
+        }
+    }
+
     protected static getInstance(input: AddMeshInput, instanceIndex: number) {
         const { mesh, meshes } = input;
         if (mesh !== undefined) {
@@ -278,7 +344,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         }
     }
 
-    abstract getData(): D;
+    abstract getData(ctx: RuntimeContext): Promise<D>;
 
     abstract getBlob(ctx: RuntimeContext): Promise<Blob>;
 }

+ 2 - 67
src/extensions/geo-export/obj-exporter.ts

@@ -4,7 +4,6 @@
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  */
 
-import { sort, arraySwap } from '../../mol-data/util';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { Box3D } from '../../mol-math/geometry';
 import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
@@ -69,70 +68,6 @@ export class ObjExporter extends MeshExporter<ObjData> {
         }
     }
 
-    private static quantizeColors(colorArray: Uint8Array, vertexCount: number) {
-        if (vertexCount <= 1024) return;
-        const rgb = Vec3();
-        const min = Vec3();
-        const max = Vec3();
-        const sum = Vec3();
-        const colorMap = new Map<Color, Color>();
-        const colorComparers = [
-            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]),
-            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]),
-            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]),
-        ];
-
-        const medianCut = (colors: Color[], l: number, r: number, depth: number) => {
-            if (l > r) return;
-            if (l === r || depth >= 10) {
-                // Find the average color.
-                Vec3.set(sum, 0, 0, 0);
-                for (let i = l; i <= r; ++i) {
-                    Color.toVec3(rgb, colors[i]);
-                    Vec3.add(sum, sum, rgb);
-                }
-                Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1)));
-                const averageColor = Color.fromArray(rgb, 0);
-                for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor);
-                return;
-            }
-
-            // Find the color channel with the greatest range.
-            Vec3.set(min, 255, 255, 255);
-            Vec3.set(max, 0, 0, 0);
-            for (let i = l; i <= r; ++i) {
-                Color.toVec3(rgb, colors[i]);
-                for (let j = 0; j < 3; ++j) {
-                    Vec3.min(min, min, rgb);
-                    Vec3.max(max, max, rgb);
-                }
-            }
-            let k = 0;
-            if (max[1] - min[1] > max[k] - min[k]) k = 1;
-            if (max[2] - min[2] > max[k] - min[k]) k = 2;
-
-            sort(colors, l, r + 1, colorComparers[k], arraySwap);
-
-            const m = (l + r) >> 1;
-            medianCut(colors, l, m, depth + 1);
-            medianCut(colors, m + 1, r, depth + 1);
-        };
-
-        // Create an array of unique colors and use the median cut algorithm.
-        const colorSet = new Set<Color>();
-        for (let i = 0; i < vertexCount; ++i) {
-            colorSet.add(Color.fromArray(colorArray, i * 3));
-        }
-        const colors = Array.from(colorSet);
-        medianCut(colors, 0, colors.length - 1, 0);
-
-        // Map actual colors to quantized colors.
-        for (let i = 0; i < vertexCount; ++i) {
-            const color = colorMap.get(Color.fromArray(colorArray, i * 3));
-            Color.toArray(color!, colorArray, i * 3);
-        }
-    }
-
     protected async addMeshWithColors(input: AddMeshInput) {
         const { mesh, values, isGeoTexture, webgl, ctx } = input;
 
@@ -256,7 +191,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
         }
     }
 
-    getData() {
+    async getData() {
         return {
             obj: StringBuilder.getString(this.obj),
             mtl: StringBuilder.getString(this.mtl)
@@ -264,7 +199,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
     }
 
     async getBlob(ctx: RuntimeContext) {
-        const { obj, mtl } = this.getData();
+        const { obj, mtl } = await this.getData();
         const objData = new Uint8Array(obj.length);
         asciiWrite(objData, obj);
         const mtlData = new Uint8Array(mtl.length);

+ 2 - 2
src/extensions/geo-export/render-object-exporter.ts

@@ -9,12 +9,12 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 import { RuntimeContext } from '../../mol-task';
 
 export type RenderObjectExportData = {
-    [k: string]: string | Uint8Array | undefined
+    [k: string]: string | Uint8Array | ArrayBuffer | undefined
 }
 
 export interface RenderObjectExporter<D extends RenderObjectExportData> {
     readonly fileExtension: string
     add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext): Promise<void> | undefined
-    getData(): D
+    getData(ctx: RuntimeContext): Promise<D>
     getBlob(ctx: RuntimeContext): Promise<Blob>
 }

+ 2 - 2
src/extensions/geo-export/stl-exporter.ts

@@ -89,7 +89,7 @@ export class StlExporter extends MeshExporter<StlData> {
         }
     }
 
-    getData() {
+    async getData() {
         const stl = new Uint8Array(84 + 50 * this.triangleCount);
 
         asciiWrite(stl, `Exported from Mol* ${PLUGIN_VERSION}`);
@@ -106,7 +106,7 @@ export class StlExporter extends MeshExporter<StlData> {
     }
 
     async getBlob(ctx: RuntimeContext) {
-        return new Blob([this.getData().stl], { type: 'model/stl' });
+        return new Blob([(await this.getData()).stl], { type: 'model/stl' });
     }
 
     constructor(boundingBox: Box3D) {

+ 30 - 1
src/extensions/geo-export/ui.tsx

@@ -7,7 +7,7 @@
 import { merge } from 'rxjs';
 import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
 import { Button } from '../../mol-plugin-ui/controls/common';
-import { GetAppSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
+import { GetAppSvg, CubeScanSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
 import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
 import { download } from '../../mol-util/download';
 import { GeometryParams, GeometryControls } from './controls';
@@ -18,6 +18,7 @@ interface State {
 
 export class GeometryExporterUI extends CollapsableControls<{}, State> {
     private _controls: GeometryControls | undefined;
+    private isARSupported: boolean | undefined;
 
     get controls() {
         return this._controls || (this._controls = new GeometryControls(this.plugin));
@@ -32,6 +33,9 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
     }
 
     protected renderControls(): JSX.Element {
+        if (this.isARSupported === undefined) {
+            this.isARSupported = !!document.createElement('a').relList?.supports?.('ar');
+        }
         const ctrl = this.controls;
         return <>
             <ParameterControls
@@ -45,6 +49,13 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
                 disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
                 Save
             </Button>
+            {this.isARSupported && ctrl.behaviors.params.value.format === 'usdz' &&
+                <Button icon={CubeScanSvg}
+                    onClick={this.viewInAR} style={{ marginTop: 1 }}
+                    disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
+                    View in AR
+                </Button>
+            }
         </>;
     }
 
@@ -75,4 +86,22 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
             this.setState({ busy: false });
         }
     }
+
+    viewInAR = async () => {
+        try {
+            this.setState({ busy: true });
+            const data = await this.controls.exportGeometry();
+            this.setState({ busy: false });
+            const a = document.createElement('a');
+            a.rel = 'ar';
+            a.href = URL.createObjectURL(data.blob);
+            // For in-place viewing of USDZ on iOS, the link must contain a single child that is either an img or picture.
+            // https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/
+            a.appendChild(document.createElement('img'));
+            setTimeout(() => URL.revokeObjectURL(a.href), 4E4); // 40s
+            setTimeout(() => a.dispatchEvent(new MouseEvent('click')));
+        } catch {
+            this.setState({ busy: false });
+        }
+    }
 }

+ 262 - 0
src/extensions/geo-export/usdz-exporter.ts

@@ -0,0 +1,262 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ */
+
+import { Style } from '../../mol-gl/renderer';
+import { asciiWrite } from '../../mol-io/common/ascii';
+import { Box3D } from '../../mol-math/geometry';
+import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
+import { PLUGIN_VERSION } from '../../mol-plugin/version';
+import { RuntimeContext } from '../../mol-task';
+import { StringBuilder } from '../../mol-util';
+import { Color } from '../../mol-util/color/color';
+import { zip } from '../../mol-util/zip/zip';
+import { MeshExporter, AddMeshInput } from './mesh-exporter';
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3fromArray = Vec3.fromArray;
+const v3transformMat4 = Vec3.transformMat4;
+const v3transformMat3 = Vec3.transformMat3;
+const mat3directionTransform = Mat3.directionTransform;
+
+// https://graphics.pixar.com/usd/docs/index.html
+
+export type UsdzData = {
+    usdz: ArrayBuffer
+}
+
+export class UsdzExporter extends MeshExporter<UsdzData> {
+    readonly fileExtension = 'usdz';
+    private meshes: string[] = [];
+    private materials: string[] = [];
+    private materialSet = new Set<number>();
+    private centerTransform: Mat4;
+
+    private static getMaterialKey(color: Color, alpha: number) {
+        return color * 256 + Math.round(alpha * 255);
+    }
+
+    private addMaterial(color: Color, alpha: number) {
+        const materialKey = UsdzExporter.getMaterialKey(color, alpha);
+        if (this.materialSet.has(materialKey)) return;
+        this.materialSet.add(materialKey);
+        const [r, g, b] = Color.toRgbNormalized(color);
+        this.materials.push(`
+def Material "material${materialKey}"
+{
+    token outputs:surface.connect = </material${materialKey}/shader.outputs:surface>
+    def Shader "shader"
+    {
+        uniform token info:id = "UsdPreviewSurface"
+        color3f inputs:diffuseColor = (${r},${g},${b})
+        float inputs:opacity = ${alpha}
+        float inputs:metallic = ${this.style.metalness}
+        float inputs:roughness = ${this.style.roughness}
+        token outputs:surface
+    }
+}
+`);
+    }
+
+    protected async addMeshWithColors(input: AddMeshInput) {
+        const { mesh, values, isGeoTexture, webgl, ctx } = input;
+
+        const t = Mat4();
+        const n = Mat3();
+        const tmpV = Vec3();
+        const stride = isGeoTexture ? 4 : 3;
+
+        const groupCount = values.uGroupCount.ref.value;
+        const colorType = values.dColorType.ref.value;
+        const tColor = values.tColor.ref.value.array;
+        const uAlpha = values.uAlpha.ref.value;
+        const dTransparency = values.dTransparency.ref.value;
+        const tTransparency = values.tTransparency.ref.value;
+        const aTransform = values.aTransform.ref.value;
+        const instanceCount = values.uInstanceCount.ref.value;
+
+        let interpolatedColors: Uint8Array;
+        if (colorType === 'volume' || colorType === 'volumeInstance') {
+            interpolatedColors = UsdzExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
+            UsdzExporter.quantizeColors(interpolatedColors, mesh!.vertexCount);
+        }
+
+        await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
+
+            const { vertices, normals, indices, groups, vertexCount, drawCount } = UsdzExporter.getInstance(input, instanceIndex);
+
+            Mat4.fromArray(t, aTransform, instanceIndex * 16);
+            Mat4.mul(t, this.centerTransform, t);
+            mat3directionTransform(n, t);
+
+            const vertexBuilder = StringBuilder.create();
+            const normalBuilder = StringBuilder.create();
+            const indexBuilder = StringBuilder.create();
+
+            // position
+            for (let i = 0; i < vertexCount; ++i) {
+                v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
+                StringBuilder.writeSafe(vertexBuilder, (i === 0) ? '(' : ',(');
+                StringBuilder.writeFloat(vertexBuilder, tmpV[0], 10000);
+                StringBuilder.writeSafe(vertexBuilder, ',');
+                StringBuilder.writeFloat(vertexBuilder, tmpV[1], 10000);
+                StringBuilder.writeSafe(vertexBuilder, ',');
+                StringBuilder.writeFloat(vertexBuilder, tmpV[2], 10000);
+                StringBuilder.writeSafe(vertexBuilder, ')');
+            }
+
+            // normal
+            for (let i = 0; i < vertexCount; ++i) {
+                v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
+                StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',(');
+                StringBuilder.writeFloat(normalBuilder, tmpV[0], 100);
+                StringBuilder.writeSafe(normalBuilder, ',');
+                StringBuilder.writeFloat(normalBuilder, tmpV[1], 100);
+                StringBuilder.writeSafe(normalBuilder, ',');
+                StringBuilder.writeFloat(normalBuilder, tmpV[2], 100);
+                StringBuilder.writeSafe(normalBuilder, ')');
+            }
+
+            // face
+            for (let i = 0; i < drawCount; ++i) {
+                const v = isGeoTexture ? i : indices![i];
+                if (i > 0) StringBuilder.writeSafe(indexBuilder, ',');
+                StringBuilder.writeInteger(indexBuilder, v);
+            }
+
+            // color
+            const faceIndicesByMaterial = new Map<number, number[]>();
+            for (let i = 0; i < drawCount; i += 3) {
+                let color: Color;
+                switch (colorType) {
+                    case 'uniform':
+                        color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
+                        break;
+                    case 'instance':
+                        color = Color.fromArray(tColor, instanceIndex * 3);
+                        break;
+                    case 'group': {
+                        const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
+                        color = Color.fromArray(tColor, group * 3);
+                        break;
+                    }
+                    case 'groupInstance': {
+                        const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
+                        color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
+                        break;
+                    }
+                    case 'vertex':
+                        color = Color.fromArray(tColor, indices![i] * 3);
+                        break;
+                    case 'vertexInstance':
+                        color = Color.fromArray(tColor, (instanceIndex * vertexCount + indices![i]) * 3);
+                        break;
+                    case 'volume':
+                        color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3);
+                        break;
+                    case 'volumeInstance':
+                        color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3);
+                        break;
+                    default: throw new Error('Unsupported color type.');
+                }
+
+                let alpha = uAlpha;
+                if (dTransparency) {
+                    const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
+                    const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
+                    alpha *= 1 - transparency;
+                }
+
+                this.addMaterial(color, alpha);
+
+                const materialKey = UsdzExporter.getMaterialKey(color, alpha);
+                let faceIndices = faceIndicesByMaterial.get(materialKey);
+                if (faceIndices === undefined) {
+                    faceIndices = [];
+                    faceIndicesByMaterial.set(materialKey, faceIndices);
+                }
+                faceIndices.push(i / 3);
+            }
+
+            // If this mesh uses only one material, bind it to the material directly.
+            // Otherwise, use GeomSubsets to bind it to multiple materials.
+            let materialBinding: string;
+            if (faceIndicesByMaterial.size === 1) {
+                const materialKey = faceIndicesByMaterial.keys().next().value;
+                materialBinding = `rel material:binding = </material${materialKey}>`;
+            } else {
+                const geomSubsets: string[] = [];
+                faceIndicesByMaterial.forEach((faceIndices: number[], materialKey: number) => {
+                    geomSubsets.push(`
+    def GeomSubset "g${materialKey}"
+    {
+        uniform token elementType = "face"
+        uniform token familyName = "materialBind"
+        int[] indices = [${faceIndices.join(',')}]
+        rel material:binding = </material${materialKey}>
+    }
+`);
+                });
+                materialBinding = geomSubsets.join('');
+            }
+
+            this.meshes.push(`
+def Mesh "mesh${this.meshes.length}"
+{
+    int[] faceVertexCounts = [${new Array(drawCount / 3).fill(3).join(',')}]
+    int[] faceVertexIndices = [${StringBuilder.getString(indexBuilder)}]
+    point3f[] points = [${StringBuilder.getString(vertexBuilder)}]
+    normal3f[] primvars:normals = [${StringBuilder.getString(normalBuilder)}] (
+        interpolation = "vertex"
+    )
+    uniform token subdivisionScheme = "none"
+    ${materialBinding}
+}
+`);
+        }
+    }
+
+    async getData(ctx: RuntimeContext) {
+        const header = `#usda 1.0
+(
+    customLayerData = {
+        string creator = "Mol* ${PLUGIN_VERSION}"
+    }
+    metersPerUnit = 1
+)
+`;
+        const usda = [header, ...this.materials, ...this.meshes].join('');
+        const usdaData = new Uint8Array(usda.length);
+        asciiWrite(usdaData, usda);
+        const zipDataObj = {
+            ['model.usda']: usdaData
+        };
+        return {
+            usdz: await zip(ctx, zipDataObj, true)
+        };
+    }
+
+    async getBlob(ctx: RuntimeContext) {
+        const { usdz } = await this.getData(ctx);
+        return new Blob([usdz], { type: 'model/vnd.usdz+zip' });
+    }
+
+    constructor(private style: Style, boundingBox: Box3D, radius: number) {
+        super();
+        const t = Mat4();
+        // scale the model so that it fits within 1 meter
+        Mat4.fromUniformScaling(t, Math.min(1 / (radius * 2), 1));
+        // translate the model so that it sits on the ground plane (y = 0)
+        Mat4.translate(t, t, Vec3.create(
+            -(boundingBox.min[0] + boundingBox.max[0]) / 2,
+            -boundingBox.min[1],
+            -(boundingBox.min[2] + boundingBox.max[2]) / 2
+        ));
+        this.centerTransform = t;
+    }
+}

+ 2 - 1
src/extensions/rcsb/validation-report/representation.ts

@@ -16,7 +16,7 @@ import { RepresentationContext, RepresentationParamsGetter, Representation } fro
 import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider, ComplexRepresentation } from '../../../mol-repr/structure/representation';
 import { VisualContext } from '../../../mol-repr/visual';
 import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr/structure/units-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { ClashesProvider, IntraUnitClashes, InterUnitClashes, ValidationReport } from './prop';
@@ -28,6 +28,7 @@ import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { bondLabel } from '../../../mol-theme/label';
 import { getUnitKindsParam } from '../../../mol-repr/structure/params';
+import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
 
 //
 

+ 0 - 3
src/mol-geo/geometry/mesh/mesh.ts

@@ -387,9 +387,6 @@ export namespace Mesh {
 
     function createValues(mesh: Mesh, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): MeshValues {
         const { instanceCount, groupCount } = locationIt;
-        if (instanceCount !== transform.instanceCount.ref.value) {
-            throw new Error('instanceCount values in TransformData and LocationIterator differ');
-        }
         const positionIt = createPositionIterator(mesh, transform);
 
         const color = createColors(locationIt, positionIt, theme.color);

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

@@ -163,9 +163,6 @@ export namespace Spheres {
 
     function createValues(spheres: Spheres, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): SpheresValues {
         const { instanceCount, groupCount } = locationIt;
-        if (instanceCount !== transform.instanceCount.ref.value) {
-            throw new Error('instanceCount values in TransformData and LocationIterator differ');
-        }
         const positionIt = createPositionIterator(spheres, transform);
 
         const color = createColors(locationIt, positionIt, theme.color);

+ 0 - 3
src/mol-geo/geometry/text/text.ts

@@ -206,9 +206,6 @@ export namespace Text {
 
     function createValues(text: Text, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextValues {
         const { instanceCount, groupCount } = locationIt;
-        if (instanceCount !== transform.instanceCount.ref.value) {
-            throw new Error('instanceCount values in TransformData and LocationIterator differ');
-        }
         const positionIt = createPositionIterator(text, transform);
 
         const color = createColors(locationIt, positionIt, theme.color);

+ 17 - 10
src/mol-model-formats/structure/common/component.ts

@@ -13,21 +13,26 @@ import { mmCIF_chemComp_schema } from '../../../mol-io/reader/cif/schema/mmcif-e
 type Component = Table.Row<Pick<mmCIF_chemComp_schema, 'id' | 'name' | 'type'>>
 
 const ProteinAtomIdsList = [
-    new Set([ 'CA' ]),
-    new Set([ 'C' ]),
-    new Set([ 'N' ])
+    new Set(['CA']),
+    new Set(['C']),
+    new Set(['N'])
 ];
 const RnaAtomIdsList = [
-    new Set([ 'P', 'O3\'', 'O3*' ]),
-    new Set([ 'C4\'', 'C4*' ]),
-    new Set([ 'O2\'', 'O2*', 'F2\'', 'F2*' ])
+    new Set(['P', 'O3\'', 'O3*']),
+    new Set(['C4\'', 'C4*']),
+    new Set(['O2\'', 'O2*', 'F2\'', 'F2*'])
 ];
 const DnaAtomIdsList = [
-    new Set([ 'P', 'O3\'', 'O3*' ]),
-    new Set([ 'C3\'', 'C3*' ]),
-    new Set([ 'O2\'', 'O2*', 'F2\'', 'F2*' ])
+    new Set(['P', 'O3\'', 'O3*']),
+    new Set(['C3\'', 'C3*']),
+    new Set(['O2\'', 'O2*', 'F2\'', 'F2*'])
 ];
 
+/** Used to reduce false positives for atom name-based type guessing */
+const NonPolymerNames = new Set([
+    'FMN', 'NCN', 'FNS', 'FMA' // Mononucleotides
+]);
+
 const StandardComponents = (function() {
     const map = new Map<string, Component>();
     const components: Component[] = [
@@ -151,9 +156,11 @@ export class ComponentBuilder {
                 this.set(StandardComponents.get(compId)!);
             } else if (WaterNames.has(compId)) {
                 this.set({ id: compId, name: 'WATER', type: 'non-polymer' });
+            } else if (NonPolymerNames.has(compId)) {
+                this.set({ id: compId, name: this.namesMap.get(compId) || compId, type: 'non-polymer' });
             } else {
                 const atomIds = this.getAtomIds(index);
-                if (CharmmIonComponents.has(compId) && atomIds.size === 1) {
+                if (atomIds.size === 1 && CharmmIonComponents.has(compId)) {
                     this.set(CharmmIonComponents.get(compId)!);
                 } else {
                     const type = this.getType(atomIds);

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

@@ -15,12 +15,13 @@ import { VisualContext } from '../../../mol-repr/visual';
 import { Theme } from '../../../mol-theme/theme';
 import { InteractionsProvider } from '../interactions';
 import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr/structure/units-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { Interactions } from '../interactions/interactions';
 import { InteractionFlag } from '../interactions/common';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
 
 async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);

+ 5 - 0
src/mol-model/loci.ts

@@ -231,6 +231,11 @@ namespace Loci {
                 ? StructureElement.Loci.extendToWholeModels(loci)
                 : loci;
         },
+        'operator': (loci: Loci) => {
+            return StructureElement.Loci.is(loci)
+                ? StructureElement.Loci.extendToWholeOperators(loci)
+                : loci;
+        },
         'structure': (loci: Loci) => {
             return StructureElement.Loci.is(loci)
                 ? Structure.toStructureElementLoci(loci.structure)

+ 21 - 0
src/mol-model/structure/structure/element/loci.ts

@@ -488,6 +488,27 @@ export namespace Loci {
         return Loci(loci.structure, elements);
     }
 
+    export function extendToWholeOperators(loci: Loci): Loci {
+        const elements: Loci['elements'][0][] = [];
+        const operators = new Set<string>();
+        const { units } = loci.structure;
+
+        for (let i = 0, len = loci.elements.length; i < len; i++) {
+            const e = loci.elements[i];
+            operators.add(e.unit.conformation.operator.name);
+        }
+
+        for (let i = 0, il = units.length; i < il; ++i) {
+            const unit = units[i];
+            if (operators.has(unit.conformation.operator.name)) {
+                const indices = OrderedSet.ofBounds(0, unit.elements.length) as OrderedSet<UnitIndex>;
+                elements[elements.length] = { unit, indices };
+            }
+        }
+
+        return Loci(loci.structure, elements);
+    }
+
     //
 
     const boundaryHelper = new BoundaryHelper('98');

+ 3 - 0
src/mol-plugin-ui/controls/icons.tsx

@@ -41,6 +41,9 @@ export function MoleculeSvg() { return _Molecule; }
 const _CubeOutline = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" /></svg>;
 export function CubeOutlineSvg() { return _CubeOutline; }
 
+const _CubeScan = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M17,22V20H20V17H22V20.5C22,20.89 21.84,21.24 21.54,21.54C21.24,21.84 20.89,22 20.5,22H17M7,22H3.5C3.11,22 2.76,21.84 2.46,21.54C2.16,21.24 2,20.89 2,20.5V17H4V20H7V22M17,2H20.5C20.89,2 21.24,2.16 21.54,2.46C21.84,2.76 22,3.11 22,3.5V7H20V4H17V2M7,2V4H4V7H2V3.5C2,3.11 2.16,2.76 2.46,2.46C2.76,2.16 3.11,2 3.5,2H7M13,17.25L17,14.95V10.36L13,12.66V17.25M12,10.92L16,8.63L12,6.28L8,8.63L12,10.92M7,14.95L11,17.25V12.66L7,10.36V14.95M18.23,7.59C18.73,7.91 19,8.34 19,8.91V15.23C19,15.8 18.73,16.23 18.23,16.55L12.75,19.73C12.25,20.05 11.75,20.05 11.25,19.73L5.77,16.55C5.27,16.23 5,15.8 5,15.23V8.91C5,8.34 5.27,7.91 5.77,7.59L11.25,4.41C11.5,4.28 11.75,4.22 12,4.22C12.25,4.22 12.5,4.28 12.75,4.41L18.23,7.59Z" /></svg>;
+export function CubeScanSvg() { return _CubeScan; }
+
 const _CubeSend = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M16,4L9,8.04V15.96L16,20L23,15.96V8.04M16,6.31L19.8,8.5L16,10.69L12.21,8.5M0,7V9H7V7M11,10.11L15,12.42V17.11L11,14.81M21,10.11V14.81L17,17.11V12.42M2,11V13H7V11M4,15V17H7V15" /></svg>;
 export function CubeSendSvg() { return _CubeSend; }
 

+ 1 - 0
src/mol-repr/structure/params.ts

@@ -22,6 +22,7 @@ export function getUnitKindsParam(defaultValue: UnitKind[]) {
 
 export const StructureParams = {
     unitKinds: getUnitKindsParam(['atomic', 'spheres']),
+    includeParent: PD.Boolean(false, { isHidden: true }),
 };
 export type StructureParams = typeof StructureParams
 

+ 1 - 0
src/mol-repr/structure/representation/ball-and-stick.ts

@@ -27,6 +27,7 @@ export const BallAndStickParams = {
     traceOnly: PD.Boolean(false, { isHidden: true }), // not useful here
     ...IntraUnitBondCylinderParams,
     ...InterUnitBondCylinderParams,
+    includeParent: PD.Boolean(false),
     unitKinds: getUnitKindsParam(['atomic']),
     sizeFactor: PD.Numeric(0.15, { min: 0.01, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0.01, max: 3, step: 0.01 }),

+ 1 - 0
src/mol-repr/structure/representation/ellipsoid.ts

@@ -25,6 +25,7 @@ export const EllipsoidParams = {
     ...EllipsoidMeshParams,
     ...IntraUnitBondCylinderParams,
     ...InterUnitBondCylinderParams,
+    includeParent: PD.Boolean(false),
     adjustCylinderLength: PD.Boolean(false, { isHidden: true }), // not useful here
     unitKinds: getUnitKindsParam(['atomic']),
     sizeFactor: PD.Numeric(1, { min: 0.01, max: 10, step: 0.01 }),

+ 1 - 0
src/mol-repr/structure/representation/line.ts

@@ -23,6 +23,7 @@ const LineVisuals = {
 export const LineParams = {
     ...IntraUnitBondLineParams,
     ...InterUnitBondLineParams,
+    includeParent: PD.Boolean(false),
     sizeFactor: PD.Numeric(1.5, { min: 0.01, max: 10, step: 0.01 }),
     unitKinds: getUnitKindsParam(['atomic']),
     visuals: PD.MultiSelect(['intra-bond', 'inter-bond'], PD.objectToOptions(LineVisuals))

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

@@ -8,7 +8,6 @@
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationState } from './representation';
 import { Visual } from '../visual';
-import { StructureGroup } from './units-visual';
 import { RepresentationContext, RepresentationParamsGetter } from '../representation';
 import { Structure, Unit, StructureElement, Bond } from '../../mol-model/structure';
 import { Subject } from 'rxjs';
@@ -25,6 +24,7 @@ import { Interval } from '../../mol-data/int';
 import { StructureParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
 import { WebGLContext } from '../../mol-gl/webgl/context';
+import { StructureGroup } from './visual/util/common';
 
 export interface UnitsVisual<P extends StructureParams> extends Visual<StructureGroup, P> { }
 

+ 5 - 7
src/mol-repr/structure/units-visual.ts

@@ -11,7 +11,7 @@ import { Visual, VisualContext } from '../visual';
 import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
 import { LocationIterator } from '../../mol-geo/util/location-iterator';
 import { Theme } from '../../mol-theme/theme';
-import { createUnitsTransform, includesUnitKind } from './visual/util/common';
+import { createUnitsTransform, includesUnitKind, StructureGroup } from './visual/util/common';
 import { createRenderObject, GraphicsRenderObject, RenderObjectValues } from '../../mol-gl/render-object';
 import { PickingId } from '../../mol-geo/geometry/picking';
 import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
@@ -41,13 +41,11 @@ import { Clipping } from '../../mol-theme/clipping';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { isPromiseLike } from '../../mol-util/type-helpers';
 
-export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
-
 export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
 
-function createUnitsRenderObject<G extends Geometry>(group: Unit.SymmetryGroup, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) {
+function createUnitsRenderObject<G extends Geometry>(structureGroup: StructureGroup, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<StructureParams & Geometry.Params<G>>, materialId: number) {
     const { createValues, createRenderableState } = Geometry.getUtils(geometry);
-    const transform = createUnitsTransform(group);
+    const transform = createUnitsTransform(structureGroup, props.includeParent);
     const values = createValues(geometry, transform, locationIt, theme, props);
     const state = createRenderableState(props);
     return createRenderObject(geometry.kind, values, state, materialId);
@@ -179,7 +177,7 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
         if (updateState.createNew) {
             locationIt = createLocationIterator(newStructureGroup);
             if (newGeometry) {
-                renderObject = createUnitsRenderObject(newStructureGroup.group, newGeometry, locationIt, newTheme, newProps, materialId);
+                renderObject = createUnitsRenderObject(newStructureGroup, newGeometry, locationIt, newTheme, newProps, materialId);
                 positionIt = createPositionIterator(newGeometry, renderObject.values);
             } else {
                 throw new Error('expected geometry to be given');
@@ -198,7 +196,7 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
 
             if (updateState.updateMatrix) {
                 // console.log('update matrix');
-                createUnitsTransform(newStructureGroup.group, renderObject.values);
+                createUnitsTransform(newStructureGroup, newProps.includeParent, renderObject.values);
             }
 
             if (updateState.createGeometry) {

+ 2 - 1
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -13,7 +13,7 @@ import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { arrayEqual } from '../../../mol-util';
 import { createLinkCylinderImpostors, createLinkCylinderMesh, LinkBuilderProps, LinkStyle } from './util/link';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup, UnitsCylindersParams, UnitsCylindersVisual } from '../units-visual';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, UnitsCylindersParams, UnitsCylindersVisual } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { BondType } from '../../../mol-model/structure/model/types';
 import { BondCylinderParams, BondIterator, eachIntraBond, getIntraBondLoci, makeIntraBondIgnoreTest } from './util/bond';
@@ -23,6 +23,7 @@ import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
 import { SortedArray } from '../../../mol-data/int';
 import { arrayIntersectionSize } from '../../../mol-util/array';
+import { StructureGroup } from './util/common';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const isBondType = BondType.is;

+ 2 - 1
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -11,7 +11,7 @@ import { Theme } from '../../../mol-theme/theme';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { arrayEqual } from '../../../mol-util';
 import { LinkStyle, createLinkLines, LinkBuilderProps } from './util/link';
-import { UnitsVisual, UnitsLinesParams, UnitsLinesVisual, StructureGroup } from '../units-visual';
+import { UnitsVisual, UnitsLinesParams, UnitsLinesVisual } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { BondType } from '../../../mol-model/structure/model/types';
 import { BondIterator, BondLineParams, getIntraBondLoci, eachIntraBond, makeIntraBondIgnoreTest } from './util/bond';
@@ -19,6 +19,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
 import { Lines } from '../../../mol-geo/geometry/lines/lines';
 import { IntAdjacencyGraph } from '../../../mol-math/graph';
 import { arrayIntersectionSize } from '../../../mol-util/array';
+import { StructureGroup } from './util/common';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const isBondType = BondType.is;

+ 2 - 1
src/mol-repr/structure/visual/element-sphere.ts

@@ -6,12 +6,13 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { UnitsMeshParams, UnitsSpheresParams, UnitsVisual, UnitsSpheresVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
+import { UnitsMeshParams, UnitsSpheresParams, UnitsVisual, UnitsSpheresVisual, UnitsMeshVisual } from '../units-visual';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { createElementSphereImpostor, ElementIterator, getElementLoci, eachElement, createElementSphereMesh } from './util/element';
 import { VisualUpdateState } from '../../util';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
 import { Structure } from '../../../mol-model/structure';
+import { StructureGroup } from './util/common';
 
 export const ElementSphereParams = {
     ...UnitsMeshParams,

+ 2 - 2
src/mol-repr/structure/visual/gaussian-surface-mesh.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { UnitsMeshParams, UnitsTextureMeshParams, UnitsVisual, UnitsMeshVisual, UnitsTextureMeshVisual, StructureGroup } from '../units-visual';
+import { UnitsMeshParams, UnitsTextureMeshParams, UnitsVisual, UnitsMeshVisual, UnitsTextureMeshVisual } from '../units-visual';
 import { GaussianDensityParams, computeUnitGaussianDensity, computeUnitGaussianDensityTexture2d, GaussianDensityProps, computeStructureGaussianDensity, computeStructureGaussianDensityTexture2d } from './util/gaussian';
 import { VisualContext } from '../../visual';
 import { Unit, Structure } from '../../../mol-model/structure';
@@ -18,7 +18,7 @@ import { TextureMesh } from '../../../mol-geo/geometry/texture-mesh/texture-mesh
 import { extractIsosurface } from '../../../mol-gl/compute/marching-cubes/isosurface';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { ComplexVisual, ComplexMeshParams, ComplexMeshVisual, ComplexTextureMeshVisual, ComplexTextureMeshParams } from '../complex-visual';
-import { getUnitExtraRadius, getStructureExtraRadius, getVolumeSliceInfo } from './util/common';
+import { getUnitExtraRadius, getStructureExtraRadius, getVolumeSliceInfo, StructureGroup } from './util/common';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { MeshValues } from '../../../mol-gl/renderable/mesh';
 import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';

+ 2 - 1
src/mol-repr/structure/visual/orientation-ellipsoid-mesh.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr/structure/units-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { VisualContext } from '../../../mol-repr/visual';
 import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
@@ -22,6 +22,7 @@ import { UnitIndex } from '../../../mol-model/structure/structure/element/elemen
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { MoleculeType } from '../../../mol-model/structure/model/types';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
+import { StructureGroup } from './util/common';
 
 export const OrientationEllipsoidMeshParams = {
     ...UnitsMeshParams,

+ 2 - 1
src/mol-repr/structure/visual/polymer-backbone-cylinder.ts

@@ -14,7 +14,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra';
 import { CylinderProps } from '../../../mol-geo/primitive/cylinder';
 import { eachPolymerElement, getPolymerElementLoci, NucleicShift, PolymerLocationIterator, StandardShift } from './util/polymer';
 import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, UnitsCylindersVisual, UnitsCylindersParams, StructureGroup } from '../units-visual';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, UnitsCylindersVisual, UnitsCylindersParams } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
 import { Sphere3D } from '../../../mol-math/geometry';
@@ -23,6 +23,7 @@ import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
 import { CylindersBuilder } from '../../../mol-geo/geometry/cylinders/cylinders-builder';
 import { eachPolymerBackboneLink } from './util/polymer/backbone';
+import { StructureGroup } from './util/common';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3scale = Vec3.scale;

+ 2 - 1
src/mol-repr/structure/visual/polymer-backbone-sphere.ts

@@ -12,7 +12,7 @@ import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { eachPolymerElement, getPolymerElementLoci, PolymerLocationIterator } from './util/polymer';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, UnitsSpheresVisual, UnitsSpheresParams, StructureGroup } from '../units-visual';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, UnitsSpheresVisual, UnitsSpheresParams } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
 import { Sphere3D } from '../../../mol-math/geometry';
@@ -22,6 +22,7 @@ import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
 import { SpheresBuilder } from '../../../mol-geo/geometry/spheres/spheres-builder';
 import { eachPolymerBackboneElement } from './util/polymer/backbone';
+import { StructureGroup } from './util/common';
 
 export const PolymerBackboneSphereParams = {
     ...UnitsMeshParams,

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

@@ -14,7 +14,7 @@ import { createCurveSegmentState, PolymerTraceIterator, interpolateCurveSegment,
 import { isNucleic, SecondaryStructureType } from '../../../mol-model/structure/model/types';
 import { addSheet } from '../../../mol-geo/geometry/mesh/builder/sheet';
 import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { SecondaryStructureProvider } from '../../../mol-model-props/computed/secondary-structure';
 import { addRibbon } from '../../../mol-geo/geometry/mesh/builder/ribbon';
@@ -22,6 +22,7 @@ import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { StructureGroup } from './util/common';
 
 export const PolymerTraceMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),

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

@@ -8,13 +8,12 @@ import { BondType } from '../../../../mol-model/structure/model/types';
 import { Unit, StructureElement, Structure, Bond } from '../../../../mol-model/structure';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { LocationIterator } from '../../../../mol-geo/util/location-iterator';
-import { StructureGroup } from '../../units-visual';
 import { LinkCylinderParams, LinkLineParams } from './link';
 import { ObjectKeys } from '../../../../mol-util/type-helpers';
 import { PickingId } from '../../../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from '../../../../mol-model/loci';
 import { Interval, OrderedSet, SortedArray } from '../../../../mol-data/int';
-import { isH, isHydrogen } from './common';
+import { isH, isHydrogen, StructureGroup } from './common';
 
 export const BondParams = {
     includeTypes: PD.MultiSelect(ObjectKeys(BondType.Names), PD.objectToOptions(BondType.Names)),

+ 7 - 1
src/mol-repr/structure/visual/util/common.ts

@@ -71,7 +71,13 @@ export function getAltResidueLociFromId(structure: Structure, unit: Unit.Atomic,
 
 //
 
-export function createUnitsTransform({ units }: Unit.SymmetryGroup, transformData?: TransformData) {
+export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
+
+export function createUnitsTransform(structureGroup: StructureGroup, includeParent: boolean, transformData?: TransformData) {
+    const { child } = structureGroup.structure;
+    const units: ReadonlyArray<Unit> = includeParent && child
+        ? structureGroup.group.units.filter(u => child.unitMap.has(u.id))
+        : structureGroup.group.units;
     const unitCount = units.length;
     const n = unitCount * 16;
     const array = transformData && transformData.aTransform.ref.value.length >= n ? transformData.aTransform.ref.value : new Float32Array(n);

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

@@ -17,10 +17,9 @@ import { PickingId } from '../../../../mol-geo/geometry/picking';
 import { LocationIterator } from '../../../../mol-geo/util/location-iterator';
 import { VisualContext } from '../../../../mol-repr/visual';
 import { Theme } from '../../../../mol-theme/theme';
-import { StructureGroup } from '../../../../mol-repr/structure/units-visual';
 import { Spheres } from '../../../../mol-geo/geometry/spheres/spheres';
 import { SpheresBuilder } from '../../../../mol-geo/geometry/spheres/spheres-builder';
-import { isTrace, isH } from './common';
+import { isTrace, isH, StructureGroup } from './common';
 import { Sphere3D } from '../../../../mol-math/geometry';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)

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

@@ -9,8 +9,7 @@ import { Loci, EmptyLoci } from '../../../../mol-model/loci';
 import { Interval } from '../../../../mol-data/int';
 import { LocationIterator } from '../../../../mol-geo/util/location-iterator';
 import { PickingId } from '../../../../mol-geo/geometry/picking';
-import { StructureGroup } from '../../../../mol-repr/structure/units-visual';
-import { getResidueLoci } from './common';
+import { getResidueLoci, StructureGroup } from './common';
 import { eachAtomicUnitTracedElement } from './polymer';
 
 export namespace NucleotideLocationIterator {

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

@@ -11,8 +11,7 @@ import { OrderedSet, Interval, SortedArray } from '../../../../mol-data/int';
 import { EmptyLoci, Loci } from '../../../../mol-model/loci';
 import { LocationIterator } from '../../../../mol-geo/util/location-iterator';
 import { PickingId } from '../../../../mol-geo/geometry/picking';
-import { StructureGroup } from '../../../structure/units-visual';
-import { getResidueLoci } from './common';
+import { getResidueLoci, StructureGroup } from './common';
 
 export * from './polymer/backbone';
 export * from './polymer/gap-iterator';