Browse Source

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

David Sehnal 5 years ago
parent
commit
89be1767e9
31 changed files with 789 additions and 327 deletions
  1. 34 0
      src/mol-geo/geometry/mesh/builder/axes.ts
  2. 49 3
      src/mol-geo/geometry/mesh/builder/box.ts
  3. 47 38
      src/mol-geo/geometry/text/text-builder.ts
  4. 1 0
      src/mol-geo/geometry/text/text.ts
  5. 4 2
      src/mol-gl/renderer.ts
  6. 10 0
      src/mol-model/loci.ts
  7. 54 0
      src/mol-model/structure/structure/element/loci.ts
  8. 18 0
      src/mol-model/structure/structure/unit.ts
  9. 26 0
      src/mol-model/structure/structure/util/principal-axes.ts
  10. 1 1
      src/mol-plugin/behavior/dynamic/labels.ts
  11. 0 1
      src/mol-plugin/index.ts
  12. 1 1
      src/mol-plugin/state/representation/model.ts
  13. 0 29
      src/mol-plugin/state/transforms/representation.ts
  14. 1 1
      src/mol-plugin/ui/structure/selection.tsx
  15. 1 1
      src/mol-plugin/util/structure-labels.ts
  16. 0 165
      src/mol-plugin/util/structure-orientation.ts
  17. 34 1
      src/mol-plugin/util/structure-selection-helper.ts
  18. 2 2
      src/mol-repr/shape/loci/angle.ts
  19. 2 2
      src/mol-repr/shape/loci/dihedral.ts
  20. 2 2
      src/mol-repr/shape/loci/distance.ts
  21. 1 1
      src/mol-repr/shape/loci/label.ts
  22. 41 68
      src/mol-repr/shape/loci/orientation.ts
  23. 33 6
      src/mol-repr/structure/complex-visual.ts
  24. 4 0
      src/mol-repr/structure/registry.ts
  25. 5 1
      src/mol-repr/structure/representation.ts
  26. 43 0
      src/mol-repr/structure/representation/label.ts
  27. 44 0
      src/mol-repr/structure/representation/orientation.ts
  28. 25 1
      src/mol-repr/structure/units-visual.ts
  29. 176 0
      src/mol-repr/structure/visual/label-text.ts
  30. 129 0
      src/mol-repr/structure/visual/orientation-ellipsoid-mesh.ts
  31. 1 1
      src/tests/browser/render-text.ts

+ 34 - 0
src/mol-geo/geometry/mesh/builder/axes.ts

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3, Mat4 } from '../../../../mol-math/linear-algebra';
+import { MeshBuilder } from '../mesh-builder';
+import { Axes3D } from '../../../../mol-math/geometry';
+import { createCage } from '../../../primitive/cage';
+
+const tmpVec = Vec3()
+const tmpMatrix = Mat4.identity()
+
+const tmpVertices = new Float32Array(6 * 3)
+const tmpEdges = new Uint8Array([0, 1, 2, 3, 4, 5])
+
+export function addAxes(state: MeshBuilder.State, axes: Axes3D, radiusScale: number, detail: number, radialSegments: number) {
+    const { origin, dirA, dirB, dirC } = axes
+
+    Vec3.add(tmpVec, origin, dirA)
+    Vec3.toArray(Vec3.add(tmpVec, origin, dirA), tmpVertices, 0)
+    Vec3.toArray(Vec3.sub(tmpVec, origin, dirA), tmpVertices, 3)
+    Vec3.toArray(Vec3.add(tmpVec, origin, dirB), tmpVertices, 6)
+    Vec3.toArray(Vec3.sub(tmpVec, origin, dirB), tmpVertices, 9)
+    Vec3.toArray(Vec3.add(tmpVec, origin, dirC), tmpVertices, 12)
+    Vec3.toArray(Vec3.sub(tmpVec, origin, dirC), tmpVertices, 15)
+
+    const cage = createCage(tmpVertices, tmpEdges)
+    const volume = Axes3D.volume(axes)
+    const radius = (Math.cbrt(volume) / 300) * radiusScale
+
+    MeshBuilder.addCage(state, tmpMatrix, cage, radius, detail, radialSegments)
+}

+ 49 - 3
src/mol-geo/geometry/mesh/builder/bounding-box.ts → src/mol-geo/geometry/mesh/builder/box.ts

@@ -1,15 +1,16 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Vec3 } from '../../../../mol-math/linear-algebra';
-import { Box3D } from '../../../../mol-math/geometry';
+import { Vec3, Mat4 } from '../../../../mol-math/linear-algebra';
+import { Box3D, Axes3D } from '../../../../mol-math/geometry';
 import { MeshBuilder } from '../mesh-builder';
 import { CylinderProps } from '../../../primitive/cylinder';
 import { addCylinder } from './cylinder';
 import { addSphere } from './sphere';
+import { createCage } from '../../../primitive/cage';
 
 const tmpStart = Vec3.zero()
 const tmpEnd = Vec3.zero()
@@ -62,4 +63,49 @@ export function addBoundingBox(state: MeshBuilder.State, box: Box3D, radius: num
     Vec3.set(tmpEnd, min[0], max[1], max[2])
     addSphere(state, tmpEnd, radius, detail)
     addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
+}
+
+//
+
+const tmpBoxVecCorner = Vec3()
+const tmpBoxVecA = Vec3()
+const tmpBoxVecB = Vec3()
+const tmpBoxVecC = Vec3()
+const tmpMatrix = Mat4.identity()
+
+const tmpVertices = new Float32Array(8 * 3)
+const tmpEdges = new Uint8Array([
+    0, 1, 0, 3, 0, 6, 1, 2, 1, 7, 2, 3,
+    2, 4, 3, 5, 4, 5, 4, 7, 5, 6, 6, 7
+])
+
+export function addOrientedBox(state: MeshBuilder.State, axes: Axes3D, radiusScale: number, detail: number, radialSegments: number) {
+    const { origin, dirA, dirB, dirC } = axes
+    const negDirA = Vec3.negate(tmpBoxVecA, dirA)
+    const negDirB = Vec3.negate(tmpBoxVecB, dirB)
+    const negDirC = Vec3.negate(tmpBoxVecC, dirC)
+
+    let offset = 0
+    const addCornerHelper = function (v1: Vec3, v2: Vec3, v3: Vec3) {
+        Vec3.copy(tmpBoxVecCorner, origin)
+        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v1)
+        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v2)
+        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v3)
+        Vec3.toArray(tmpBoxVecCorner, tmpVertices, offset)
+        offset += 3
+    }
+    addCornerHelper(dirA, dirB, dirC)
+    addCornerHelper(dirA, dirB, negDirC)
+    addCornerHelper(dirA, negDirB, negDirC)
+    addCornerHelper(dirA, negDirB, dirC)
+    addCornerHelper(negDirA, negDirB, negDirC)
+    addCornerHelper(negDirA, negDirB, dirC)
+    addCornerHelper(negDirA, dirB, dirC)
+    addCornerHelper(negDirA, dirB, negDirC)
+
+    const cage = createCage(tmpVertices, tmpEdges)
+    const volume = Axes3D.volume(axes)
+    const radius = (Math.cbrt(volume) / 300) * radiusScale
+
+    MeshBuilder.addCage(state, tmpMatrix, cage, radius, detail, radialSegments)
 }

+ 47 - 38
src/mol-geo/geometry/text/text-builder.ts

@@ -16,7 +16,7 @@ const quadIndices = new Uint16Array([
 ])
 
 export interface TextBuilder {
-    add(str: string, x: number, y: number, z: number, depth: number, group: number): void
+    add(str: string, x: number, y: number, z: number, depth: number, scale: number, group: number): void
     getText(): Text
 }
 
