Browse Source

improved low-poly geo (cylinder, sheet, tube, ribbon)

Alexander Rose 5 years ago
parent
commit
66b9f6104c

+ 10 - 3
src/mol-geo/geometry/mesh/builder/cylinder.ts

@@ -1,13 +1,15 @@
 /**
- * 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, Mat4 } from '../../../../mol-math/linear-algebra';
 import { MeshBuilder } from '../mesh-builder';
-import { Primitive } from '../../../primitive/primitive';
+import { Primitive, transformPrimitive } from '../../../primitive/primitive';
 import { Cylinder, CylinderProps } from '../../../primitive/cylinder';
+import { Prism } from '../../../primitive/prism';
+import { polygon } from '../../../primitive/polygon';
 
 const cylinderMap = new Map<string, Primitive>()
 const up = Vec3.create(0, 1, 0)
@@ -34,7 +36,12 @@ function getCylinder(props: CylinderProps) {
     const key = JSON.stringify(props)
     let cylinder = cylinderMap.get(key)
     if (cylinder === undefined) {
-        cylinder = Cylinder(props)
+        if (props.radialSegments && props.radialSegments <= 4) {
+            const box = Prism(polygon(4, true, props.radiusTop), props)
+            cylinder = transformPrimitive(box, Mat4.rotX90)
+        } else {
+            cylinder = Cylinder(props)
+        }
         cylinderMap.set(key, cylinder)
     }
     return cylinder

+ 108 - 0
src/mol-geo/geometry/mesh/builder/ribbon.ts

@@ -0,0 +1,108 @@
+/**
+ * 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>
+ */
+
+import { Vec3 } from '../../../../mol-math/linear-algebra';
+import { ChunkedArray } from '../../../../mol-data/util';
+import { MeshBuilder } from '../mesh-builder';
+
+const tA = Vec3.zero()
+const tB = Vec3.zero()
+const tV = Vec3.zero()
+
+const horizontalVector = Vec3.zero()
+const verticalVector = Vec3.zero()
+const normalOffset = Vec3.zero()
+const positionVector = Vec3.zero()
+const normalVector = Vec3.zero()
+const torsionVector = Vec3.zero()
+
+/** set arrowHeight = 0 for no arrow */
+export function addRibbon(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, arrowHeight: number) {
+    const { currentGroup, vertices, normals, indices, groups } = state
+
+    let vertexCount = vertices.elementCount
+    let offsetLength = 0
+
+    if (arrowHeight > 0) {
+        Vec3.fromArray(tA, controlPoints, 0)
+        Vec3.fromArray(tB, controlPoints, linearSegments * 3)
+        offsetLength = arrowHeight / Vec3.magnitude(Vec3.sub(tV, tB, tA))
+    }
+
+    for (let i = 0; i <= linearSegments; ++i) {
+        const width = widthValues[i]
+        const height = heightValues[i]
+
+        const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments);
+        const i3 = i * 3
+
+        Vec3.fromArray(verticalVector, normalVectors, i3)
+        Vec3.scale(verticalVector, verticalVector, actualHeight);
+
+        Vec3.fromArray(horizontalVector, binormalVectors, i3)
+        Vec3.scale(horizontalVector, horizontalVector, width);
+
+        if (arrowHeight > 0) {
+            Vec3.fromArray(tA, normalVectors, i3)
+            Vec3.fromArray(tB, binormalVectors, i3)
+            Vec3.scale(normalOffset, Vec3.cross(normalOffset, tA, tB), offsetLength)
+        }
+
+        Vec3.fromArray(positionVector, controlPoints, i3)
+        Vec3.fromArray(normalVector, normalVectors, i3)
+        Vec3.fromArray(torsionVector, binormalVectors, i3)
+
+        Vec3.add(tA, positionVector, verticalVector)
+        Vec3.negate(tB, torsionVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        Vec3.sub(tA, positionVector, verticalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        Vec3.add(tA, positionVector, verticalVector)
+        Vec3.copy(tB, torsionVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        Vec3.sub(tA, positionVector, verticalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+    }
+
+    for (let i = 0; i < linearSegments; ++i) {
+        ChunkedArray.add3(
+            indices,
+            vertexCount + i * 4,
+            vertexCount + (i + 1) * 4 + 1,
+            vertexCount + i * 4 + 1
+        );
+        ChunkedArray.add3(
+            indices,
+            vertexCount + i * 4,
+            vertexCount + (i + 1) * 4,
+            vertexCount + (i + 1) * 4 + 1
+        );
+
+        ChunkedArray.add3(
+            indices,
+            vertexCount + i * 4 + 2 + 1,
+            vertexCount + (i + 1) * 4 + 2 + 1,
+            vertexCount + i * 4 + 2
+        );
+        ChunkedArray.add3(
+            indices,
+            vertexCount + i * 4 + 2,
+            vertexCount + (i + 1) * 4 + 2 + 1,
+            vertexCount + (i + 1) * 4 + 2
+        );
+    }
+
+    const addedVertexCount = (linearSegments + 1) * 4
+    for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(groups, currentGroup)
+}

+ 36 - 10
src/mol-geo/geometry/mesh/builder/sheet.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>
@@ -71,7 +71,7 @@ function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLi
 }
 
 /** set arrowHeight = 0 for no arrow */
-export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean) {
+export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, arrowHeight: number, startCap: boolean, endCap: boolean) {
     const { currentGroup, vertices, normals, indices, groups } = state
 
     let vertexCount = vertices.elementCount
@@ -84,6 +84,9 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
     }
 
     for (let i = 0; i <= linearSegments; ++i) {
+        const width = widthValues[i]
+        const height = heightValues[i]
+
         const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments);
         const i3 = i * 3
 
@@ -141,32 +144,55 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
     }
 
     for (let i = 0; i < linearSegments; ++i) {
-        for (let j = 0; j < 4; j++) {
+        // the triangles are arranged such that opposing triangles of the sheet align
+        // which prevents triangle intersection within tight curves
+        for (let j = 0; j < 2; j++) {
+            ChunkedArray.add3(
+                indices,
+                vertexCount + i * 8 + 2 * j, // a
+                vertexCount + (i + 1) * 8 + 2 * j + 1, // c
+                vertexCount + i * 8 + 2 * j + 1 // b
+            );
+            ChunkedArray.add3(
+                indices,
+                vertexCount + i * 8 + 2 * j, // a
+                vertexCount + (i + 1) * 8 + 2 * j, // d
+                vertexCount + (i + 1) * 8 + 2 * j + 1 // c
+            );
+        }
+        for (let j = 2; j < 4; j++) {
             ChunkedArray.add3(
                 indices,
-                vertexCount + i * 8 + 2 * j,
-                vertexCount + (i + 1) * 8 + 2 * j + 1,
-                vertexCount + i * 8 + 2 * j + 1
+                vertexCount + i * 8 + 2 * j, // a
+                vertexCount + (i + 1) * 8 + 2 * j, // d
+                vertexCount + i * 8 + 2 * j + 1, // b
             );
             ChunkedArray.add3(
                 indices,
-                vertexCount + i * 8 + 2 * j,
-                vertexCount + (i + 1) * 8 + 2 * j,
-                vertexCount + (i + 1) * 8 + 2 * j + 1
+                vertexCount + (i + 1) * 8 + 2 * j, // d
+                vertexCount + (i + 1) * 8 + 2 * j + 1, // c
+                vertexCount + i * 8 + 2 * j + 1, // b
             );
         }
     }
 
     if (startCap) {
+        const width = widthValues[0]
+        const height = heightValues[0]
         const h = arrowHeight === 0 ? height : arrowHeight
         addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h)
     } else if (arrowHeight > 0) {
+        const width = widthValues[0]
+        const height = heightValues[0]
         addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height)
         addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height)
     }
 
     if (endCap && arrowHeight === 0) {
-        addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, height, height)
+        const width = widthValues[linearSegments]
+        const height = heightValues[linearSegments]
+        // use negative height to flip the direction the cap's triangles are facing
+        addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, -height, -height)
     }
 
     const addedVertexCount = (linearSegments + 1) * 8 +

