فهرست منبع

improved measurements repr, including added dihedral repr

Alexander Rose 5 سال پیش
والد
کامیت
572a574bf3

+ 18 - 40
src/mol-geo/primitive/circle.ts

@@ -5,11 +5,10 @@
  */
 
 import { Primitive } from './primitive';
-import { Cage } from './cage';
 
 export const DefaultCircleProps = {
     radius: 1,
-    segments: 4,
+    segments: 36,
     thetaStart: 0,
     thetaLength: Math.PI * 2
 }
@@ -27,65 +26,44 @@ export function Circle(props?: CirclerProps): Primitive {
 
     // center
     vertices[0] = 0; vertices[1] = 0; vertices[2] = 0;
-    normals[0] = 0; normals[1] = 0; normals[2] = 1;
+    normals[0] = 0; normals[1] = 1; normals[2] = 0;
 
     // vertices & normals
     for (let s = 0, i = 3; s < segments; ++s, i += 3) {
         const segment = thetaStart + s / segments * thetaLength;
 
-        vertices[i] = radius * Math.cos(segment)
-        vertices[i + 1] = radius * Math.sin(segment)
-        vertices[i + 2] = 0
+        vertices[i] = radius * Math.sin(segment)
+        vertices[i + 1] = 0
+        vertices[i + 2] = radius * Math.cos(segment)
 
-        normals[i] = 0; normals[i + 1] = 0; normals[i + 2] = 1;
+        normals[i] = 0; normals[i + 1] = 1; normals[i + 2] = 0;
     }
 
     // indices
-    for (let s = 1, i = 0; s <= segments; ++s, i += 3) {
+    for (let s = 1, i = 0; s < segments; ++s, i += 3) {
         indices[i] = s; indices[i + 1] = s + 1; indices[i + 2] = 0;
     }
 
     if (isFull) {
-        // indices[i] = s; indices[i + 1] = s + 1; indices[i + 2] = 0;
+        const j = (segments - 1) * 3
+        indices[j] = segments
+        indices[j + 1] = 1
+        indices[j + 2] = 0
     } else {
         const segment = thetaStart + thetaLength;
         const i = (segments + 1) * 3
 
-        vertices[i] = radius * Math.cos(segment)
-        vertices[i + 1] = radius * Math.sin(segment)
-        vertices[i + 2] = 0
+        vertices[i] = radius * Math.sin(segment)
+        vertices[i + 1] = 0
+        vertices[i + 2] = radius * Math.cos(segment)
 
-        normals[i] = 0; normals[i + 1] = 0; normals[i + 2] = 1;
+        normals[i] = 0; normals[i + 1] = 1; normals[i + 2] = 0;
 
-        const j = segments * 3
-        indices[j] = segments + 1
-        indices[j + 1] = segments + 2
+        const j = (segments - 1) * 3
+        indices[j] = segments
+        indices[j + 1] = segments + 1
         indices[j + 2] = 0
     }
 
     return { vertices, normals, indices }
-}
-
-export function CircleCage(props?: CirclerProps): Cage {
-    const { radius, segments, thetaStart, thetaLength } = { ...DefaultCircleProps, ...props }
-
-    const n = segments * 3
-    const vertices = new Float32Array(n)
-    const edges = new Uint32Array(n)
-
-    for (let s = 0, i = 0; s <= segments; ++s, i += 3) {
-        const segment = thetaStart + s / segments * thetaLength;
-
-        // vertex
-        vertices[i] = radius * Math.cos(segment)
-        vertices[i + 1] = radius * Math.sin(segment)
-        vertices[i + 2] = 0
-    }
-
-    // indices
-    for (let s = 1, i = 0; s <= segments; ++s, i += 3) {
-        edges[0] = s; edges[1] = s + 1;
-    }
-
-    return { vertices, edges }
 }

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

@@ -51,6 +51,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Representation.StructureOrientation3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsDistance3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsAngle3D),
+        PluginSpec.Action(StateTransforms.Representation.StructureSelectionsDihedral3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsLabel3D),
         PluginSpec.Action(StateTransforms.Representation.StructureSelectionsOrientation3D),
         PluginSpec.Action(StateTransforms.Representation.ModelUnitcell3D),

+ 10 - 1
src/mol-plugin/state/transforms/helpers.ts

