Browse Source

wip, measurements representations

Alexander Rose 5 years ago
parent
commit
58644b3d5c

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

@@ -49,6 +49,10 @@ export const DefaultPluginSpec: PluginSpec = {
         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.StructureSelectionsLabel3D),
+        PluginSpec.Action(StateTransforms.Representation.StructureSelectionsOrientation3D),
         PluginSpec.Action(StateTransforms.Representation.ModelUnitcell3D),
         PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),

+ 28 - 0
src/mol-plugin/state/transforms/helpers.ts

@@ -6,6 +6,11 @@
 
 import { Structure } from '../../../mol-model/structure';
 import { ComputedSecondaryStructure } from '../../../mol-model-props/computed/secondary-structure';
+import { PluginStateObject } from '../objects';
+import { DistanceData } from '../../../mol-repr/shape/loci/distance';
+import { LabelData } from '../../../mol-repr/shape/loci/label';
+import { OrientationData } from '../../../mol-repr/shape/loci/orientation';
+import { AngleData } from '../../../mol-repr/shape/loci/angle';
 
 /**
  * Attaches ComputedSecondaryStructure property when unavailable in sourceData and
@@ -19,4 +24,27 @@ export async function ensureSecondaryStructure(s: Structure) {
             await ComputedSecondaryStructure.attachFromCifOrCompute(s)
         }
     }
+}
+
+export function getDistanceDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DistanceData {
+    const lociA = s[0].loci
+    const lociB = s[1].loci
+    return { pairs: [{ lociA, lociB }] }
+}
+
+export function getAngleDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): AngleData {
+    const lociA = s[0].loci
+    const lociB = s[1].loci
+    const lociC = s[2].loci
+    return { triplets: [{ lociA, lociB, lociC }] }
+}
+
+export function getLabelDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): LabelData {
+    const loci = s[0].loci
+    return { infos: [{ loci }] }
+}
+
+export function getOrientationDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): OrientationData {
+    const loci = s[0].loci
+    return { loci }
 }

+ 139 - 2
src/mol-plugin/state/transforms/representation.ts

@@ -32,7 +32,12 @@ 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 } from '../../util/structure-orientation';
+import { getStructureOrientationRepresentation, OrientationParams as _OrientationParams } from '../../util/structure-orientation';
+import { DistanceParams, DistanceRepresentation } from '../../../mol-repr/shape/loci/distance';
+import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections } from './helpers';
+import { LabelParams, LabelRepresentation } from '../../../mol-repr/shape/loci/label';
+import { OrientationRepresentation, OrientationParams } from '../../../mol-repr/shape/loci/orientation';
+import { AngleParams, AngleRepresentation } from '../../../mol-repr/shape/loci/angle';
 
 export { StructureRepresentation3D }
 export { StructureRepresentation3DHelpers }
@@ -709,7 +714,7 @@ const StructureOrientation3D = PluginStateTransform.BuiltIn({
     from: SO.Molecule.Structure,
     to: SO.Shape.Representation3D,
     params: {
-        ...OrientationParams,
+        ..._OrientationParams,
     }
 })({
     canAutoUpdate({ oldParams, newParams }) {
@@ -727,4 +732,136 @@ const StructureOrientation3D = PluginStateTransform.BuiltIn({
             return StateTransformer.UpdateResult.Updated;
         });
     }
+});
+
+export { StructureSelectionsDistance3D }
+type StructureSelectionsDistance3D = typeof StructureSelectionsDistance3D
+const StructureSelectionsDistance3D = PluginStateTransform.BuiltIn({
+    name: 'structure-selections-distance-3d',
+    display: '3D Distance',
+    from: SO.Molecule.Structure.Selections,
+    to: SO.Shape.Representation3D,
+    params: {
+        ...DistanceParams,
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Structure Distance', async ctx => {
+            const data = getDistanceDataFromStructureSelections(a.data)
+            const repr = DistanceRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.structureRepresentation.themeCtx }, () => DistanceParams)
+            await repr.createOrUpdate(params, data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Distance` });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Structure Distance', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams }
+            const data = getDistanceDataFromStructureSelections(a.data)
+            await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
+            b.data.source = a
+            return StateTransformer.UpdateResult.Updated;
+        });
+    },
+});
+
+export { StructureSelectionsAngle3D }
+type StructureSelectionsAngle3D = typeof StructureSelectionsAngle3D
+const StructureSelectionsAngle3D = PluginStateTransform.BuiltIn({
+    name: 'structure-selections-angle-3d',
+    display: '3D Angle',
+    from: SO.Molecule.Structure.Selections,
+    to: SO.Shape.Representation3D,
+    params: {
+        ...AngleParams,
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Structure Angle', async ctx => {
+            const data = getAngleDataFromStructureSelections(a.data)
+            const repr = AngleRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.structureRepresentation.themeCtx }, () => AngleParams)
+            await repr.createOrUpdate(params, data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Angle` });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Structure Angle', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams }
+            const data = getAngleDataFromStructureSelections(a.data)
+            await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
+            b.data.source = a
+            return StateTransformer.UpdateResult.Updated;
+        });
+    },
+});
+
+export { StructureSelectionsLabel3D }
+type StructureSelectionsLabel3D = typeof StructureSelectionsLabel3D
+const StructureSelectionsLabel3D = PluginStateTransform.BuiltIn({
+    name: 'structure-selections-label-3d',
+    display: '3D Label',
+    from: SO.Molecule.Structure.Selections,
+    to: SO.Shape.Representation3D,
+    params: {
+        ...LabelParams,
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Structure Label', async ctx => {
+            const data = getLabelDataFromStructureSelections(a.data)
+            const repr = LabelRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.structureRepresentation.themeCtx }, () => LabelParams)
+            await repr.createOrUpdate(params, data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Label` });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Structure Label', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams }
+            const data = getLabelDataFromStructureSelections(a.data)
+            await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
+            b.data.source = a
+            return StateTransformer.UpdateResult.Updated;
+        });
+    },
+});
+
+export { StructureSelectionsOrientation3D }
+type StructureSelectionsOrientation3D = typeof StructureSelectionsOrientation3D
+const StructureSelectionsOrientation3D = PluginStateTransform.BuiltIn({
+    name: 'structure-selections-orientation-3d',
+    display: '3D Orientation',
+    from: SO.Molecule.Structure.Selections,
+    to: SO.Shape.Representation3D,
+    params: {
+        ...OrientationParams,
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Structure Orientation', async ctx => {
+            const data = getOrientationDataFromStructureSelections(a.data)
+            const repr = OrientationRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.structureRepresentation.themeCtx }, () => OrientationParams)
+            await repr.createOrUpdate(params, data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Orientation` });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Structure Orientation', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams }
+            const data = getOrientationDataFromStructureSelections(a.data)
+            await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
+            b.data.source = a
+            return StateTransformer.UpdateResult.Updated;
+        });
+    },
 });

+ 12 - 2
src/mol-plugin/ui/structure/selection.tsx

@@ -80,6 +80,11 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         this.plugin.helpers.measurement.addDistance(loci[0].loci, loci[1].loci);
     }
 
+    measureAngle = () => {
+        const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
+        this.plugin.helpers.measurement.addAngle(loci[0].loci, loci[1].loci, loci[2].loci);
+    }
+
     setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
         if (p.name === 'granularity') {
             PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { granularity: p.value } });
@@ -178,8 +183,13 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
                     <button className='msp-btn msp-btn-block' onClick={this.measureDistance} title='Measure distance between latest 2 selections'>
                         Measure Distance
                     </button>
-                </div>
-                }
+                </div>}
+                {latest.length >= 3 &&
+                    <div className='msp-control-row msp-row-text'>
+                    <button className='msp-btn msp-btn-block' onClick={this.measureAngle} title='Measure angle between latest 3 selections'>
+                        Measure Angle
+                    </button>
+                </div>}
             </>}
         </div>
     }

+ 38 - 8
src/mol-plugin/util/structure-measurement.ts

@@ -35,14 +35,44 @@ class StructureMeasurementManager {
         arraySetAdd(dependsOn, cellB.transform.ref);
 
         const update = this.getGroup();
-        update.apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
-            selections: [
-                { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
-                { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) }
-            ],
-            isTransitive: true,
-            label: 'Distance'
-        }, { dependsOn });
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections: [
+                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
+                    { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) }
+                ],
+                isTransitive: true,
+                label: 'Distance'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsDistance3D)
+
+        const state = this.context.state.dataState;
+        await PluginCommands.State.Update.dispatch(this.context, { state, tree: update, options: { doNotLogTiming: true } });
+    }
+
+    async addAngle(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci) {
+        const cellA = this.context.helpers.substructureParent.get(a.structure);
+        const cellB = this.context.helpers.substructureParent.get(b.structure);
+        const cellC = this.context.helpers.substructureParent.get(c.structure);
+
+        if (!cellA || !cellB || !cellC) return;
+
+        const dependsOn = [cellA.transform.ref];
+        arraySetAdd(dependsOn, cellB.transform.ref);
+        arraySetAdd(dependsOn, cellC.transform.ref);
+
+        const update = this.getGroup();
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections: [
+                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
+                    { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) },
+                    { key: 'c', ref: cellC.transform.ref, expression: StructureElement.Loci.toExpression(c) }
+                ],
+                isTransitive: true,
+                label: 'Angle'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsAngle3D)
 
         const state = this.context.state.dataState;
         await PluginCommands.State.Update.dispatch(this.context, { state, tree: update, options: { doNotLogTiming: true } });

+ 220 - 0
src/mol-repr/shape/loci/angle.ts

@@ -0,0 +1,220 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci } from '../../../mol-model/loci';
+import { RuntimeContext } from '../../../mol-task';
+import { stringToWords } from '../../../mol-util/string';
+import { Lines } from '../../../mol-geo/geometry/lines/lines';
+import { Text } from '../../../mol-geo/geometry/text/text';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { ColorNames } from '../../../mol-util/color/names';
+import { ShapeRepresentation } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation';
+import { Shape } from '../../../mol-model/shape';
+import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
+import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
+import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
+import { radToDeg } from '../../../mol-math/misc';
+import { Circle } from '../../../mol-geo/primitive/circle';
+
+export interface AngleData {
+    triplets: { lociA: Loci, lociB: Loci, lociC: Loci }[]
+}
+
+const SharedParams = {
+    color: PD.Color(ColorNames.darkgreen),
+}
+
+const LinesParams = {
+    ...Lines.Params,
+    ...SharedParams,
+    lineSizeAttenuation: PD.Boolean(true),
+    linesSize: PD.Numeric(0.05, { min: 0.01, max: 5, step: 0.01 }),
+}
+
+const VectorsParams = {
+    ...LinesParams
+}
+type VectorsParams = typeof VectorsParams
+
+const ArcParams = {
+    ...LinesParams
+}
+type ArcParams = typeof ArcParams
+
+const SectorParams = {
+    ...Mesh.Params,
+    ...SharedParams,
+}
+type SectorParams = typeof SectorParams
+
+const TextParams = {
+    ...Text.Params,
+    borderWidth: PD.Numeric(0.25, { min: 0, max: 0.5, step: 0.01 }),
+    textColor: PD.Color(ColorNames.black),
+    textSize: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
+}
+type TextParams = typeof TextParams
+
+const AngleVisuals = {
+    'vectors': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, VectorsParams>) => ShapeRepresentation(getVectorsShape, Lines.Utils),
+    'arc': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, ArcParams>) => ShapeRepresentation(getArcShape, Lines.Utils),
+    'sector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, SectorParams>) => ShapeRepresentation(getSectorShape, Mesh.Utils),
+    '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])
+
+export const AngleParams = {
+    ...VectorsParams,
+    ...ArcParams,
+    ...SectorParams,
+    ...TextParams,
+    visuals: PD.MultiSelect<AngleVisualName>(['vectors', 'arc', 'sector', 'text'], AngleVisualOptions),
+}
+export type AngleParams = typeof AngleParams
+export type AngleProps = PD.Values<AngleParams>
+
+//
+
+const tmpVecA = Vec3()
+const tmpVecB = Vec3()
+const tmpVecC = Vec3()
+// const tmpVecD = Vec3()
+
+const tmpDirA = Vec3()
+const tmpDirB = Vec3()
+const tmpCenter = Vec3()
+
+//
+
+function buildVectorsLines(data: AngleData, props: AngleProps, lines?: Lines): Lines {
+    const builder = LinesBuilder.create(128, 64, lines)
+    for (let i = 0, il = data.triplets.length; i < il; ++i) {
+        const { lociA, lociB, lociC } = data.triplets[i]
+        Loci.getCenter(lociA, tmpVecA)
+        Loci.getCenter(lociB, tmpVecB)
+        Loci.getCenter(lociC, tmpVecC)
+        builder.addFixedLengthDashes(tmpVecA, tmpVecB, 0.1, i)
+        builder.addFixedLengthDashes(tmpVecB, tmpVecC, 0.1, i)
+    }
+    return builder.getLines()
+}
+
+function getVectorsShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Lines>) {
+    const lines = buildVectorsLines(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return 'Angle Vectors'
+    }
+    return Shape.create('Angle Vectors', data, lines, () => props.color, () => props.linesSize, getLabel)
+}
+
+//
+
+function buildArcLines(data: AngleData, props: AngleProps, lines?: Lines): Lines {
+    const builder = LinesBuilder.create(128, 64, lines)
+
+    return builder.getLines()
+}
+
+function getArcShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Lines>) {
+    const lines = buildArcLines(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return 'Angle Arc'
+    }
+    return Shape.create('Angle Arc', data, lines, () => props.color, () => props.linesSize, getLabel)
+}
+
+//
+
+function buildSectorMesh(data: AngleData, props: AngleProps, mesh?: Mesh): Mesh {
+    const state = MeshBuilder.createState(128, 64, mesh)
+    const m = Mat4()
+    const tmpVec = Vec3()
+
+    for (let i = 0, il = data.triplets.length; i < il; ++i) {
+        const { lociA, lociB, lociC } = data.triplets[i]
+        console.log(data.triplets[i])
+        Loci.getCenter(lociA, tmpVecA)
+        Loci.getCenter(lociB, tmpVecB)
+        Loci.getCenter(lociC, tmpVecC)
+        Vec3.sub(tmpDirA, tmpVecA, tmpVecB)
+        Vec3.sub(tmpDirB, tmpVecC, tmpVecB)
+
+        const lenA = Vec3.magnitude(tmpDirA)
+        const lenB = Vec3.magnitude(tmpDirB)
+        let dir: Vec3, len: number
+        if (lenA <= lenB) {
+            dir = tmpDirA
+            len = lenA
+        } else {
+            dir = tmpDirB
+            len = lenB
+        }
+
+        const center = tmpVecB
+        const dirMajor = Vec3.cross(Vec3(), tmpDirA, tmpDirB)
+        const dirMinor = dir
+
+        Vec3.add(tmpVec, center, dirMajor)
+        Mat4.targetTo(m, center, tmpVec, dirMinor)
+        Mat4.setTranslation(m, center)
+
+        const angle = Vec3.angle(tmpDirA, tmpDirB)
+        const circle = Circle({ radius: len, thetaLength: angle })
+        console.log(circle)
+
+        MeshBuilder.addPrimitive(state, m, circle)
+        MeshBuilder.addPrimitiveFlipped(state, m, circle)
+    }
+
+    return MeshBuilder.getMesh(state)
+}
+
+function getSectorShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Mesh>) {
+    const mesh = buildSectorMesh(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return 'Angle Sector'
+    }
+    return Shape.create('Angle Sector', data, mesh, () => props.color, () => 1, getLabel)
+}
+
+//
+
+function buildText(data: AngleData, props: AngleProps, text?: Text): Text {
+    const builder = TextBuilder.create(props, 128, 64, text)
+    for (let i = 0, il = data.triplets.length; i < il; ++i) {
+        const {  lociA, lociB, lociC } = data.triplets[i]
+        Loci.getCenter(lociA, tmpVecA)
+        Loci.getCenter(lociB, tmpVecB)
+        Loci.getCenter(lociC, tmpVecC)
+        Vec3.sub(tmpDirA, tmpVecB, tmpVecA)
+        Vec3.sub(tmpDirB, tmpVecC, tmpVecB)
+        Vec3.add(tmpCenter, tmpVecA, tmpVecC)
+        Vec3.scale(tmpCenter, tmpCenter, 0.5)
+        const angle = radToDeg(Vec3.angle(tmpDirA, tmpDirB)).toPrecision(2)
+        const label = `${angle}\u00B0`
+        builder.add(label, tmpCenter[0], tmpCenter[1], tmpCenter[2], 0.1, i)
+    }
+    return builder.getText()
+}
+
+function getTextShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Text>) {
+    const text = buildText(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return 'Angle Text'
+    }
+    return Shape.create('Angle Text', data, text, () => props.textColor, () => props.textSize, getLabel)
+}
+
+//
+
+export type AngleRepresentation = Representation<AngleData, AngleParams>
+export function AngleRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, AngleParams>): AngleRepresentation {
+    return Representation.createMulti('Angle', ctx, getParams, Representation.StateBuilder, AngleVisuals as unknown as Representation.Def<AngleData, AngleParams>)
+}

+ 7 - 0
src/mol-repr/shape/loci/dihedral.ts

@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+// TODO

+ 112 - 0
src/mol-repr/shape/loci/distance.ts

@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci } from '../../../mol-model/loci';
+import { RuntimeContext } from '../../../mol-task';
+import { stringToWords } from '../../../mol-util/string';
+import { Lines } from '../../../mol-geo/geometry/lines/lines';
+import { Text } from '../../../mol-geo/geometry/text/text';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { ColorNames } from '../../../mol-util/color/names';
+import { ShapeRepresentation } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation';
+import { Shape } from '../../../mol-model/shape';
+import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
+import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+
+export interface DistanceData {
+    pairs: { lociA: Loci, lociB: Loci }[]
+}
+
+const LineParams = {
+    ...Lines.Params,
+    lineSizeAttenuation: PD.Boolean(true),
+    linesColor: PD.Color(ColorNames.darkgreen),
+    linesSize: PD.Numeric(0.05, { min: 0.01, max: 5, step: 0.01 }),
+}
+type LineParams = typeof LineParams
+
+const TextParams = {
+    ...Text.Params,
+    borderWidth: PD.Numeric(0.25, { min: 0, max: 0.5, step: 0.01 }),
+    textColor: PD.Color(ColorNames.black),
+    textSize: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
+    unitLabel: PD.Text('\u212B')
+}
+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),
+}
+type DistanceVisualName = keyof typeof DistanceVisuals
+const DistanceVisualOptions = Object.keys(DistanceVisuals).map(name => [name, stringToWords(name)] as [DistanceVisualName, string])
+
+export const DistanceParams = {
+    ...LineParams,
+    ...TextParams,
+    visuals: PD.MultiSelect<DistanceVisualName>(['lines', 'text'], DistanceVisualOptions),
+}
+export type DistanceParams = typeof DistanceParams
+export type DistanceProps = PD.Values<DistanceParams>
+
+//
+
+const tmpStartV = Vec3()
+const tmpEndV = Vec3()
+const tmpCenterV = Vec3()
+
+function buildLines(data: DistanceData, props: DistanceProps, lines?: Lines): Lines {
+    const builder = LinesBuilder.create(128, 64, lines)
+    for (let i = 0, il = data.pairs.length; i < il; ++i) {
+        const { lociA, lociB } = data.pairs[i]
+        Loci.getCenter(lociA, tmpStartV)
+        Loci.getCenter(lociB, tmpEndV)
+        builder.addFixedLengthDashes(tmpStartV, tmpEndV, 0.1, i)
+    }
+    return builder.getLines()
+}
+
+function getLinesShape(ctx: RuntimeContext, data: DistanceData, props: DistanceProps, shape?: Shape<Lines>) {
+    const lines = buildLines(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return 'Distance Line'
+    }
+    return Shape.create('Distance Lines', data, lines, () => props.linesColor, () => props.linesSize, getLabel)
+}
+
+//
+
+function buildText(data: DistanceData, props: DistanceProps, text?: Text): Text {
+    const builder = TextBuilder.create(props, 128, 64, text)
+    for (let i = 0, il = data.pairs.length; i < il; ++i) {
+        const { lociA, lociB } = data.pairs[i]
+        Loci.getCenter(lociA, tmpStartV)
+        Loci.getCenter(lociB, tmpEndV)
+        Vec3.add(tmpCenterV, tmpStartV, tmpEndV)
+        Vec3.scale(tmpCenterV, tmpCenterV, 0.5)
+        const dist = Vec3.distance(tmpStartV, tmpEndV).toPrecision(2)
+        const label = `${dist} ${props.unitLabel}`
+        builder.add(label, tmpCenterV[0], tmpCenterV[1], tmpCenterV[2], 0.1, i)
+    }
+    return builder.getText()
+}
+
+function getTextShape(ctx: RuntimeContext, data: DistanceData, props: DistanceProps, shape?: Shape<Text>) {
+    const text = buildText(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return 'Distance Text'
+    }
+    return Shape.create('Distance Text', data, text, () => props.textColor, () => props.textSize, getLabel)
+}
+
+//
+
+export type DistanceRepresentation = Representation<DistanceData, DistanceParams>
+export function DistanceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<DistanceData, DistanceParams>): DistanceRepresentation {
+    return Representation.createMulti('Distance', ctx, getParams, Representation.StateBuilder, DistanceVisuals as unknown as Representation.Def<DistanceData, DistanceParams>)
+}

+ 75 - 0
src/mol-repr/shape/loci/label.ts

@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci } from '../../../mol-model/loci';
+import { RuntimeContext } from '../../../mol-task';
+import { stringToWords } from '../../../mol-util/string';
+import { Text } from '../../../mol-geo/geometry/text/text';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { ColorNames } from '../../../mol-util/color/names';
+import { ShapeRepresentation } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation';
+import { Shape } from '../../../mol-model/shape';
+import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
+import { Sphere3D } from '../../../mol-math/geometry';
+import { lociLabel } from '../../../mol-theme/label';
+
+export interface LabelData {
+    infos: { loci: Loci, label?: string }[]
+}
+
+const TextParams = {
+    ...Text.Params,
+    borderWidth: PD.Numeric(0.25, { min: 0, max: 0.5, step: 0.01 }),
+    textColor: PD.Color(ColorNames.black),
+    textSize: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
+}
+type TextParams = typeof TextParams
+
+const LabelVisuals = {
+    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<LabelData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils),
+}
+type LabelVisualName = keyof typeof LabelVisuals
+const LabelVisualOptions = Object.keys(LabelVisuals).map(name => [name, stringToWords(name)] as [LabelVisualName, string])
+
+export const LabelParams = {
+    ...TextParams,
+    visuals: PD.MultiSelect<LabelVisualName>(['text'], LabelVisualOptions),
+}
+export type LabelParams = typeof LabelParams
+export type LabelProps = PD.Values<LabelParams>
+
+//
+
+const tmpSphere = Sphere3D()
+
+function buildText(data: LabelData, props: LabelProps, text?: Text): Text {
+    const builder = TextBuilder.create(props, 128, 64, text)
+    for (let i = 0, il = data.infos.length; i < il; ++i) {
+        const d = data.infos[i]
+        const sphere = Loci.getBoundingSphere(d.loci, tmpSphere)
+        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)
+    }
+    return builder.getText()
+}
+
+function getTextShape(ctx: RuntimeContext, data: LabelData, props: LabelProps, shape?: Shape<Text>) {
+    const text = buildText(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number) {
+        return 'Label Text'
+    }
+    return Shape.create('Label Text', data, text, () => props.textColor, () => props.textSize, getLabel)
+}
+
+//
+
+export type LabelRepresentation = Representation<LabelData, LabelParams>
+export function LabelRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<LabelData, LabelParams>): LabelRepresentation {
+    return Representation.createMulti('Label', ctx, getParams, Representation.StateBuilder, LabelVisuals as unknown as Representation.Def<LabelData, LabelParams>)
+}

+ 166 - 0
src/mol-repr/shape/loci/orientation.ts

@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci } from '../../../mol-model/loci';
+import { RuntimeContext } from '../../../mol-task';
+import { stringToWords } from '../../../mol-util/string';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+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';
+
+export interface OrientationData {
+    loci: Loci
+}
+
+const SharedParams = {
+    color: PD.Color(ColorNames.orange),
+    scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 })
+}
+
+const AxesParams = {
+    ...Mesh.Params,
+    ...SharedParams
+}
+type AxesParams = typeof AxesParams
+
+const BoxParams = {
+    ...Mesh.Params,
+    ...SharedParams
+}
+type BoxParams = typeof BoxParams
+
+const OrientationVisuals = {
+    'axes': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<OrientationData, AxesParams>) => ShapeRepresentation(getAxesShape, Mesh.Utils),
+    'box': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<OrientationData, BoxParams>) => ShapeRepresentation(getBoxShape, Mesh.Utils),
+}
+type OrientationVisualName = keyof typeof OrientationVisuals
+const OrientationVisualOptions = Object.keys(OrientationVisuals).map(name => [name, stringToWords(name)] as [OrientationVisualName, string])
+
+export const OrientationParams = {
+    ...AxesParams,
+    ...BoxParams,
+    visuals: PD.MultiSelect<OrientationVisualName>(['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>
+
+//
+
+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)
+
+    return MeshBuilder.getMesh(state)
+}
+
+function getAxesShape(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 ? buildAxesMesh(principalAxes, props, shape && shape.geometry) : Mesh.createEmpty(shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return `Principal Axes of ${label}`
+    }
+    return Shape.create('Principal Axes', data, mesh, () => props.color, () => 1, getLabel)
+}
+
+//
+
+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)
+
+    return MeshBuilder.getMesh(state)
+}
+
+function getBoxShape(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 ? buildBoxMesh(principalAxes, props, shape && shape.geometry) : Mesh.createEmpty(shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return `Oriented Box of ${label}`
+    }
+    return Shape.create('Oriented Box', 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>)
+}