+ 10 - 2
src/mol-geo/geometry/mesh/builder/tube.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>
@@ -50,7 +50,15 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
             const t = 2 * Math.PI * j / radialSegments;
 
             add3AndScale2(surfacePoint, u, v, controlPoint, h * Math.cos(t), w * Math.sin(t))
-            add2AndScale2(normalVector, u, v, w * Math.cos(t), h * Math.sin(t))
+            if (radialSegments === 2) {
+                // add2AndScale2(normalVector, u, v, w * Math.cos(t), h * Math.sin(t))
+                Vec3.copy(normalVector, v)
+                console.log(i, t)
+                Vec3.normalize(normalVector, normalVector)
+                if (t !== 0 || i % 2 === 0) Vec3.negate(normalVector, normalVector)
+            } else {
+                add2AndScale2(normalVector, u, v, w * Math.cos(t), h * Math.sin(t))
+            }
             Vec3.normalize(normalVector, normalVector)
 
             ChunkedArray.add3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);

+ 6 - 4
src/mol-geo/primitive/polygon.ts

@@ -8,16 +8,18 @@
  * Create 3d points for a polygon:
  * 3 for a triangle, 4 for a rectangle, 5 for a pentagon, 6 for a hexagon...
  */
-export function polygon(sideCount: number, shift: boolean) {
+export function polygon(sideCount: number, shift: boolean, radius = -1) {
     const points = new Float32Array(sideCount * 3)
-    const radius = sideCount <= 4 ? Math.sqrt(2) / 2 : 0.6
+    const r = radius === -1
+        ? (sideCount <= 4 ? Math.sqrt(2) / 2 : 0.6)
+        : radius
 
     const offset = shift ? 1 : 0
 
     for (let i = 0, il = sideCount; i < il; ++i) {
         const c = (i * 2 + offset) / sideCount * Math.PI
-        points[i * 3] = Math.cos(c) * radius
-        points[i * 3 + 1] = Math.sin(c) * radius
+        points[i * 3] = Math.cos(c) * r
+        points[i * 3 + 1] = Math.sin(c) * r
         points[i * 3 + 2] = 0
     }
     return points

+ 22 - 2
src/mol-geo/primitive/primitive.ts

@@ -1,10 +1,12 @@
 /**
- * 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 { Vec3, Mat4, Mat3 } from '../../mol-math/linear-algebra';
+import { getNormalMatrix } from '../util';
+import { NumberArray } from '../../mol-util/type-helpers';
 
 export interface Primitive {
     vertices: ArrayLike<number>
@@ -56,4 +58,22 @@ export function PrimitiveBuilder(triangleCount: number): PrimitiveBuilder {
         },
         getPrimitive: () => ({ vertices, normals, indices })
     }
+}
+
+const tmpV = Vec3.zero()
+const tmpMat3 = Mat3.zero()
+
+/** Transform primitive in-place */
+export function transformPrimitive(primitive: Primitive, t: Mat4) {
+    const { vertices, normals } = primitive
+    const n = getNormalMatrix(tmpMat3, t)
+    for (let i = 0, il = vertices.length; i < il; i += 3) {
+        // position
+        Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, vertices, i), t)
+        Vec3.toArray(tmpV, vertices as NumberArray, i)
+        // normal
+        Vec3.transformMat3(tmpV, Vec3.fromArray(tmpV, normals, i), n)
+        Vec3.toArray(tmpV, normals as NumberArray, i)
+    }
+    return primitive
 }