@@ -45,7 +45,7 @@ export namespace TextBuilder {
         }
 
         return {
-            add: (str: string, x: number, y: number, z: number, depth: number, group: number) => {
+            add: (str: string, x: number, y: number, z: number, depth: number, scale: number, group: number) => {
                 let bWidth = 0
                 const nChar = str.length
 
@@ -111,10 +111,10 @@ export namespace TextBuilder {
                     }
                 }
 
-                const xLeft = -xShift - margin - 0.1
-                const xRight = bWidth - xShift + margin + 0.1
-                const yTop = bHeight - yShift + margin
-                const yBottom = -yShift - margin
+                const xLeft = (-xShift - margin - 0.1) * scale
+                const xRight = (bWidth - xShift + margin + 0.1) * scale
+                const yTop = (bHeight - yShift + margin) * scale
+                const yBottom = (-yShift - margin) * scale
 
                 // background
                 if (background) {
@@ -137,45 +137,49 @@ export namespace TextBuilder {
                     let xBaseA: number, yBaseA: number
                     let xBaseB: number, yBaseB: number
                     let xBaseCenter: number, yBaseCenter: number
+
+                    const scaledTetherLength = tetherLength * scale
+                    const scaledTetherBaseWidth = tetherBaseWidth * scale
+
                     switch (attachment) {
                         case 'bottom-left':
-                            xTip = xLeft - tetherLength / 2
-                            xBaseA = xLeft + tetherBaseWidth / 2
+                            xTip = xLeft - scaledTetherLength / 2
+                            xBaseA = xLeft + scaledTetherBaseWidth / 2
                             xBaseB = xLeft
                             xBaseCenter = xLeft
-                            yTip = yBottom - tetherLength / 2
+                            yTip = yBottom - scaledTetherLength / 2
                             yBaseA = yBottom
-                            yBaseB = yBottom + tetherBaseWidth / 2
+                            yBaseB = yBottom + scaledTetherBaseWidth / 2
                             yBaseCenter = yBottom
                             break
                         case 'bottom-center':
                             xTip = 0
-                            xBaseA = tetherBaseWidth / 2
-                            xBaseB = -tetherBaseWidth / 2
+                            xBaseA = scaledTetherBaseWidth / 2
+                            xBaseB = -scaledTetherBaseWidth / 2
                             xBaseCenter = 0
-                            yTip = yBottom - tetherLength
+                            yTip = yBottom - scaledTetherLength
                             yBaseA = yBottom
                             yBaseB = yBottom
                             yBaseCenter = yBottom
                             break
                         case 'bottom-right':
-                            xTip = xRight + tetherLength / 2
+                            xTip = xRight + scaledTetherLength / 2
                             xBaseA = xRight
-                            xBaseB = xRight - tetherBaseWidth / 2
+                            xBaseB = xRight - scaledTetherBaseWidth / 2
                             xBaseCenter = xRight
-                            yTip = yBottom - tetherLength / 2
-                            yBaseA = yBottom + tetherBaseWidth / 2
+                            yTip = yBottom - scaledTetherLength / 2
+                            yBaseA = yBottom + scaledTetherBaseWidth / 2
                             yBaseB = yBottom
                             yBaseCenter = yBottom
                             break
                         case 'middle-left':
-                            xTip = xLeft - tetherLength
+                            xTip = xLeft - scaledTetherLength
                             xBaseA = xLeft
                             xBaseB = xLeft
                             xBaseCenter = xLeft
                             yTip = 0
-                            yBaseA = -tetherBaseWidth / 2
-                            yBaseB = tetherBaseWidth / 2
+                            yBaseA = -scaledTetherBaseWidth / 2
+                            yBaseB = scaledTetherBaseWidth / 2
                             yBaseCenter = 0
                             break
                         case 'middle-center':
@@ -189,42 +193,42 @@ export namespace TextBuilder {
                             yBaseCenter = 0
                             break
                         case 'middle-right':
-                            xTip = xRight + tetherLength
+                            xTip = xRight + scaledTetherLength
                             xBaseA = xRight
                             xBaseB = xRight
                             xBaseCenter = xRight
                             yTip = 0
-                            yBaseA = tetherBaseWidth / 2
-                            yBaseB = -tetherBaseWidth / 2
+                            yBaseA = scaledTetherBaseWidth / 2
+                            yBaseB = -scaledTetherBaseWidth / 2
                             yBaseCenter = 0
                             break
                         case 'top-left':
-                            xTip = xLeft - tetherLength / 2
-                            xBaseA = xLeft + tetherBaseWidth / 2
+                            xTip = xLeft - scaledTetherLength / 2
+                            xBaseA = xLeft + scaledTetherBaseWidth / 2
                             xBaseB = xLeft
                             xBaseCenter = xLeft
-                            yTip = yTop + tetherLength / 2
+                            yTip = yTop + scaledTetherLength / 2
                             yBaseA = yTop
-                            yBaseB = yTop - tetherBaseWidth / 2
+                            yBaseB = yTop - scaledTetherBaseWidth / 2
                             yBaseCenter = yTop
                             break
                         case 'top-center':
                             xTip = 0
-                            xBaseA = tetherBaseWidth / 2
-                            xBaseB = -tetherBaseWidth / 2
+                            xBaseA = scaledTetherBaseWidth / 2
+                            xBaseB = -scaledTetherBaseWidth / 2
                             xBaseCenter = 0
-                            yTip = yTop + tetherLength
+                            yTip = yTop + scaledTetherLength
                             yBaseA = yTop
                             yBaseB = yTop
                             yBaseCenter = yTop
                             break
                         case 'top-right':
-                            xTip = xRight + tetherLength / 2
+                            xTip = xRight + scaledTetherLength / 2
                             xBaseA = xRight
-                            xBaseB = xRight - tetherBaseWidth / 2
+                            xBaseB = xRight - scaledTetherBaseWidth / 2
                             xBaseCenter = xRight
-                            yTip = yTop + tetherLength / 2
-                            yBaseA = yTop - tetherBaseWidth / 2
+                            yTip = yTop + scaledTetherLength / 2
+                            yBaseA = yTop - scaledTetherBaseWidth / 2
                             yBaseB = yTop
                             yBaseCenter = yTop
                             break
@@ -252,10 +256,15 @@ export namespace TextBuilder {
                 for (let iChar = 0; iChar < nChar; ++iChar) {
                     const c = fontAtlas.get(str[iChar])
 
-                    ChunkedArray.add2(mappings, xadvance - xShift, c.nh - yShift) // top left
-                    ChunkedArray.add2(mappings, xadvance - xShift, -yShift) // bottom left
-                    ChunkedArray.add2(mappings, xadvance + c.nw - xShift, c.nh - yShift) // top right
-                    ChunkedArray.add2(mappings, xadvance + c.nw - xShift, -yShift) // bottom right
+                    const left = (xadvance - xShift) * scale
+                    const right = (xadvance + c.nw - xShift) * scale
+                    const top = (c.nh - yShift) * scale
+                    const bottom = (-yShift) * scale
+
+                    ChunkedArray.add2(mappings, left, top)
+                    ChunkedArray.add2(mappings, left, bottom)
+                    ChunkedArray.add2(mappings, right, top)
+                    ChunkedArray.add2(mappings, right, bottom)
 
                     const texWidth = fontAtlas.texture.width
                     const texHeight = fontAtlas.texture.height

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

@@ -215,6 +215,7 @@ export namespace Text {
 
     function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
         BaseGeometry.updateRenderableState(state, props)
+        state.pickable = false
         state.opaque = false
     }
 }

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

@@ -211,8 +211,10 @@ namespace Renderer {
                 state.enable(gl.BLEND)
                 for (let i = 0, il = renderables.length; i < il; ++i) {
                     const r = renderables[i]
-                    state.depthMask(r.values.uAlpha.ref.value === 1.0)
-                    if (!r.state.opaque) renderObject(r, variant)
+                    if (!r.state.opaque) {
+                        state.depthMask(false)
+                        renderObject(r, variant)
+                    }
                 }
             } else { // picking & depth
                 for (let i = 0, il = renderables.length; i < il; ++i) {

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

@@ -194,6 +194,16 @@ namespace Loci {
                 ? StructureElement.Loci.extendToWholeChains(loci)
                 : loci
         },
+        'entity': (loci: Loci) => {
+            return StructureElement.Loci.is(loci)
+                ? StructureElement.Loci.extendToWholeEntities(loci)
+                : loci
+        },
+        'model': (loci: Loci) => {
+            return StructureElement.Loci.is(loci)
+                ? StructureElement.Loci.extendToWholeModels(loci)
+                : loci
+        },
         'structure': (loci: Loci) => {
             return StructureElement.Loci.is(loci)
                 ? Structure.toStructureElementLoci(loci.structure)

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

@@ -21,6 +21,7 @@ import { Location } from './location';
 import { ChainIndex } from '../../model/indexing';
 import { PrincipalAxes } from '../../../../mol-math/linear-algebra/matrix/principal-axes';
 import { NumberArray } from '../../../../mol-util/type-helpers';
+import StructureProperties from '../properties';
 
 /** Represents multiple structure element index locations */
 export interface Loci {
@@ -356,6 +357,59 @@ export namespace Loci {
         return Loci(loci.structure, elements);
     }
 
+    function entityModelKey(location: Location) {
+        return `${location.unit.model.id}|${StructureProperties.entity.id(location)}`
+    }
+
+    export function extendToWholeEntities(loci: Loci): Loci {
+        const elements: Loci['elements'][0][] = []
+        const l = Location.create()
+        const entities = new Set<string>()
+        const { units } = loci.structure
+
+        for (let i = 0, len = loci.elements.length; i < len; i++) {
+            const e = loci.elements[i]
+            l.unit = e.unit
+            l.element = e.unit.elements[0]
+            entities.add(entityModelKey(l))
+        }
+
+        for (let i = 0, il = units.length; i < il; ++i) {
+            const unit = units[i]
+            l.unit = unit
+            l.element = unit.elements[0]
+            if (entities.has(entityModelKey(l))) {
+                const indices = OrderedSet.ofBounds(0, unit.elements.length) as OrderedSet<UnitIndex>
+                elements[elements.length] = { unit, indices }
+            }
+        }
+
+        return Loci(loci.structure, elements)
+    }
+
+    export function extendToWholeModels(loci: Loci): Loci {
+        const elements: Loci['elements'][0][] = []
+        const models = new Set<string>()
+        const { units } = loci.structure
+
+        for (let i = 0, len = loci.elements.length; i < len; i++) {
+            const e = loci.elements[i]
+            models.add(e.unit.model.id)
+        }
+
+        for (let i = 0, il = units.length; i < il; ++i) {
+            const unit = units[i]
+            if (models.has(unit.model.id)) {
+                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();
     const tempPosBoundary = Vec3.zero();
     export function getBoundary(loci: Loci): Boundary {

+ 18 - 0
src/mol-model/structure/structure/unit.ts

@@ -18,6 +18,8 @@ import { IntMap, SortedArray, Segmentation } from '../../../mol-data/int';
 import { hash2, hashFnv32a } from '../../../mol-data/util';
 import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements, getNucleotideElements, getProteinElements } from './util/polymer';
 import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
+import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
+import { getPrincipalAxes } from './util/principal-axes';
 
 /**
  * A building block of a structure that corresponds to an atomic or
@@ -183,6 +185,12 @@ namespace Unit {
             return this.props.lookup3d.ref;
         }
 
+        get principalAxes() {
+            if (this.props.principalAxes.ref) return this.props.principalAxes.ref;
+            this.props.principalAxes.ref = getPrincipalAxes(this);
+            return this.props.principalAxes.ref;
+        }
+
         get links() {
             if (this.props.links.ref) return this.props.links.ref;
             this.props.links.ref = computeIntraUnitBonds(this);
@@ -254,6 +262,7 @@ namespace Unit {
 
     interface AtomicProperties {
         lookup3d: ValueRef<Lookup3D | undefined>,
+        principalAxes: ValueRef<PrincipalAxes | undefined>,
         links: ValueRef<IntraUnitLinks | undefined>,
         rings: ValueRef<UnitRings | undefined>
         polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
@@ -266,6 +275,7 @@ namespace Unit {
     function AtomicProperties(): AtomicProperties {
         return {
             lookup3d: ValueRef.create(void 0),
+            principalAxes: ValueRef.create(void 0),
             links: ValueRef.create(void 0),
             rings: ValueRef.create(void 0),
             polymerElements: ValueRef.create(void 0),
@@ -313,6 +323,12 @@ namespace Unit {
             return this.props.lookup3d.ref;
         }
 
+        get principalAxes() {
+            if (this.props.principalAxes.ref) return this.props.principalAxes.ref;
+            this.props.principalAxes.ref = getPrincipalAxes(this as Unit.Spheres | Unit.Gaussians); // TODO get rid of casting
+            return this.props.principalAxes.ref;
+        }
+
         get polymerElements() {
             if (this.props.polymerElements.ref) return this.props.polymerElements.ref;
             this.props.polymerElements.ref = getCoarsePolymerElements(this as Unit.Spheres | Unit.Gaussians); // TODO get rid of casting
@@ -347,6 +363,7 @@ namespace Unit {
 
     interface CoarseProperties {
         lookup3d: ValueRef<Lookup3D | undefined>,
+        principalAxes: ValueRef<PrincipalAxes | undefined>,
         polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
         gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
     }
@@ -354,6 +371,7 @@ namespace Unit {
     function CoarseProperties(): CoarseProperties {
         return {
             lookup3d: ValueRef.create(void 0),
+            principalAxes: ValueRef.create(void 0),
             polymerElements: ValueRef.create(void 0),
             gapElements: ValueRef.create(void 0),
         };

+ 26 - 0
src/mol-model/structure/structure/util/principal-axes.ts

@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PrincipalAxes } from '../../../../mol-math/linear-algebra/matrix/principal-axes';
+import Unit from '../unit';
+import { Vec3 } from '../../../../mol-math/linear-algebra';
+
+const tempPos = Vec3.zero();
+export function toPositionsArray(unit: Unit) {
+    const pos = unit.conformation.invariantPosition
+    const { elements } = unit
+    const positions = new Float32Array(elements.length * 3)
+    for (let i = 0, il = elements.length; i < il; i++) {
+        pos(elements[i], tempPos)
+        Vec3.toArray(tempPos, positions, i * 3)
+    }
+    return positions
+}
+
+export function getPrincipalAxes(unit: Unit): PrincipalAxes {
+    const positions = toPositionsArray(unit)
+    return PrincipalAxes.ofPositions(positions)
+}

+ 1 - 1
src/mol-plugin/behavior/dynamic/labels.ts

@@ -66,7 +66,7 @@ function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: T
     const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
     for (let i = 0, il = texts.length; i < il; ++i) {
         const p = positions[i]
-        textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
+        textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], 1, i)
     }
     return textBuilder.getText()
 }

+ 0 - 1
src/mol-plugin/index.ts

@@ -48,7 +48,6 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Model.StructureSelectionFromScript),
         PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.StructureLabels3D),
-        PluginSpec.Action(StateTransforms.Representation.StructureOrientation3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsDistance3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsAngle3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsDihedral3D),

+ 1 - 1
src/mol-plugin/state/representation/model.ts

@@ -24,7 +24,7 @@ export namespace ModelStructureRepresentation {
             deposited: PD.EmptyGroup(),
             assembly: PD.Group({
                 id: PD.Optional(model
-                    ? PD.Select(assemblyIds[0][0], assemblyIds, { label: 'Asm Id', description: 'Assembly Id' })
+                    ? PD.Select(assemblyIds.length ? assemblyIds[0][0] : '', assemblyIds, { label: 'Asm Id', description: 'Assembly Id' })
                     : PD.Text('', { label: 'Asm Id', description: 'Assembly Id (use empty for the 1st assembly)' }))
             }, { isFlat: true }),
             'symmetry-mates': PD.Group({

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

@@ -32,7 +32,6 @@ import { Transparency } from '../../../mol-theme/transparency';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
 import { Script } from '../../../mol-script/script';
 import { getUnitcellRepresentation, UnitcellParams } from '../../util/model-unitcell';
-import { getStructureOrientationRepresentation, OrientationParams as _OrientationParams } from '../../util/structure-orientation';
 import { DistanceParams, DistanceRepresentation } from '../../../mol-repr/shape/loci/distance';
 import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections } from './helpers';
 import { LabelParams, LabelRepresentation } from '../../../mol-repr/shape/loci/label';
@@ -707,34 +706,6 @@ const ModelUnitcell3D = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureOrientation3D }
-type StructureOrientation3D = typeof StructureOrientation3D
-const StructureOrientation3D = PluginStateTransform.BuiltIn({
-    name: 'structure-orientation-3d',
-    display: '3D Orientation Box',
-    from: SO.Molecule.Structure,
-    to: SO.Shape.Representation3D,
-    params: {
-        ..._OrientationParams,
-    }
-})({
-    canAutoUpdate({ oldParams, newParams }) {
-        return true;
-    },
-    apply({ a, params }) {
-        return Task.create('Structure Orientation', async ctx => {
-            const repr = await getStructureOrientationRepresentation(ctx, a.data, params);
-            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Orientation` });
-        });
-    },
-    update({ a, b, newParams }) {
-        return Task.create('Structure Orientation', async ctx => {
-            await getStructureOrientationRepresentation(ctx, a.data, newParams, b.data.repr as ShapeRepresentation<any, any, any>);
-            return StateTransformer.UpdateResult.Updated;
-        });
-    }
-});
-
 export { StructureSelectionsDistance3D }
 type StructureSelectionsDistance3D = typeof StructureSelectionsDistance3D
 const StructureSelectionsDistance3D = PluginStateTransform.BuiltIn({

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

@@ -17,7 +17,7 @@ import { StructureElement } from '../../../mol-model/structure';
 
 const SSQ = StructureSelectionQueries
 const DefaultQueries: (keyof typeof SSQ)[] = [
-    'all', 'polymer', 'trace', 'protein', 'nucleic', 'water', 'branched', 'ligand', 'nonStandardPolymer',
+    'all', 'polymer', 'trace', 'backbone', 'protein', 'nucleic', 'water', 'branched', 'ligand', 'nonStandardPolymer',
     'surroundings', 'complement', 'bonded'
 ]
 

+ 1 - 1
src/mol-plugin/util/structure-labels.ts

@@ -29,7 +29,7 @@ function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: T
     const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
     for (let i = 0, il = texts.length; i < il; ++i) {
         const p = positions[i]
-        textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
+        textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], 1, i)
     }
     return textBuilder.getText()
 }

+ 0 - 165
src/mol-plugin/util/structure-orientation.ts

@@ -1,165 +0,0 @@
-/**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Structure, StructureElement } from '../../mol-model/structure';
-import { ShapeRepresentation } from '../../mol-repr/shape/representation';
-import { Shape } from '../../mol-model/shape';
-import { ColorNames } from '../../mol-util/color/names';
-import { RuntimeContext } from '../../mol-task';
-import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
-import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
-import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
-import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
-import { createCage } from '../../mol-geo/primitive/cage';
-import { stringToWords } from '../../mol-util/string';
-import { structureElementStatsLabel } from '../../mol-theme/label';
-import { Axes3D } from '../../mol-math/geometry';
-
-const tmpMatrixPos = Vec3.zero()
-function getPositions(structure: Structure) {
-    const positions = new Float32Array(structure.elementCount * 3)
-    for (let i = 0, m = 0, il = structure.units.length; i < il; ++i) {
-        const unit = structure.units[i]
-        const { elements } = unit
-        const pos = unit.conformation.position
-        for (let j = 0, jl = elements.length; j < jl; ++j) {
-            pos(elements[j], tmpMatrixPos)
-            Vec3.toArray(tmpMatrixPos, positions, m + j * 3)
-        }
-        m += elements.length * 3
-    }
-    return positions
-}
-
-interface OrientationData {
-    label: string
-    principalAxes: PrincipalAxes
-}
-
-const OrientationVisuals = { 'principal-axes': '', 'oriented-box': '' }
-type OrientationVisualName = keyof typeof OrientationVisuals
-const OrientationVisualOptions = Object.keys(OrientationVisuals).map(name => [name, stringToWords(name)] as [OrientationVisualName, string])
-
-export const OrientationParams = {
-    ...Mesh.Params,
-    visuals: PD.MultiSelect<OrientationVisualName>(['oriented-box'], OrientationVisualOptions),
-    color: PD.Color(ColorNames.orange),
-    scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 })
-}
-export type OrientationParams = typeof OrientationParams
-export type OrientationProps = PD.Values<OrientationParams>
-
-enum VisualGroup {
-    PrincipalAxes = 1,
-    OrientedBox = 2
-}
-
-const tmpAxesVec = Vec3()
-function buildMomentsAxes(state: MeshBuilder.State, data: OrientationData, props: OrientationProps) {
-    const { origin, dirA, dirB, dirC } = data.principalAxes.momentsAxes
-
-    const vertices = new Float32Array(6 * 3)
-    const edges = new Uint8Array([0, 1, 2, 3, 4, 5])
-    Vec3.add(tmpAxesVec, origin, dirA)
-    Vec3.toArray(Vec3.add(tmpAxesVec, origin, dirA), vertices, 0)
-    Vec3.toArray(Vec3.sub(tmpAxesVec, origin, dirA), vertices, 3)
-    Vec3.toArray(Vec3.add(tmpAxesVec, origin, dirB), vertices, 6)
-    Vec3.toArray(Vec3.sub(tmpAxesVec, origin, dirB), vertices, 9)
-    Vec3.toArray(Vec3.add(tmpAxesVec, origin, dirC), vertices, 12)
-    Vec3.toArray(Vec3.sub(tmpAxesVec, origin, dirC), vertices, 15)
-
-    const matrix = Mat4.fromTranslation(Mat4(), Vec3.negate(Vec3(), origin))
-
-    const cage = createCage(vertices, edges)
-    const volume = Axes3D.volume(data.principalAxes.boxAxes)
-    const radius = (Math.cbrt(volume) / 300) * props.scale
-    state.currentGroup = VisualGroup.PrincipalAxes
-    MeshBuilder.addCage(state, matrix, cage, radius, 2, 20)
-}
-
-const tmpBoxVecCorner = Vec3()
-const tmpBoxVecA = Vec3()
-const tmpBoxVecB = Vec3()
-const tmpBoxVecC = Vec3()
-function buildOrientedBox(state: MeshBuilder.State, data: OrientationData, props: OrientationProps) {
-    const { origin, dirA, dirB, dirC } = data.principalAxes.boxAxes
-    const negDirA = Vec3.negate(tmpBoxVecA, dirA)
-    const negDirB = Vec3.negate(tmpBoxVecB, dirB)
-    const negDirC = Vec3.negate(tmpBoxVecC, dirC)
-
-    const vertices = new Float32Array(8 * 3)
-    const edges = new Uint8Array([
-        0, 1, 0, 3, 0, 6, 1, 2, 1, 7, 2, 3,
-        2, 4, 3, 5, 4, 5, 4, 7, 5, 6, 6, 7
-    ])
-
-    let offset = 0
-    const addCornerHelper = function (v1: Vec3, v2: Vec3, v3: Vec3) {
-        Vec3.copy(tmpBoxVecCorner, origin)
-        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v1)
-        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v2)
-        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v3)
-        Vec3.toArray(tmpBoxVecCorner, vertices, offset)
-        offset += 3
-    }
-    addCornerHelper(dirA, dirB, dirC)
-    addCornerHelper(dirA, dirB, negDirC)
-    addCornerHelper(dirA, negDirB, negDirC)
-    addCornerHelper(dirA, negDirB, dirC)
-    addCornerHelper(negDirA, negDirB, negDirC)
-    addCornerHelper(negDirA, negDirB, dirC)
-    addCornerHelper(negDirA, dirB, dirC)
-    addCornerHelper(negDirA, dirB, negDirC)
-
-    const matrix = Mat4.fromTranslation(Mat4(), Vec3.negate(Vec3(), origin))
-
-    const cage = createCage(vertices, edges)
-    const volume = Axes3D.volume(data.principalAxes.boxAxes)
-    const radius = (Math.cbrt(volume) / 300) * props.scale
-    state.currentGroup = VisualGroup.OrientedBox
-    MeshBuilder.addCage(state, matrix, cage, radius, 2, 20)
-}
-
-function getOrientationMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh) {
-    const state = MeshBuilder.createState(256, 128, mesh)
-
-    if (props.visuals.includes('principal-axes')) buildMomentsAxes(state, data, props)
-    if (props.visuals.includes('oriented-box')) buildOrientedBox(state, data, props)
-
-    return MeshBuilder.getMesh(state)
-}
-
-function getLabel(structure: Structure) {
-    const loci = Structure.toStructureElementLoci(structure)
-    const remappedLoci = StructureElement.Loci.remap(loci, structure.root)
-    return structureElementStatsLabel(StructureElement.Stats.ofLoci(remappedLoci), { countsOnly: true })
-}
-
-export async function getStructureOrientationRepresentation(ctx: RuntimeContext, structure: Structure, params: OrientationProps, prev?: ShapeRepresentation<OrientationData, Mesh, Mesh.Params>) {
-    const repr = prev || ShapeRepresentation(getOrientationShape, Mesh.Utils);
-    const label = getLabel(structure)
-    const principalAxes = PrincipalAxes.ofPositions(getPositions(structure))
-    const data: OrientationData = { label, principalAxes }
-    await repr.createOrUpdate(params, data).runInContext(ctx);
-    return repr;
-}
-
-function getOrientationLabel(data: OrientationData, groupId: number): string {
-    switch (groupId) {
-        case VisualGroup.PrincipalAxes: return 'Principal Axes'
-        case VisualGroup.OrientedBox: return 'Oriented Box'
-    }
-    return 'Unknown Orientation Visual'
-}
-
-function getOrientationShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
-    const geo = getOrientationMesh(data, props, shape && shape.geometry);
-    const getLabel = function (groupId: number ) {
-        return `${getOrientationLabel(data, groupId)} of ${data.label}`
-    }
-    return Shape.create('Principal Axes', data, geo, () => props.color, () => 1, getLabel)
-}

+ 34 - 1
src/mol-plugin/util/structure-selection-helper.ts

@@ -13,7 +13,7 @@ import { compile } from '../../mol-script/runtime/query/compiler';
 import { Loci } from '../../mol-model/loci';
 import { PluginContext } from '../context';
 import Expression from '../../mol-script/language/expression';
-import { LinkType } from '../../mol-model/structure/model/types';
+import { LinkType, ProteinBackboneAtoms, NucleicBackboneAtoms } from '../../mol-model/structure/model/types';
 import { StateTransforms } from '../state/transforms';
 
 export interface StructureSelectionQuery {
@@ -55,6 +55,38 @@ const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([
     ])
 ]))
 
+// TODO maybe pre-calculate atom properties like backbone/sidechain
+const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
+    MS.struct.combinator.merge([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'entity-test': MS.core.logic.and([
+                    MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
+                    MS.core.str.match([
+                        MS.re('(polypeptide|cyclic-pseudo-peptide)', 'i'),
+                        MS.ammp('entitySubtype')
+                    ])
+                ]),
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'atom-test': MS.core.set.has([MS.set(...Array.from(ProteinBackboneAtoms)), MS.ammp('label_atom_id')])
+            })
+        ]),
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'entity-test': MS.core.logic.and([
+                    MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
+                    MS.core.str.match([
+                        MS.re('(nucleotide|peptide nucleic acid)', 'i'),
+                        MS.ammp('entitySubtype')
+                    ])
+                ]),
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'atom-test': MS.core.set.has([MS.set(...Array.from(NucleicBackboneAtoms)), MS.ammp('label_atom_id')])
+            })
+        ])
+    ])
+]))
+
 const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
         'entity-test': MS.core.logic.and([
@@ -258,6 +290,7 @@ export const StructureSelectionQueries = {
     all,
     polymer,
     trace,
+    backbone,
     protein,
     nucleic,
     proteinOrNucleic,

+ 2 - 2
src/mol-repr/shape/loci/angle.ts

@@ -70,7 +70,7 @@ const AngleVisuals = {
     'vectors': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, VectorsParams>) => ShapeRepresentation(getVectorsShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'arc': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, ArcParams>) => ShapeRepresentation(getArcShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'sector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, SectorParams>) => ShapeRepresentation(getSectorShape, Mesh.Utils, { modifyProps: p => ({ ...p, alpha: p.sectorOpacity }) }),
-    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
+    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils),
 }
 type AngleVisualName = keyof typeof AngleVisuals
 const AngleVisualOptions = Object.keys(AngleVisuals).map(name => [name, stringToWords(name)] as [AngleVisualName, string])
@@ -234,7 +234,7 @@ function buildText(data: AngleData, props: AngleProps, text?: Text): Text {
 
         const angle = radToDeg(tmpState.angle).toFixed(2)
         const label = `${angle}\u00B0`
-        builder.add(label, tmpVec[0], tmpVec[1], tmpVec[2], 0.1, i)
+        builder.add(label, tmpVec[0], tmpVec[1], tmpVec[2], 0.1, 1, i)
     }
     return builder.getText()
 }

+ 2 - 2
src/mol-repr/shape/loci/dihedral.ts

@@ -76,7 +76,7 @@ const DihedralVisuals = {
     'extenders': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ExtendersParams>) => ShapeRepresentation(getExtendersShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'arc': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ArcParams>) => ShapeRepresentation(getArcShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'sector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, SectorParams>) => ShapeRepresentation(getSectorShape, Mesh.Utils, { modifyProps: p => ({ ...p, alpha: p.sectorOpacity }) }),
-    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
+    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils),
 }
 type DihedralVisualName = keyof typeof DihedralVisuals
 const DihedralVisualOptions = Object.keys(DihedralVisuals).map(name => [name, stringToWords(name)] as [DihedralVisualName, string])
@@ -296,7 +296,7 @@ function buildText(data: DihedralData, props: DihedralProps, text?: Text): Text
 
         const angle = radToDeg(tmpState.angle).toFixed(2)
         const label = `${angle}\u00B0`
-        builder.add(label, tmpVec[0], tmpVec[1], tmpVec[2], 0.1, i)
+        builder.add(label, tmpVec[0], tmpVec[1], tmpVec[2], 0.1, 1, i)
     }
     return builder.getText()
 }

+ 2 - 2
src/mol-repr/shape/loci/distance.ts

@@ -47,7 +47,7 @@ type TextParams = typeof TextParams
 
 const DistanceVisuals = {
     'lines': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DistanceData, LineParams>) => ShapeRepresentation(getLinesShape, Lines.Utils),
-    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DistanceData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
+    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DistanceData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils),
 }
 type DistanceVisualName = keyof typeof DistanceVisuals
 const DistanceVisualOptions = Object.keys(DistanceVisuals).map(name => [name, stringToWords(name)] as [DistanceVisualName, string])
@@ -121,7 +121,7 @@ function buildText(data: DistanceData, props: DistanceProps, text?: Text): Text
         setDistanceState(data.pairs[i], tmpState)
         const { center, distance } = tmpState
         const label = `${distance.toFixed(2)} ${props.unitLabel}`
-        builder.add(label, center[0], center[1], center[2], 1, i)
+        builder.add(label, center[0], center[1], center[2], 1, 1, i)
     }
     return builder.getText()
 }

+ 1 - 1
src/mol-repr/shape/loci/label.ts

@@ -55,7 +55,7 @@ function buildText(data: LabelData, props: LabelProps, text?: Text): Text {
         if (!sphere) continue
         const { center, radius } = sphere
         const label = d.label || lociLabel(d.loci, { hidePrefix: true, htmlStyling: false })
-        builder.add(label, center[0], center[1], center[2], radius, i)
+        builder.add(label, center[0], center[1], center[2], radius, 1, i)
     }
     return builder.getText()
 }

+ 41 - 68
src/mol-repr/shape/loci/orientation.ts

@@ -12,13 +12,15 @@ import { ColorNames } from '../../../mol-util/color/names';
 import { ShapeRepresentation } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation';
 import { Shape } from '../../../mol-model/shape';
-import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
-import { createCage } from '../../../mol-geo/primitive/cage';
-import { Axes3D } from '../../../mol-math/geometry';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
 import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
 import { lociLabel } from '../../../mol-theme/label';
+import { addAxes } from '../../../mol-geo/geometry/mesh/builder/axes';
+import { addOrientedBox } from '../../../mol-geo/geometry/mesh/builder/box';
+import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
+import { Axes3D } from '../../../mol-math/geometry';
+import { Vec3 } from '../../../mol-math/linear-algebra';
 
 export interface OrientationData {
     loci: Loci
@@ -26,7 +28,7 @@ export interface OrientationData {
 
 const SharedParams = {
     color: PD.Color(ColorNames.orange),
-    scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 })
+    scale: PD.Numeric(2, { min: 0.1, max: 10, step: 0.1 })
 }
 
 const AxesParams = {
@@ -41,9 +43,16 @@ const BoxParams = {
 }
 type BoxParams = typeof BoxParams
 
+const EllipsoidParams = {
+    ...Mesh.Params,
+    ...SharedParams
+}
+type EllipsoidParams = typeof EllipsoidParams
+
 const OrientationVisuals = {
     'axes': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<OrientationData, AxesParams>) => ShapeRepresentation(getAxesShape, Mesh.Utils),
     'box': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<OrientationData, BoxParams>) => ShapeRepresentation(getBoxShape, Mesh.Utils),
+    'ellipsoid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<OrientationData, EllipsoidParams>) => ShapeRepresentation(getEllipsoidShape, Mesh.Utils),
 }
 type OrientationVisualName = keyof typeof OrientationVisuals
 const OrientationVisualOptions = Object.keys(OrientationVisuals).map(name => [name, stringToWords(name)] as [OrientationVisualName, string])
@@ -60,31 +69,10 @@ export type OrientationProps = PD.Values<OrientationParams>
 
 //
 
-const tmpAxesVec = Vec3()
-
 function buildAxesMesh(principalAxes: PrincipalAxes, props: OrientationProps, mesh?: Mesh): Mesh {
     const state = MeshBuilder.createState(256, 128, mesh)
-
-    const { origin, dirA, dirB, dirC } = principalAxes.momentsAxes
-
-    const vertices = new Float32Array(6 * 3)
-    const edges = new Uint8Array([0, 1, 2, 3, 4, 5])
-    Vec3.add(tmpAxesVec, origin, dirA)
-    Vec3.toArray(Vec3.add(tmpAxesVec, origin, dirA), vertices, 0)
-    Vec3.toArray(Vec3.sub(tmpAxesVec, origin, dirA), vertices, 3)
-    Vec3.toArray(Vec3.add(tmpAxesVec, origin, dirB), vertices, 6)
-    Vec3.toArray(Vec3.sub(tmpAxesVec, origin, dirB), vertices, 9)
-    Vec3.toArray(Vec3.add(tmpAxesVec, origin, dirC), vertices, 12)
-    Vec3.toArray(Vec3.sub(tmpAxesVec, origin, dirC), vertices, 15)
-
-    const matrix = Mat4.identity()
-
-    const cage = createCage(vertices, edges)
-    const volume = Axes3D.volume(principalAxes.boxAxes)
-    const radius = (Math.cbrt(volume) / 300) * props.scale
     state.currentGroup = 1
-    MeshBuilder.addCage(state, matrix, cage, radius, 2, 20)
-
+    addAxes(state, principalAxes.momentsAxes, props.scale, 2, 20)
     return MeshBuilder.getMesh(state)
 }
 
@@ -100,51 +88,10 @@ function getAxesShape(ctx: RuntimeContext, data: OrientationData, props: Orienta
 
 //
 
-const tmpBoxVecCorner = Vec3()
-const tmpBoxVecA = Vec3()
-const tmpBoxVecB = Vec3()
-const tmpBoxVecC = Vec3()
-
 function buildBoxMesh(principalAxes: PrincipalAxes, props: OrientationProps, mesh?: Mesh): Mesh {
     const state = MeshBuilder.createState(256, 128, mesh)
-
-    const { origin, dirA, dirB, dirC } = principalAxes.boxAxes
-    const negDirA = Vec3.negate(tmpBoxVecA, dirA)
-    const negDirB = Vec3.negate(tmpBoxVecB, dirB)
-    const negDirC = Vec3.negate(tmpBoxVecC, dirC)
-
-    const vertices = new Float32Array(8 * 3)
-    const edges = new Uint8Array([
-        0, 1, 0, 3, 0, 6, 1, 2, 1, 7, 2, 3,
-        2, 4, 3, 5, 4, 5, 4, 7, 5, 6, 6, 7
-    ])
-
-    let offset = 0
-    const addCornerHelper = function (v1: Vec3, v2: Vec3, v3: Vec3) {
-        Vec3.copy(tmpBoxVecCorner, origin)
-        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v1)
-        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v2)
-        Vec3.add(tmpBoxVecCorner, tmpBoxVecCorner, v3)
-        Vec3.toArray(tmpBoxVecCorner, vertices, offset)
-        offset += 3
-    }
-    addCornerHelper(dirA, dirB, dirC)
-    addCornerHelper(dirA, dirB, negDirC)
-    addCornerHelper(dirA, negDirB, negDirC)
-    addCornerHelper(dirA, negDirB, dirC)
-    addCornerHelper(negDirA, negDirB, negDirC)
-    addCornerHelper(negDirA, negDirB, dirC)
-    addCornerHelper(negDirA, dirB, dirC)
-    addCornerHelper(negDirA, dirB, negDirC)
-
-    const matrix = Mat4.identity()
-
-    const cage = createCage(vertices, edges)
-    const volume = Axes3D.volume(principalAxes.boxAxes)
-    const radius = (Math.cbrt(volume) / 300) * props.scale
     state.currentGroup = 1
-    MeshBuilder.addCage(state, matrix, cage, radius, 2, 20)
-
+    addOrientedBox(state, principalAxes.boxAxes, props.scale, 2, 20)
     return MeshBuilder.getMesh(state)
 }
 
@@ -160,6 +107,32 @@ function getBoxShape(ctx: RuntimeContext, data: OrientationData, props: Orientat
 
 //
 
+function buildEllipsoidMesh(principalAxes: PrincipalAxes, props: OrientationProps, mesh?: Mesh): Mesh {
+    const state = MeshBuilder.createState(256, 128, mesh)
+
+    const axes = principalAxes.boxAxes
+    const { origin, dirA, dirB } = axes
+    const size = Axes3D.size(Vec3(), axes)
+    Vec3.scale(size, size, 0.5)
+    const radiusScale = Vec3.create(size[2], size[1], size[0])
+
+    state.currentGroup = 1
+    addEllipsoid(state, origin, dirA, dirB, radiusScale, 2)
+    return MeshBuilder.getMesh(state)
+}
+
+function getEllipsoidShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
+    const label = lociLabel(data.loci, { countsOnly: true })
+    const principalAxes = Loci.getPrincipalAxes(data.loci)
+    const mesh = principalAxes ? buildEllipsoidMesh(principalAxes, props, shape && shape.geometry) : Mesh.createEmpty(shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return `Ellipsoid of ${label}`
+    }
+    return Shape.create('Ellipsoid', data, mesh, () => props.color, () => 1, getLabel)
+}
+
+//
+
 export type OrientationRepresentation = Representation<OrientationData, OrientationParams>
 export function OrientationRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<OrientationData, OrientationParams>): OrientationRepresentation {
     return Representation.createMulti('Orientation', ctx, getParams, Representation.StateBuilder, OrientationVisuals as unknown as Representation.Def<OrientationData, OrientationParams>)

+ 33 - 6
src/mol-repr/structure/complex-visual.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { StructureParams, StructureMeshParams, StructureDirectVolumeParams } from './representation';
+import { StructureParams, StructureMeshParams, StructureDirectVolumeParams, StructureTextParams } from './representation';
 import { Visual, VisualContext } from '../visual';
 import { Structure, StructureElement } from '../../mol-model/structure';
 import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
@@ -18,7 +18,6 @@ import { PickingId } from '../../mol-geo/geometry/picking';
 import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
 import { Interval } from '../../mol-data/int';
 import { VisualUpdateState } from '../util';
-import { UnitsParams } from './units-representation';
 import { ColorTheme } from '../../mol-theme/color';
 import { ValueCell, deepEqual } from '../../mol-util';
 import { createSizes } from '../../mol-geo/geometry/size-data';
@@ -28,6 +27,7 @@ import { Mat4 } from '../../mol-math/linear-algebra';
 import { Overpaint } from '../../mol-theme/overpaint';
 import { Transparency } from '../../mol-theme/transparency';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
+import { Text } from '../../mol-geo/geometry/text/text';
 import { SizeTheme } from '../../mol-theme/size';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 
@@ -56,7 +56,7 @@ interface ComplexVisualBuilder<P extends ComplexParams, G extends Geometry> {
     setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
 }
 
-interface ComplexVisualGeometryBuilder<P extends UnitsParams, G extends Geometry> extends ComplexVisualBuilder<P, G> {
+interface ComplexVisualGeometryBuilder<P extends ComplexParams, G extends Geometry> extends ComplexVisualBuilder<P, G> {
     geometryUtils: GeometryUtils<G>
 }
 
@@ -235,16 +235,43 @@ export type ComplexMeshParams = typeof ComplexMeshParams
 export interface ComplexMeshVisualBuilder<P extends ComplexMeshParams> extends ComplexVisualBuilder<P, Mesh> { }
 
 export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexMeshVisualBuilder<P>, materialId: number): ComplexVisual<P> {
-    return ComplexVisual<Mesh, StructureMeshParams & UnitsParams>({
+    return ComplexVisual<Mesh, StructureMeshParams & ComplexParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
-            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
         geometryUtils: Mesh.Utils
     }, materialId)
 }
 
+// text
+
+export const ComplexTextParams = {
+    ...StructureTextParams,
+    unitKinds: PD.MultiSelect<UnitKind>([ 'atomic', 'spheres' ], UnitKindOptions),
+}
+export type ComplexTextParams = typeof ComplexTextParams
+
+export interface ComplexTextVisualBuilder<P extends ComplexTextParams> extends ComplexVisualBuilder<P, Text> { }
+
+export function ComplexTextVisual<P extends ComplexTextParams>(builder: ComplexTextVisualBuilder<P>, materialId: number): ComplexVisual<P> {
+    return ComplexVisual<Text, StructureTextParams & ComplexParams>({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
+            builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
+            if (newProps.background !== currentProps.background) state.createGeometry = true
+            if (newProps.backgroundMargin !== currentProps.backgroundMargin) state.createGeometry = true
+            if (newProps.tether !== currentProps.tether) state.createGeometry = true
+            if (newProps.tetherLength !== currentProps.tetherLength) state.createGeometry = true
+            if (newProps.tetherBaseWidth !== currentProps.tetherBaseWidth) state.createGeometry = true
+            if (newProps.attachment !== currentProps.attachment) state.createGeometry = true
+        },
+        geometryUtils: Text.Utils
+    }, materialId)
+}
+
 // direct-volume
 
 export const ComplexDirectVolumeParams = {
@@ -256,7 +283,7 @@ export type ComplexDirectVolumeParams = typeof ComplexDirectVolumeParams
 export interface ComplexDirectVolumeVisualBuilder<P extends ComplexDirectVolumeParams> extends ComplexVisualBuilder<P, DirectVolume> { }
 
 export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(builder: ComplexDirectVolumeVisualBuilder<P>, materialId: number): ComplexVisual<P> {
-    return ComplexVisual<DirectVolume, StructureDirectVolumeParams & UnitsParams>({
+    return ComplexVisual<DirectVolume, StructureDirectVolumeParams & ComplexParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)

+ 4 - 0
src/mol-repr/structure/registry.ts

@@ -17,6 +17,8 @@ import { StructureRepresentationState } from './representation';
 import { PuttyRepresentationProvider } from './representation/putty';
 import { MolecularSurfaceRepresentationProvider } from './representation/molecular-surface';
 import { EllipsoidRepresentationProvider } from './representation/ellipsoid';
+import { OrientationRepresentationProvider } from './representation/orientation';
+import { LabelRepresentationProvider } from './representation/label';
 
 export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> {
     constructor() {
@@ -36,7 +38,9 @@ export const BuiltInStructureRepresentations = {
     'ellipsoid': EllipsoidRepresentationProvider,
     'gaussian-surface': GaussianSurfaceRepresentationProvider,
     // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work
+    'label': LabelRepresentationProvider,
     'molecular-surface': MolecularSurfaceRepresentationProvider,
+    'orientation': OrientationRepresentationProvider,
     'point': PointRepresentationProvider,
     'putty': PuttyRepresentationProvider,
     'spacefill': SpacefillRepresentationProvider,

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 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>
@@ -16,6 +16,7 @@ import { Points } from '../../mol-geo/geometry/points/points';
 import { Lines } from '../../mol-geo/geometry/lines/lines';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
+import { Text } from '../../mol-geo/geometry/text/text';
 
 export interface StructureRepresentationState extends Representation.State {
     unitTransforms: StructureUnitTransforms | null
@@ -54,6 +55,9 @@ export type StructurePointsParams = typeof StructurePointsParams
 export const StructureLinesParams = { ...Lines.Params, ...StructureParams }
 export type StructureLinesParams = typeof StructureLinesParams
 
+export const StructureTextParams = { ...Text.Params, ...StructureParams }
+export type StructureTextParams = typeof StructureTextParams
+
 export const StructureDirectVolumeParams = { ...DirectVolume.Params, ...StructureParams }
 export type StructureDirectVolumeParams = typeof StructureDirectVolumeParams
 

+ 43 - 0
src/mol-repr/structure/representation/label.ts

@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, ComplexRepresentation } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../../mol-repr/representation';
+import { ThemeRegistryContext } from '../../../mol-theme/theme';
+import { Structure } from '../../../mol-model/structure';
+import { LabelTextVisual, LabelTextParams } from '../visual/label-text';
+
+const LabelVisuals = {
+    'label-text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, LabelTextParams>) => ComplexRepresentation('Label text', ctx, getParams, LabelTextVisual),
+}
+type LabelVisualName = keyof typeof LabelVisuals
+const LabelVisualOptions = Object.keys(LabelVisuals).map(name => [name, name] as [LabelVisualName, string])
+
+export const LabelParams = {
+    ...LabelTextParams,
+    visuals: PD.MultiSelect<LabelVisualName>(['label-text'], LabelVisualOptions),
+}
+export type LabelParams = typeof LabelParams
+export function getLabelParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(LabelParams)
+}
+
+export type LabelRepresentation = StructureRepresentation<LabelParams>
+export function LabelRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, LabelParams>): LabelRepresentation {
+    return Representation.createMulti('Label', ctx, getParams, StructureRepresentationStateBuilder, LabelVisuals as unknown as Representation.Def<Structure, LabelParams>)
+}
+
+export const LabelRepresentationProvider: StructureRepresentationProvider<LabelParams> = {
+    label: 'Label',
+    description: 'Displays labels.',
+    factory: LabelRepresentation,
+    getParams: getLabelParams,
+    defaultValues: PD.getDefaultValues(LabelParams),
+    defaultColorTheme: 'uniform',
+    defaultSizeTheme: 'uniform',
+    isApplicable: (structure: Structure) => structure.elementCount > 0
+}

+ 44 - 0
src/mol-repr/structure/representation/orientation.ts

@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { UnitsRepresentation } from '../units-representation';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../../mol-repr/representation';
+import { ThemeRegistryContext } from '../../../mol-theme/theme';
+import { Structure } from '../../../mol-model/structure';
+import { OrientationEllipsoidMeshParams, OrientationEllipsoidMeshVisual } from '../visual/orientation-ellipsoid-mesh';
+
+const OrientationVisuals = {
+    'orientation-ellipsoid-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, OrientationEllipsoidMeshParams>) => UnitsRepresentation('Orientation ellipsoid mesh', ctx, getParams, OrientationEllipsoidMeshVisual),
+}
+type OrientationVisualName = keyof typeof OrientationVisuals
+const OrientationVisualOptions = Object.keys(OrientationVisuals).map(name => [name, name] as [OrientationVisualName, string])
+
+export const OrientationParams = {
+    ...OrientationEllipsoidMeshParams,
+    visuals: PD.MultiSelect<OrientationVisualName>(['orientation-ellipsoid-mesh'], OrientationVisualOptions),
+}
+export type OrientationParams = typeof OrientationParams
+export function getOrientationParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(OrientationParams)
+}
+
+export type OrientationRepresentation = StructureRepresentation<OrientationParams>
+export function OrientationRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, OrientationParams>): OrientationRepresentation {
+    return Representation.createMulti('Orientation', ctx, getParams, StructureRepresentationStateBuilder, OrientationVisuals as unknown as Representation.Def<Structure, OrientationParams>)
+}
+
+export const OrientationRepresentationProvider: StructureRepresentationProvider<OrientationParams> = {
+    label: 'Orientation',
+    description: 'Displays orientation ellipsoids for polymer chains.',
+    factory: OrientationRepresentation,
+    getParams: getOrientationParams,
+    defaultValues: PD.getDefaultValues(OrientationParams),
+    defaultColorTheme: 'polymer-id',
+    defaultSizeTheme: 'uniform',
+    isApplicable: (structure: Structure) => structure.elementCount > 0
+}

+ 25 - 1
src/mol-repr/structure/units-visual.ts

@@ -27,12 +27,13 @@ import { createColors } from '../../mol-geo/geometry/color-data';
 import { Mat4 } from '../../mol-math/linear-algebra';
 import { Overpaint } from '../../mol-theme/overpaint';
 import { Transparency } from '../../mol-theme/transparency';
-import { StructureMeshParams, StructureSpheresParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureTextureMeshParams } from './representation';
+import { StructureMeshParams, StructureSpheresParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureTextureMeshParams, StructureTextParams } from './representation';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { SizeTheme } from '../../mol-theme/size';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
 import { Points } from '../../mol-geo/geometry/points/points';
 import { Lines } from '../../mol-geo/geometry/lines/lines';
+import { Text } from '../../mol-geo/geometry/text/text';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 
@@ -342,6 +343,29 @@ export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLines
     }, materialId)
 }
 
+// text
+
+export const UnitsTextParams = { ...StructureTextParams, ...UnitsParams }
+export type UnitsTextParams = typeof UnitsTextParams
+export interface UnitsTextVisualBuilder<P extends UnitsTextParams> extends UnitsVisualBuilder<P, Text> { }
+
+export function UnitsTextVisual<P extends UnitsTextParams>(builder: UnitsTextVisualBuilder<P>, materialId: number): UnitsVisual<P> {
+    return UnitsVisual<Text, StructureTextParams & UnitsParams>({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
+            builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme, newStructureGroup, currentStructureGroup)
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
+            if (newProps.background !== currentProps.background) state.createGeometry = true
+            if (newProps.backgroundMargin !== currentProps.backgroundMargin) state.createGeometry = true
+            if (newProps.tether !== currentProps.tether) state.createGeometry = true
+            if (newProps.tetherLength !== currentProps.tetherLength) state.createGeometry = true
+            if (newProps.tetherBaseWidth !== currentProps.tetherBaseWidth) state.createGeometry = true
+            if (newProps.attachment !== currentProps.attachment) state.createGeometry = true
+        },
+        geometryUtils: Text.Utils
+    }, materialId)
+}
+
 // direct-volume
 
 export const UnitsDirectVolumeParams = { ...StructureDirectVolumeParams, ...UnitsParams }

+ 176 - 0
src/mol-repr/structure/visual/label-text.ts

@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { VisualUpdateState } from '../../../mol-repr/util';
+import { VisualContext } from '../../../mol-repr/visual';
+import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
+import { Theme } from '../../../mol-theme/theme';
+import { Text } from '../../../mol-geo/geometry/text/text';
+import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
+import { ComplexTextVisual, ComplexTextParams, ComplexVisual } from '../complex-visual';
+import { ElementIterator, getSerialElementLoci, eachSerialElement } from './util/element';
+import { ColorNames } from '../../../mol-util/color/names';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { PhysicalSizeTheme } from '../../../mol-theme/size/physical';
+import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
+
+export const LabelTextParams = {
+    ...ComplexTextParams,
+    background: PD.Boolean(true),
+    backgroundMargin: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
+    backgroundColor: PD.Color(ColorNames.black),
+    backgroundOpacity: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
+    level: PD.Select('residue', [['chain', 'Chain'], ['residue', 'Residue'], ['element', 'Element']]),
+    chainScale: PD.Numeric(10, { min: 0, max: 20, step: 0.1 }),
+    residueScale: PD.Numeric(1, { min: 0, max: 20, step: 0.1 }),
+    elementScale: PD.Numeric(0.5, { min: 0, max: 20, step: 0.1 }),
+}
+export type LabelTextParams = typeof LabelTextParams
+export type LabelTextProps = PD.Values<LabelTextParams>
+export type LabelLevels = LabelTextProps['level']
+
+export function LabelTextVisual(materialId: number): ComplexVisual<LabelTextParams> {
+    return ComplexTextVisual<LabelTextParams>({
+        defaultProps: PD.getDefaultValues(LabelTextParams),
+        createGeometry: createLabelText,
+        createLocationIterator: ElementIterator.fromStructure,
+        getLoci: getSerialElementLoci,
+        eachLocation: eachSerialElement,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<LabelTextParams>, currentProps: PD.Values<LabelTextParams>) => {
+            state.createGeometry = (
+                newProps.level !== currentProps.level ||
+                (newProps.level === 'chain' && newProps.chainScale !== currentProps.chainScale) ||
+                (newProps.level === 'residue' && newProps.residueScale !== currentProps.residueScale) ||
+                (newProps.level === 'element' && newProps.elementScale !== currentProps.elementScale)
+            )
+        }
+    }, materialId)
+}
+
+function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme, props: LabelTextProps, text?: Text): Text {
+
+    switch (props.level) {
+        case 'chain': return createChainText(ctx, structure, theme, props, text)
+        case 'residue': return createResidueText(ctx, structure, theme, props, text)
+        case 'element': return createElementText(ctx, structure, theme, props, text)
+    }
+}
+
+//
+
+const tmpVec = Vec3();
+const boundaryHelper = new BoundaryHelper();
+
+function createChainText(ctx: VisualContext, structure: Structure, theme: Theme, props: LabelTextProps, text?: Text): Text {
+
+    const l = StructureElement.Location.create();
+    const { units, serialMapping } = structure;
+    const { auth_asym_id, label_asym_id } = StructureProperties.chain;
+    const { unitElementCount } = serialMapping
+
+    const count = units.length
+    const { chainScale } = props
+    const builder = TextBuilder.create(props, count, count / 2, text)
+
+    for (let i = 0, il = units.length; i < il; ++i) {
+        const unit = units[i]
+        l.unit = unit
+        l.element = unit.elements[0]
+        const { center, radius } = unit.lookup3d.boundary.sphere
+        Vec3.transformMat4(tmpVec, center, unit.conformation.operator.matrix)
+        const authId = auth_asym_id(l)
+        const labelId = label_asym_id(l)
+        const text = authId === labelId ? labelId : `${labelId} [${authId}]`
+        builder.add(text, tmpVec[0], tmpVec[1], tmpVec[2], radius, chainScale, unitElementCount[i])
+    }
+
+    return builder.getText()
+}
+
+function createResidueText(ctx: VisualContext, structure: Structure, theme: Theme, props: LabelTextProps, text?: Text): Text {
+
+    const l = StructureElement.Location.create();
+    const { units, serialMapping } = structure;
+    const { auth_seq_id, label_comp_id } = StructureProperties.residue;
+    const { unitElementCount } = serialMapping
+
+    const count = structure.polymerResidueCount * 2
+    const { residueScale } = props
+    const builder = TextBuilder.create(props, count, count / 2, text)
+
+    for (let i = 0, il = units.length; i < il; ++i) {
+        const unit = units[i]
+        const pos = unit.conformation.position;
+        const { elements } = unit
+        l.unit = unit
+        l.element = unit.elements[0]
+
+        const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index;
+
+        let j = 0, jl = elements.length;
+        while (j < jl) {
+            const start = j, rI = residueIndex[elements[j]];
+            j++;
+            while (j < jl && residueIndex[elements[j]] === rI) j++;
+
+            boundaryHelper.reset(0);
+            for (let eI = start; eI < j; eI++) {
+                pos(elements[eI], tmpVec);
+                boundaryHelper.boundaryStep(tmpVec, 0);
+            }
+            boundaryHelper.finishBoundaryStep();
+            for (let eI = start; eI < j; eI++) {
+                pos(elements[eI], tmpVec);
+                boundaryHelper.extendStep(tmpVec, 0);
+            }
+
+            l.element = elements[start];
+
+            const { center, radius } = boundaryHelper
+            const authSeqId = auth_seq_id(l)
+            const compId = label_comp_id(l)
+
+            const text = `${compId} ${authSeqId}`
+            builder.add(text, center[0], center[1], center[2], radius, residueScale, unitElementCount[i])
+        }
+    }
+
+    return builder.getText()
+}
+
+function createElementText(ctx: VisualContext, structure: Structure, theme: Theme, props: LabelTextProps, text?: Text): Text {
+
+    const l = StructureElement.Location.create();
+    const { units, serialMapping } = structure;
+    const { label_atom_id, label_alt_id } = StructureProperties.atom;
+    const { unitElementCount } = serialMapping
+
+    const sizeTheme = PhysicalSizeTheme({}, {})
+
+    const count = structure.elementCount
+    const { elementScale } = props
+    const builder = TextBuilder.create(props, count, count / 2, text)
+
+    for (let i = 0, il = units.length; i < il; ++i) {
+        const unit = units[i]
+        const pos = unit.conformation.position;
+        const { elements } = unit
+        l.unit = unit
+
+        for (let j = 0, _j = elements.length; j < _j; j++) {
+            l.element = elements[j];
+            pos(l.element, tmpVec);
+            const atomId = label_atom_id(l)
+            const altId = label_alt_id(l)
+            const text = altId ? `${atomId}%${altId}` : atomId
+            builder.add(text, tmpVec[0], tmpVec[1], tmpVec[2], sizeTheme.size(l), elementScale, unitElementCount[i])
+        }
+    }
+
+    return builder.getText()
+}

+ 129 - 0
src/mol-repr/structure/visual/orientation-ellipsoid-mesh.ts

@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } 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';
+import { Theme } from '../../../mol-theme/theme';
+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 { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
+import { Axes3D } from '../../../mol-math/geometry';
+import { PickingId } from '../../../mol-geo/geometry/picking';
+import { OrderedSet, Interval } from '../../../mol-data/int';
+import { EmptyLoci, Loci } from '../../../mol-model/loci';
+import { UnitIndex } from '../../../mol-model/structure/structure/element/element';
+import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+import { MoleculeType } from '../../../mol-model/structure/model/types';
+
+export const OrientationEllipsoidMeshParams = {
+    ...UnitsMeshParams,
+    sizeFactor: PD.Numeric(1, { min: 0, max: 2, step: 0.1 }),
+    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
+}
+export type OrientationEllipsoidMeshParams = typeof OrientationEllipsoidMeshParams
+
+export function OrientationEllipsoidMeshVisual(materialId: number): UnitsVisual<OrientationEllipsoidMeshParams> {
+    return UnitsMeshVisual<OrientationEllipsoidMeshParams>({
+        defaultProps: PD.getDefaultValues(OrientationEllipsoidMeshParams),
+        createGeometry: createOrientationEllipsoidMesh,
+        createLocationIterator: UnitIterator,
+        getLoci: getUnitLoci,
+        eachLocation: eachUnit,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<OrientationEllipsoidMeshParams>, currentProps: PD.Values<OrientationEllipsoidMeshParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.detail !== currentProps.detail
+            )
+        }
+    }, materialId)
+}
+
+//
+
+export interface OrientationEllipsoidMeshProps {
+    detail: number,
+    sizeFactor: number,
+}
+
+function isUnitApplicable(unit: Unit) {
+    if (Unit.Traits.is(unit.traits, Unit.Trait.MultiChain)) return false
+    if (Unit.Traits.is(unit.traits, Unit.Trait.Patitioned)) return false
+    if (Unit.isCoarse(unit)) return true
+    if (unit.elements.length === 0) return false
+    unit.model.atomicHierarchy.derived.residue.moleculeType
+    const rI = unit.residueIndex[unit.elements[0]]
+    const mt = unit.model.atomicHierarchy.derived.residue.moleculeType[rI]
+    if (mt === MoleculeType.Ion) return false
+    if (mt === MoleculeType.Water) return false
+    return true
+}
+
+export function createOrientationEllipsoidMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: OrientationEllipsoidMeshProps, mesh?: Mesh): Mesh {
+    if (!isUnitApplicable(unit)) return Mesh.createEmpty(mesh)
+
+    const { detail, sizeFactor } = props
+
+    const vertexCount = 256
+    const builderState = MeshBuilder.createState(vertexCount, vertexCount / 2, mesh)
+    const axes = unit.principalAxes.boxAxes
+    const { origin, dirA, dirB } = axes
+
+    const size = Axes3D.size(Vec3(), axes)
+    Vec3.scale(size, size, sizeFactor / 2)
+    const radiusScale = Vec3.create(size[2], size[1], size[0])
+
+    builderState.currentGroup = 0
+    addEllipsoid(builderState, origin, dirA, dirB, radiusScale, detail)
+
+    return MeshBuilder.getMesh(builderState)
+}
+
+//
+
+function UnitIterator(group: Unit.SymmetryGroup): LocationIterator {
+    const groupCount = 1
+    const instanceCount = group.units.length
+    const location = StructureElement.Location.create()
+    const getLocation = (groupIndex: number, instanceIndex: number) => {
+        const unit = group.units[instanceIndex]
+        location.unit = unit
+        location.element = unit.elements[groupIndex]
+        return location
+    }
+    return LocationIterator(groupCount, instanceCount, getLocation)
+}
+
+function getUnitLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
+    const { objectId, instanceId } = pickingId
+    if (id === objectId) {
+        const { structure, group } = structureGroup
+        const unit = group.units[instanceId]
+        const indices = OrderedSet.ofBounds(0, unit.elements.length) as OrderedSet<UnitIndex>
+        return StructureElement.Loci(structure, [{ unit, indices }])
+    }
+    return EmptyLoci
+}
+
+function eachUnit(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+    let changed = false
+    if (!StructureElement.Loci.is(loci)) return false
+    const { structure, group } = structureGroup
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
+    const elementCount = group.elements.length
+    for (const e of loci.elements) {
+        const unitIdx = group.unitIndexMap.get(e.unit.id)
+        if (unitIdx !== undefined) {
+            if (OrderedSet.size(e.indices) === elementCount) {
+                if (apply(Interval.ofSingleton(unitIdx))) changed = true
+            }
+        }
+    }
+    return changed
+}

+ 1 - 1
src/tests/browser/render-text.ts

@@ -42,7 +42,7 @@ function textRepr() {
     }
 
     const textBuilder = TextBuilder.create(props, 1, 1)
-    textBuilder.add('Hello world', 0, 0, 0, 1, 0)
+    textBuilder.add('Hello world', 0, 0, 0, 1, 1, 0)
     // textBuilder.add('Добрый день', 0, 1, 0, 0, 0)
     // textBuilder.add('美好的一天', 0, 2, 0, 0, 0)
     // textBuilder.add('¿Cómo estás?', 0, -1, 0, 0, 0)