@@ -11,6 +11,7 @@ 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';
+import { DihedralData } from '../../../mol-repr/shape/loci/dihedral';
 
 /**
  * Attaches ComputedSecondaryStructure property when unavailable in sourceData and
@@ -36,7 +37,15 @@ export function getAngleDataFromStructureSelections(s: ReadonlyArray<PluginState
     const lociA = s[0].loci
     const lociB = s[1].loci
     const lociC = s[2].loci
-    return { triplets: [{ lociA, lociB, lociC }] }
+    return { triples: [{ lociA, lociB, lociC }] }
+}
+
+export function getDihedralDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DihedralData {
+    const lociA = s[0].loci
+    const lociB = s[1].loci
+    const lociC = s[2].loci
+    const lociD = s[3].loci
+    return { quads: [{ lociA, lociB, lociC, lociD }] }
 }
 
 export function getLabelDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): LabelData {

+ 35 - 1
src/mol-plugin/state/transforms/representation.ts

@@ -34,10 +34,11 @@ 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 } from './helpers';
+import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections } 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';
+import { DihedralParams, DihedralRepresentation } from '../../../mol-repr/shape/loci/dihedral';
 
 export { StructureRepresentation3D }
 export { StructureRepresentation3DHelpers }
@@ -800,6 +801,39 @@ const StructureSelectionsAngle3D = PluginStateTransform.BuiltIn({
     },
 });
 
+export { StructureSelectionsDihedral3D }
+type StructureSelectionsDihedral3D = typeof StructureSelectionsDihedral3D
+const StructureSelectionsDihedral3D = PluginStateTransform.BuiltIn({
+    name: 'structure-selections-dihedral-3d',
+    display: '3D Dihedral',
+    from: SO.Molecule.Structure.Selections,
+    to: SO.Shape.Representation3D,
+    params: {
+        ...DihedralParams,
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Structure Dihedral', async ctx => {
+            const data = getDihedralDataFromStructureSelections(a.data)
+            const repr = DihedralRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.structureRepresentation.themeCtx }, () => DihedralParams)
+            await repr.createOrUpdate(params, data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: `Dihedral` });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Structure Dihedral', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams }
+            const data = getDihedralDataFromStructureSelections(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({

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

@@ -85,6 +85,11 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         this.plugin.helpers.measurement.addAngle(loci[0].loci, loci[1].loci, loci[2].loci);
     }
 
+    measureDihedral = () => {
+        const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
+        this.plugin.helpers.measurement.addDihedral(loci[0].loci, loci[1].loci, loci[2].loci, loci[3].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 } });
@@ -150,7 +155,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
 
         // TODO: fix the styles, move them to CSS
 
-        for (let i = 0, _i = Math.min(3, mng.latestLoci.length); i < _i; i++) {
+        for (let i = 0, _i = Math.min(4, mng.latestLoci.length); i < _i; i++) {
             const e = mng.latestLoci[i];
             latest.push(<li key={e!.label}>
                 <button className='msp-btn msp-btn-block msp-form-control' style={{ borderRight: '6px solid transparent', overflow: 'hidden' }}
@@ -190,6 +195,12 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
                         Measure Angle
                     </button>
                 </div>}
+                {latest.length >= 4 &&
+                    <div className='msp-control-row msp-row-text'>
+                    <button className='msp-btn msp-btn-block' onClick={this.measureDihedral} title='Measure dihedral between latest 4 selections'>
+                        Measure Dihedral
+                    </button>
+                </div>}
             </>}
         </div>
     }

+ 31 - 0
src/mol-plugin/util/structure-measurement.ts

@@ -78,6 +78,37 @@ class StructureMeasurementManager {
         await PluginCommands.State.Update.dispatch(this.context, { state, tree: update, options: { doNotLogTiming: true } });
     }
 
+    async addDihedral(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci, d: 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);
+        const cellD = this.context.helpers.substructureParent.get(d.structure);
+
+        if (!cellA || !cellB || !cellC || !cellD) return;
+
+        const dependsOn = [cellA.transform.ref];
+        arraySetAdd(dependsOn, cellB.transform.ref);
+        arraySetAdd(dependsOn, cellC.transform.ref);
+        arraySetAdd(dependsOn, cellD.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) },
+                    { key: 'd', ref: cellD.transform.ref, expression: StructureElement.Loci.toExpression(d) }
+                ],
+                isTransitive: true,
+                label: 'Dihedral'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsDihedral3D)
+
+        const state = this.context.state.dataState;
+        await PluginCommands.State.Update.dispatch(this.context, { state, tree: update, options: { doNotLogTiming: true } });
+    }
+
     constructor(private context: PluginContext) {
 
     }

+ 113 - 79
src/mol-repr/shape/loci/angle.ts

@@ -19,22 +19,25 @@ 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 { radToDeg, arcLength } from '../../../mol-math/misc';
 import { Circle } from '../../../mol-geo/primitive/circle';
+import { transformPrimitive } from '../../../mol-geo/primitive/primitive';
 
 export interface AngleData {
-    triplets: { lociA: Loci, lociB: Loci, lociC: Loci }[]
+    triples: Loci.Triple[]
 }
 
 const SharedParams = {
-    color: PD.Color(ColorNames.darkgreen),
+    color: PD.Color(ColorNames.lightgreen),
+    arcScale: PD.Numeric(0.7, { min: 0.01, max: 1, step: 0.01 })
 }
 
 const LinesParams = {
     ...Lines.Params,
     ...SharedParams,
     lineSizeAttenuation: PD.Boolean(true),
-    linesSize: PD.Numeric(0.05, { min: 0.01, max: 5, step: 0.01 }),
+    linesSize: PD.Numeric(0.04, { min: 0.01, max: 5, step: 0.01 }),
+    dashLength: PD.Numeric(0.04, { min: 0.01, max: 0.2, step: 0.01 }),
 }
 
 const VectorsParams = {
@@ -50,22 +53,23 @@ type ArcParams = typeof ArcParams
 const SectorParams = {
     ...Mesh.Params,
     ...SharedParams,
+    sectorOpacity: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }),
 }
 type SectorParams = typeof SectorParams
 
 const TextParams = {
     ...Text.Params,
-    borderWidth: PD.Numeric(0.25, { min: 0, max: 0.5, step: 0.01 }),
+    borderWidth: PD.Numeric(0.2, { 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 }),
+    textSize: PD.Numeric(0.4, { 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),
+    '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 }) }),
 }
 type AngleVisualName = keyof typeof AngleVisuals
 const AngleVisualOptions = Object.keys(AngleVisuals).map(name => [name, stringToWords(name)] as [AngleVisualName, string])
@@ -75,33 +79,82 @@ export const AngleParams = {
     ...ArcParams,
     ...SectorParams,
     ...TextParams,
-    visuals: PD.MultiSelect<AngleVisualName>(['vectors', 'arc', 'sector', 'text'], AngleVisualOptions),
+    visuals: PD.MultiSelect<AngleVisualName>(['vectors', '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()
+function getAngleState() {
+    return {
+        pointA: Vec3(),
+        pointB: Vec3(),
+        pointC: Vec3(),
 
-const tmpDirA = Vec3()
-const tmpDirB = Vec3()
-const tmpCenter = Vec3()
+        arcDirA: Vec3(),
+        arcDirC: Vec3(),
+        arcNormal: Vec3(),
+
+        radius: 0,
+        angle: 0,
+    }
+}
+type AngleState = ReturnType<typeof getAngleState>
+
+const tmpVec = Vec3()
+const tmpMat = Mat4()
+
+function setAngleState(triple: Loci.Triple, state: AngleState, arcScale: number) {
+    const { pointA, pointB, pointC } = state
+    const { arcDirA, arcDirC, arcNormal } = state
+
+    const { lociA, lociB, lociC } = triple
+    Loci.getCenter(lociA, pointA)
+    Loci.getCenter(lociB, pointB)
+    Loci.getCenter(lociC, pointC)
+
+    Vec3.sub(arcDirA, pointA, pointB)
+    Vec3.sub(arcDirC, pointC, pointB)
+    Vec3.cross(arcNormal, arcDirA, arcDirC)
+
+    const len = Math.min(Vec3.magnitude(arcDirA), Vec3.magnitude(arcDirC))
+    const radius = len * arcScale
+
+    state.radius = radius
+    state.angle = Vec3.angle(arcDirA, arcDirC)
+
+    return state
+}
+
+function getCircle(state: AngleState, segmentLength?: number) {
+    const { radius, angle } = state
+    const segments = segmentLength ? arcLength(angle, radius) / segmentLength : 32
+
+    Mat4.targetTo(tmpMat, state.pointB, state.pointA, state.arcNormal)
+    Mat4.setTranslation(tmpMat, state.pointB)
+    Mat4.mul(tmpMat, tmpMat, Mat4.rotY180)
+
+    const circle = Circle({ radius, thetaLength: angle, segments })
+    return transformPrimitive(circle, tmpMat)
+}
+
+const tmpState = getAngleState()
+
+function angleLabel(triple: Loci.Triple, arcScale: number) {
+    setAngleState(triple, tmpState, arcScale)
+    const angle = radToDeg(tmpState.angle).toFixed(2)
+    return `Angle ${angle}\u00B0`
+}
 
 //
 
 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)
+    for (let i = 0, il = data.triples.length; i < il; ++i) {
+        setAngleState(data.triples[i], tmpState, props.arcScale)
+        builder.addFixedLengthDashes(tmpState.pointB, tmpState.pointA, props.dashLength, i)
+        builder.addFixedLengthDashes(tmpState.pointB, tmpState.pointC, props.dashLength, i)
     }
     return builder.getLines()
 }
@@ -109,7 +162,7 @@ function buildVectorsLines(data: AngleData, props: AngleProps, lines?: Lines): L
 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 angleLabel(data.triples[groupId], props.arcScale)
     }
     return Shape.create('Angle Vectors', data, lines, () => props.color, () => props.linesSize, getLabel)
 }
@@ -118,14 +171,30 @@ function getVectorsShape(ctx: RuntimeContext, data: AngleData, props: AngleProps
 
 function buildArcLines(data: AngleData, props: AngleProps, lines?: Lines): Lines {
     const builder = LinesBuilder.create(128, 64, lines)
-
+    for (let i = 0, il = data.triples.length; i < il; ++i) {
+        setAngleState(data.triples[i], tmpState, props.arcScale)
+        const circle = getCircle(tmpState, props.dashLength)
+        const { indices, vertices } = circle
+        for (let j = 0, jl = indices.length; j < jl; j += 3) {
+            if (j % 2 === 1) continue // draw every other segment to get dashes
+            const start = indices[j] * 3
+            const end = indices[j + 1] * 3
+            const startX = vertices[start]
+            const startY = vertices[start + 1]
+            const startZ = vertices[start + 2]
+            const endX = vertices[end]
+            const endY = vertices[end + 1]
+            const endZ = vertices[end + 2]
+            builder.add(startX, startY, startZ, endX, endY, endZ, i)
+        }
+    }
     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 angleLabel(data.triples[groupId], props.arcScale)
     }
     return Shape.create('Angle Arc', data, lines, () => props.color, () => props.linesSize, getLabel)
 }
@@ -134,52 +203,19 @@ function getArcShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, sh
 
 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)
+    for (let i = 0, il = data.triples.length; i < il; ++i) {
+        setAngleState(data.triples[i], tmpState, props.arcScale)
+        const circle = getCircle(tmpState)
+        MeshBuilder.addPrimitive(state, Mat4.id, circle)
+        MeshBuilder.addPrimitiveFlipped(state, Mat4.id, 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 angleLabel(data.triples[groupId], props.arcScale)
     }
     return Shape.create('Angle Sector', data, mesh, () => props.color, () => 1, getLabel)
 }
@@ -188,18 +224,16 @@ function getSectorShape(ctx: RuntimeContext, data: AngleData, props: AngleProps,
 
 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)
+    for (let i = 0, il = data.triples.length; i < il; ++i) {
+        setAngleState(data.triples[i], tmpState, props.arcScale)
+
+        Vec3.add(tmpVec, tmpState.arcDirA, tmpState.arcDirC)
+        Vec3.setMagnitude(tmpVec, tmpVec, tmpState.radius)
+        Vec3.add(tmpVec, tmpState.pointB, tmpVec)
+
+        const angle = radToDeg(tmpState.angle).toFixed(2)
         const label = `${angle}\u00B0`
-        builder.add(label, tmpCenter[0], tmpCenter[1], tmpCenter[2], 0.1, i)
+        builder.add(label, tmpVec[0], tmpVec[1], tmpVec[2], 0.1, i)
     }
     return builder.getText()
 }
@@ -207,7 +241,7 @@ function buildText(data: AngleData, props: AngleProps, text?: Text): Text {
 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 angleLabel(data.triples[groupId], props.arcScale)
     }
     return Shape.create('Angle Text', data, text, () => props.textColor, () => props.textSize, getLabel)
 }

+ 310 - 1
src/mol-repr/shape/loci/dihedral.ts

@@ -4,4 +4,313 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-// TODO
+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 { arcLength, halfPI, radToDeg } from '../../../mol-math/misc';
+import { Circle } from '../../../mol-geo/primitive/circle';
+import { transformPrimitive } from '../../../mol-geo/primitive/primitive';
+
+export interface DihedralData {
+    quads: Loci.Quad[]
+}
+
+const SharedParams = {
+    color: PD.Color(ColorNames.lightgreen),
+    arcScale: PD.Numeric(0.7, { min: 0.01, max: 1, step: 0.01 })
+}
+
+const LinesParams = {
+    ...Lines.Params,
+    ...SharedParams,
+    lineSizeAttenuation: PD.Boolean(true),
+    linesSize: PD.Numeric(0.04, { min: 0.01, max: 5, step: 0.01 }),
+    dashLength: PD.Numeric(0.04, { min: 0.01, max: 0.2, step: 0.01 }),
+}
+
+const VectorsParams = {
+    ...LinesParams
+}
+type VectorsParams = typeof VectorsParams
+
+const ExtendersParams = {
+    ...LinesParams
+}
+type ExtendersParams = typeof ExtendersParams
+
+const ArcParams = {
+    ...LinesParams
+}
+type ArcParams = typeof ArcParams
+
+const SectorParams = {
+    ...Mesh.Params,
+    ...SharedParams,
+    sectorOpacity: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }),
+}
+type SectorParams = typeof SectorParams
+
+const TextParams = {
+    ...Text.Params,
+    borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 }),
+    textColor: PD.Color(ColorNames.black),
+    textSize: PD.Numeric(0.4, { min: 0.1, max: 5, step: 0.1 }),
+}
+type TextParams = typeof TextParams
+
+const DihedralVisuals = {
+    'vectors': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, VectorsParams>) => ShapeRepresentation(getVectorsShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
+    '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 }) }),
+}
+type DihedralVisualName = keyof typeof DihedralVisuals
+const DihedralVisualOptions = Object.keys(DihedralVisuals).map(name => [name, stringToWords(name)] as [DihedralVisualName, string])
+
+export const DihedralParams = {
+    ...VectorsParams,
+    ...ExtendersParams,
+    ...ArcParams,
+    ...SectorParams,
+    ...TextParams,
+    visuals: PD.MultiSelect<DihedralVisualName>(['extenders', 'sector', 'text'], DihedralVisualOptions),
+}
+export type DihedralParams = typeof DihedralParams
+export type DihedralProps = PD.Values<DihedralParams>
+
+//
+
+function getDihedralState() {
+    return {
+        pointA: Vec3(),
+        pointB: Vec3(),
+        pointC: Vec3(),
+        pointD: Vec3(),
+
+        dirBA: Vec3(),
+        dirCD: Vec3(),
+
+        projA: Vec3(),
+        projD: Vec3(),
+
+        arcPointA: Vec3(),
+        arcPointD: Vec3(),
+        arcDirA: Vec3(),
+        arcDirD: Vec3(),
+        arcCenter: Vec3(),
+        arcNormal: Vec3(),
+
+        radius: 0,
+        angle: 0,
+    }
+}
+type DihedralState = ReturnType<typeof getDihedralState>
+
+const tmpVec = Vec3()
+const tmpMat = Mat4()
+
+// TODO improper dihedrals are not handled correctly
+function setDihedralState(quad: Loci.Quad, state: DihedralState, arcScale: number) {
+    const { pointA, pointB, pointC, pointD, dirBA, dirCD, projA, projD } = state
+    const { arcPointA, arcPointD, arcDirA, arcDirD, arcCenter, arcNormal } = state
+
+    const { lociA, lociB, lociC, lociD } = quad
+    Loci.getCenter(lociA, pointA)
+    Loci.getCenter(lociB, pointB)
+    Loci.getCenter(lociC, pointC)
+    Loci.getCenter(lociD, pointD)
+
+    Vec3.add(arcCenter, pointB, pointC)
+    Vec3.scale(arcCenter, arcCenter, 0.5)
+
+    Vec3.sub(dirBA, pointA, pointB)
+    Vec3.sub(dirCD, pointD, pointC)
+    Vec3.add(arcPointA, arcCenter, dirBA)
+    Vec3.add(arcPointD, arcCenter, dirCD)
+
+    Vec3.sub(arcNormal, pointC, pointB)
+    Vec3.orthogonalize(arcDirA, arcNormal, dirBA)
+    Vec3.orthogonalize(arcDirD, arcNormal, dirCD)
+
+    Vec3.projectPointOnVector(projA, arcPointA, arcDirA, arcCenter)
+    Vec3.projectPointOnVector(projD, arcPointD, arcDirD, arcCenter)
+    const len = Math.min(Vec3.distance(projA, arcCenter), Vec3.distance(projD, arcCenter))
+    const radius = len * arcScale
+
+    Vec3.setMagnitude(arcDirA, arcDirA, radius)
+    Vec3.setMagnitude(arcDirD, arcDirD, radius)
+    Vec3.add(arcPointA, arcCenter, arcDirA)
+    Vec3.add(arcPointD, arcCenter, arcDirD)
+    state.radius = radius
+    state.angle = Vec3.angle(arcDirA, arcDirD)
+
+    Vec3.matchDirection(tmpVec, arcNormal, Vec3.sub(tmpVec, arcPointA, pointA))
+    const angleA = Vec3.angle(dirBA, tmpVec)
+    const lenA = radius / Math.cos(angleA > halfPI ? angleA - halfPI : angleA)
+    Vec3.add(projA, pointB, Vec3.setMagnitude(tmpVec, dirBA, lenA))
+
+    Vec3.matchDirection(tmpVec, arcNormal, Vec3.sub(tmpVec, arcPointD, pointD))
+    const angleD = Vec3.angle(dirCD, tmpVec)
+    const lenD = radius / Math.cos(angleD > halfPI ? angleD - halfPI : angleD)
+    Vec3.add(projD, pointC, Vec3.setMagnitude(tmpVec, dirCD, lenD))
+
+    return state
+}
+
+function getCircle(state: DihedralState, segmentLength?: number) {
+    const { radius, angle } = state
+    const segments = segmentLength ? arcLength(angle, radius) / segmentLength : 32
+
+    Mat4.targetTo(tmpMat, state.arcCenter, angle > halfPI ? state.arcPointA : state.arcPointD, state.arcNormal)
+    Mat4.setTranslation(tmpMat, state.arcCenter)
+    Mat4.mul(tmpMat, tmpMat, Mat4.rotY180)
+
+    const circle = Circle({ radius, thetaLength: angle, segments })
+    return transformPrimitive(circle, tmpMat)
+}
+
+const tmpState = getDihedralState()
+function dihedralLabel(quad: Loci.Quad, arcScale: number) {
+    setDihedralState(quad, tmpState, arcScale)
+    const angle = radToDeg(tmpState.angle).toFixed(2)
+    return `Dihedral ${angle}\u00B0`
+}
+
+//
+
+function buildVectorsLines(data: DihedralData, props: DihedralProps, lines?: Lines): Lines {
+    const builder = LinesBuilder.create(128, 64, lines)
+    for (let i = 0, il = data.quads.length; i < il; ++i) {
+        setDihedralState(data.quads[i], tmpState, props.arcScale)
+        builder.addFixedLengthDashes(tmpState.arcCenter, tmpState.arcPointA, props.dashLength, i)
+        builder.addFixedLengthDashes(tmpState.arcCenter, tmpState.arcPointD, props.dashLength, i)
+    }
+    return builder.getLines()
+}
+
+function getVectorsShape(ctx: RuntimeContext, data: DihedralData, props: DihedralProps, shape?: Shape<Lines>) {
+    const lines = buildVectorsLines(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return dihedralLabel(data.quads[groupId], props.arcScale)
+    }
+    return Shape.create('Dihedral Vectors', data, lines, () => props.color, () => props.linesSize, getLabel)
+}
+
+//
+
+function buildExtendersLines(data: DihedralData, props: DihedralProps, lines?: Lines): Lines {
+    const builder = LinesBuilder.create(128, 64, lines)
+    for (let i = 0, il = data.quads.length; i < il; ++i) {
+        setDihedralState(data.quads[i], tmpState, props.arcScale)
+        builder.addFixedLengthDashes(tmpState.arcPointA, tmpState.projA, props.dashLength, i)
+        builder.addFixedLengthDashes(tmpState.arcPointD, tmpState.projD, props.dashLength, i)
+    }
+    return builder.getLines()
+}
+
+function getExtendersShape(ctx: RuntimeContext, data: DihedralData, props: DihedralProps, shape?: Shape<Lines>) {
+    const lines = buildExtendersLines(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return dihedralLabel(data.quads[groupId], props.arcScale)
+    }
+    return Shape.create('Dihedral Extenders', data, lines, () => props.color, () => props.linesSize, getLabel)
+}
+
+//
+
+function buildArcLines(data: DihedralData, props: DihedralProps, lines?: Lines): Lines {
+    const builder = LinesBuilder.create(128, 64, lines)
+    for (let i = 0, il = data.quads.length; i < il; ++i) {
+        setDihedralState(data.quads[i], tmpState, props.arcScale)
+        const circle = getCircle(tmpState, props.dashLength)
+        const { indices, vertices } = circle
+        for (let j = 0, jl = indices.length; j < jl; j += 3) {
+            if (j % 2 === 1) continue // draw every other segment to get dashes
+            const start = indices[j] * 3
+            const end = indices[j + 1] * 3
+            const startX = vertices[start]
+            const startY = vertices[start + 1]
+            const startZ = vertices[start + 2]
+            const endX = vertices[end]
+            const endY = vertices[end + 1]
+            const endZ = vertices[end + 2]
+            builder.add(startX, startY, startZ, endX, endY, endZ, i)
+        }
+    }
+    return builder.getLines()
+}
+
+function getArcShape(ctx: RuntimeContext, data: DihedralData, props: DihedralProps, shape?: Shape<Lines>) {
+    const lines = buildArcLines(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return dihedralLabel(data.quads[groupId], props.arcScale)
+    }
+    return Shape.create('Dihedral Arc', data, lines, () => props.color, () => props.linesSize, getLabel)
+}
+
+//
+
+function buildSectorMesh(data: DihedralData, props: DihedralProps, mesh?: Mesh): Mesh {
+    const state = MeshBuilder.createState(128, 64, mesh)
+    for (let i = 0, il = data.quads.length; i < il; ++i) {
+        setDihedralState(data.quads[i], tmpState, props.arcScale)
+        const circle = getCircle(tmpState)
+        MeshBuilder.addPrimitive(state, Mat4.id, circle)
+        MeshBuilder.addPrimitiveFlipped(state, Mat4.id, circle)
+    }
+    return MeshBuilder.getMesh(state)
+}
+
+function getSectorShape(ctx: RuntimeContext, data: DihedralData, props: DihedralProps, shape?: Shape<Mesh>) {
+    const mesh = buildSectorMesh(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return dihedralLabel(data.quads[groupId], props.arcScale)
+    }
+    return Shape.create('Dihedral Sector', data, mesh, () => props.color, () => 1, getLabel)
+}
+
+//
+
+function buildText(data: DihedralData, props: DihedralProps, text?: Text): Text {
+    const builder = TextBuilder.create(props, 128, 64, text)
+    for (let i = 0, il = data.quads.length; i < il; ++i) {
+        setDihedralState(data.quads[i], tmpState, props.arcScale)
+
+        Vec3.add(tmpVec, tmpState.arcDirA, tmpState.arcDirD)
+        Vec3.setMagnitude(tmpVec, tmpVec, tmpState.radius)
+        Vec3.add(tmpVec, tmpState.arcCenter, tmpVec)
+
+        const angle = radToDeg(tmpState.angle).toFixed(2)
+        const label = `${angle}\u00B0`
+        builder.add(label, tmpVec[0], tmpVec[1], tmpVec[2], 0.1, i)
+    }
+    return builder.getText()
+}
+
+function getTextShape(ctx: RuntimeContext, data: DihedralData, props: DihedralProps, shape?: Shape<Text>) {
+    const text = buildText(data, props, shape && shape.geometry);
+    const getLabel = function (groupId: number ) {
+        return dihedralLabel(data.quads[groupId], props.arcScale)
+    }
+    return Shape.create('Dihedral Text', data, text, () => props.textColor, () => props.textSize, getLabel)
+}
+
+//
+
+export type DihedralRepresentation = Representation<DihedralData, DihedralParams>
+export function DihedralRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, DihedralParams>): DihedralRepresentation {
+    return Representation.createMulti('Dihedral', ctx, getParams, Representation.StateBuilder, DihedralVisuals as unknown as Representation.Def<DihedralData, DihedralParams>)
+}

+ 54 - 24
src/mol-repr/shape/loci/distance.ts

@@ -19,29 +19,35 @@ import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 
 export interface DistanceData {
-    pairs: { lociA: Loci, lociB: Loci }[]
+    pairs: Loci.Pair[]
+}
+
+const SharedParams = {
+    unitLabel: PD.Text('\u212B')
 }
 
 const LineParams = {
     ...Lines.Params,
+    ...SharedParams,
     lineSizeAttenuation: PD.Boolean(true),
-    linesColor: PD.Color(ColorNames.darkgreen),
-    linesSize: PD.Numeric(0.05, { min: 0.01, max: 5, step: 0.01 }),
+    linesColor: PD.Color(ColorNames.lightgreen),
+    linesSize: PD.Numeric(0.075, { min: 0.01, max: 5, step: 0.01 }),
+    dashLength: PD.Numeric(0.2, { min: 0.01, max: 0.2, step: 0.01 }),
 }
 type LineParams = typeof LineParams
 
 const TextParams = {
     ...Text.Params,
-    borderWidth: PD.Numeric(0.25, { min: 0, max: 0.5, step: 0.01 }),
+    ...SharedParams,
+    borderWidth: PD.Numeric(0.2, { 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')
+    textSize: PD.Numeric(0.4, { min: 0.1, max: 5, step: 0.1 }),
 }
 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),
+    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DistanceData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
 }
 type DistanceVisualName = keyof typeof DistanceVisuals
 const DistanceVisualOptions = Object.keys(DistanceVisuals).map(name => [name, stringToWords(name)] as [DistanceVisualName, string])
@@ -56,17 +62,45 @@ export type DistanceProps = PD.Values<DistanceParams>
 
 //
 
-const tmpStartV = Vec3()
-const tmpEndV = Vec3()
-const tmpCenterV = Vec3()
+function getDistanceState() {
+    return {
+        pointA: Vec3(),
+        pointB: Vec3(),
+
+        center: Vec3(),
+        distance: 0,
+    }
+}
+type DistanceState = ReturnType<typeof getDistanceState>
+
+function setDistanceState(pair: Loci.Pair, state: DistanceState) {
+    const { pointA, pointB, center } = state
+
+    const { lociA, lociB } = pair
+    Loci.getCenter(lociA, pointA)
+    Loci.getCenter(lociB, pointB)
+
+    Vec3.add(center, pointA, pointB)
+    Vec3.scale(center, center, 0.5)
+    state.distance = Vec3.distance(pointA, pointB)
+
+    return state
+}
+
+const tmpState = getDistanceState()
+
+function distanceLabel(pair: Loci.Pair, unitLabel: string) {
+    setDistanceState(pair, tmpState)
+    return `Distance ${tmpState.distance.toFixed(2)} ${unitLabel}`
+}
+
+//
 
 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)
+        setDistanceState(data.pairs[i], tmpState)
+        builder.addFixedLengthDashes(tmpState.pointA, tmpState.pointB, props.dashLength, i)
     }
     return builder.getLines()
 }
@@ -74,7 +108,7 @@ function buildLines(data: DistanceData, props: DistanceProps, lines?: Lines): Li
 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 distanceLabel(data.pairs[groupId], props.unitLabel)
     }
     return Shape.create('Distance Lines', data, lines, () => props.linesColor, () => props.linesSize, getLabel)
 }
@@ -84,14 +118,10 @@ function getLinesShape(ctx: RuntimeContext, data: DistanceData, props: DistanceP
 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)
+        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)
     }
     return builder.getText()
 }
@@ -99,7 +129,7 @@ function buildText(data: DistanceData, props: DistanceProps, text?: Text): Text
 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 distanceLabel(data.pairs[groupId], props.unitLabel)
     }
     return Shape.create('Distance Text', data, text, () => props.textColor, () => props.textSize, getLabel)
 }

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

@@ -23,9 +23,9 @@ export interface LabelData {
 
 const TextParams = {
     ...Text.Params,
-    borderWidth: PD.Numeric(0.25, { min: 0, max: 0.5, step: 0.01 }),
+    borderWidth: PD.Numeric(0.2, { 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 }),
+    textSize: PD.Numeric(0.4, { min: 0.1, max: 5, step: 0.1 }),
 }
 type TextParams = typeof TextParams