+ 35 - 17
src/mol-geo/primitive/prism.ts

@@ -9,26 +9,39 @@ import { Primitive, PrimitiveBuilder } from './primitive';
 import { polygon } from './polygon'
 import { Cage } from './cage';
 
-const on = Vec3.create(0, 0, -0.5), op = Vec3.create(0, 0, 0.5)
-const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
+const on = Vec3(), op = Vec3()
+const a = Vec3(), b = Vec3(), c = Vec3(), d = Vec3()
+
+export const DefaultPrismProps = {
+    height: 1,
+    topCap: true,
+    bottomCap: true,
+}
+export type PrismProps = Partial<typeof DefaultPrismProps>
 
 /**
  * Create a prism with a base of 4 or more points
  */
-export function Prism(points: ArrayLike<number>): Primitive {
+export function Prism(points: ArrayLike<number>, props?: PrismProps): Primitive {
     const sideCount = points.length / 3
     if (sideCount < 4) throw new Error('need at least 4 points to build a prism')
 
+    const { height, topCap, bottomCap } = { ...DefaultPrismProps, ...props };
+
     const count = 4 * sideCount
     const builder = PrimitiveBuilder(count)
+    const halfHeight = height * 0.5
+
+    Vec3.set(on, 0, 0, -halfHeight)
+    Vec3.set(op, 0, 0, halfHeight)
 
     // create sides
     for (let i = 0; i < sideCount; ++i) {
         const ni = (i + 1) % sideCount
-        Vec3.set(a, points[i * 3], points[i * 3 + 1], -0.5)
-        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5)
-        Vec3.set(c, points[ni * 3], points[ni * 3 + 1], 0.5)
-        Vec3.set(d, points[i * 3], points[i * 3 + 1], 0.5)
+        Vec3.set(a, points[i * 3], points[i * 3 + 1], -halfHeight)
+        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -halfHeight)
+        Vec3.set(c, points[ni * 3], points[ni * 3 + 1], halfHeight)
+        Vec3.set(d, points[i * 3], points[i * 3 + 1], halfHeight)
         builder.add(a, b, c)
         builder.add(c, d, a)
     }
@@ -36,12 +49,16 @@ export function Prism(points: ArrayLike<number>): Primitive {
     // create bases
     for (let i = 0; i < sideCount; ++i) {
         const ni = (i + 1) % sideCount
-        Vec3.set(a, points[i * 3], points[i * 3 + 1], -0.5)
-        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5)
-        builder.add(on, b, a)
-        Vec3.set(a, points[i * 3], points[i * 3 + 1], 0.5)
-        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], 0.5)
-        builder.add(a, b, op)
+        if (topCap) {
+            Vec3.set(a, points[i * 3], points[i * 3 + 1], -halfHeight)
+            Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -halfHeight)
+            builder.add(on, b, a)
+        }
+        if (bottomCap) {
+            Vec3.set(a, points[i * 3], points[i * 3 + 1], halfHeight)
+            Vec3.set(b, points[ni * 3], points[ni * 3 + 1], halfHeight)
+            builder.add(a, b, op)
+        }
     }
 
     return builder.getPrimitive()
@@ -70,20 +87,21 @@ export function HexagonalPrism() {
 /**
  * Create a prism cage
  */
-export function PrismCage(points: ArrayLike<number>): Cage {
+export function PrismCage(points: ArrayLike<number>, height = 1): Cage {
     const sideCount = points.length / 3
 
-    // const count = 4 * sideCount
     const vertices: number[] = []
     const edges: number[] = []
 
+    const halfHeight = height * 0.5
+
     let offset = 0
 
     // vertices and side edges
     for (let i = 0; i < sideCount; ++i) {
         vertices.push(
-            points[i * 3], points[i * 3 + 1], -0.5,
-            points[i * 3], points[i * 3 + 1], 0.5
+            points[i * 3], points[i * 3 + 1], -halfHeight,
+            points[i * 3], points[i * 3 + 1], halfHeight
         )
         edges.push(offset, offset + 1)
         offset += 2

+ 1 - 1
src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts

@@ -31,7 +31,7 @@ export const AssemblySymmetryAxesParams = {
     sizeFactor: PD.Numeric(0.4, { min: 0, max: 3, step: 0.01 }),
 
     ...ComplexMeshParams,
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
 }
 export type AssemblySymmetryAxesParams = typeof AssemblySymmetryAxesParams

+ 1 - 1
src/mol-repr/structure/visual/nucleotide-block-mesh.ts

@@ -36,7 +36,7 @@ const box = Box()
 
 export const NucleotideBlockMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
 }
 export const DefaultNucleotideBlockMeshProps = PD.getDefaultValues(NucleotideBlockMeshParams)
 export type NucleotideBlockMeshProps = typeof DefaultNucleotideBlockMeshProps

+ 1 - 1
src/mol-repr/structure/visual/nucleotide-ring-mesh.ts

@@ -35,7 +35,7 @@ const normal = Vec3.zero()
 
 export const NucleotideRingMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
 }
 export const DefaultNucleotideRingMeshProps = PD.getDefaultValues(NucleotideRingMeshParams)

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

@@ -21,7 +21,7 @@ import { VisualUpdateState } from '../../util';
 
 export const PolymerBackboneCylinderParams = {
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
 }
 export const DefaultPolymerBackboneCylinderProps = PD.getDefaultValues(PolymerBackboneCylinderParams)
 export type PolymerBackboneCylinderProps = typeof DefaultPolymerBackboneCylinderProps

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

@@ -23,7 +23,7 @@ const segmentCount = 10
 
 export const PolymerGapCylinderParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
 }
 export const DefaultPolymerGapCylinderProps = PD.getDefaultValues(PolymerGapCylinderParams)
 export type PolymerGapCylinderProps = typeof DefaultPolymerGapCylinderProps

+ 31 - 7
src/mol-repr/structure/visual/polymer-trace-mesh.ts

@@ -17,13 +17,14 @@ import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
 import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { ComputedSecondaryStructure } from '../../../mol-model-props/computed/secondary-structure';
+import { addRibbon } from '../../../mol-geo/geometry/mesh/builder/ribbon';
 
 export const PolymerTraceMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
     linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }),
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
     aspectRatio: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }),
-    arrowFactor: PD.Numeric(1.5, { min: 0.1, max: 5, step: 0.1 }),
+    arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }),
 }
 export const DefaultPolymerTraceMeshProps = PD.getDefaultValues(PolymerTraceMeshParams)
 export type PolymerTraceMeshProps = typeof DefaultPolymerTraceMeshProps
@@ -67,9 +68,18 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
         }
 
         if (isSheet) {
+            const h0 = w0 * aspectRatio
             const h1 = w1 * aspectRatio
+            const h2 = w2 * aspectRatio
             const arrowHeight = v.secStrucLast ? h1 * arrowFactor : 0
-            addSheet(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, w1, h1, arrowHeight, v.secStrucFirst, v.secStrucLast)
+
+            interpolateSizes(state, w0, w1, w2, h0, h1, h2, shift)
+
+            if (radialSegments === 2) {
+                addRibbon(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, widthValues, heightValues, arrowHeight)
+            } else {
+                addSheet(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, widthValues, heightValues, arrowHeight, v.secStrucFirst, v.secStrucLast)
+            }
         } else {
             let h0: number, h1: number, h2: number
             if (isHelix && !v.isCoarseBackbone) {
@@ -78,18 +88,32 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
                 h2 = w2 * aspectRatio
             } else if (isNucleicType && !v.isCoarseBackbone) {
                 h0 = w0 * aspectRatio;
-                [w0, h0] = [h0, w0]
                 h1 = w1 * aspectRatio;
-                [w1, h1] = [h1, w1]
                 h2 = w2 * aspectRatio;
-                [w2, h2] = [h2, w2]
+                [w0, h0] = [h0, w0];
+                [w1, h1] = [h1, w1];
+                [w2, h2] = [h2, w2];
             } else {
                 h0 = w0
                 h1 = w1
                 h2 = w2
             }
+
             interpolateSizes(state, w0, w1, w2, h0, h1, h2, shift)
-            addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, 1, v.secStrucFirst || v.coarseBackboneFirst, v.secStrucLast || v.coarseBackboneLast)
+
+            if (radialSegments === 2) {
+                if (isNucleicType && !v.isCoarseBackbone) {
+                    // TODO find a cleaner way to swap normal and binormal for nucleic types
+                    for (let i = 0, il = binormalVectors.length; i < il; i++) binormalVectors[i] *= -1
+                    addRibbon(builderState, curvePoints, binormalVectors, normalVectors, linearSegments, heightValues, widthValues, 0)
+                } else {
+                    addRibbon(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, widthValues, heightValues, 0)
+                }
+            } else if (radialSegments === 4) {
+                addSheet(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, widthValues, heightValues, 0, v.secStrucFirst || v.coarseBackboneFirst, v.secStrucLast || v.coarseBackboneLast)
+            } else {
+                addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, 1, v.secStrucFirst || v.coarseBackboneFirst, v.secStrucLast || v.coarseBackboneLast)
+            }
         }
 
         ++i

+ 11 - 2
src/mol-repr/structure/visual/polymer-tube-mesh.ts

@@ -15,11 +15,13 @@ import { isNucleic } from '../../../mol-model/structure/model/types';
 import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
 import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
 import { VisualUpdateState } from '../../util';
+import { addSheet } from '../../../mol-geo/geometry/mesh/builder/sheet';
+import { addRibbon } from '../../../mol-geo/geometry/mesh/builder/ribbon';
 
 export const PolymerTubeMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
     linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }),
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
 }
 export const DefaultPolymerTubeMeshProps = PD.getDefaultValues(PolymerTubeMeshParams)
 export type PolymerTubeMeshProps = typeof DefaultPolymerTubeMeshProps
@@ -55,7 +57,14 @@ function createPolymerTubeMesh(ctx: VisualContext, unit: Unit, structure: Struct
         let s2 = theme.size.size(v.centerNext) * sizeFactor
 
         interpolateSizes(state, s0, s1, s2, s0, s1, s2, shift)
-        addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, 1, v.first, v.last)
+
+        if (radialSegments === 2) {
+            addRibbon(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, widthValues, heightValues, 0)
+        } else if (radialSegments === 4) {
+            addSheet(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, widthValues, heightValues, 0, v.secStrucFirst || v.coarseBackboneFirst, v.secStrucLast || v.coarseBackboneLast)
+        } else {
+            addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, 1, v.first, v.last)
+        }
 
         ++i
     }

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

@@ -18,7 +18,7 @@ import { VisualContext } from '../../../../mol-repr/visual';
 export const LinkCylinderParams = {
     linkScale: PD.Numeric(0.4, { min: 0, max: 1, step: 0.1 }),
     linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
-    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }),
 }
 export const DefaultLinkCylinderProps = PD.getDefaultValues(LinkCylinderParams)
 export type LinkCylinderProps = typeof DefaultLinkCylinderProps