ソースを参照

add helix profile option to cartoon repr

Alexander Rose 3 年 前
コミット
1578211157

+ 1 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Added ``ViewerOptions.collapseRightPanel``
 - Added ``Viewer.loadTrajectory`` to support loading "composed" trajectories (e.g. from gro + xtc)
 - Fix: handle parent in Structure.remapModel
+- Add ``rounded`` and ``square`` helix profile options to Cartoon representation (in addition to the default ``elliptical``)
 
 ## [v2.3.6] - 2021-11-8
 

+ 18 - 11
src/mol-geo/geometry/mesh/builder/sheet.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -40,7 +40,7 @@ const v3set = Vec3.set;
 const caAdd3 = ChunkedArray.add3;
 const caAdd = ChunkedArray.add;
 
-function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
+function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number, flip: boolean) {
     const { vertices, normals, indices } = state;
     const vertexCount = vertices.elementCount;
 
@@ -74,11 +74,19 @@ function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLi
         v3copy(verticalVector, verticalLeftVector);
     }
 
-    for (let i = 0; i < 4; ++i) {
-        caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
+    if (flip) {
+        for (let i = 0; i < 4; ++i) {
+            caAdd3(normals, -normalVector[0], -normalVector[1], -normalVector[2]);
+        }
+        caAdd3(indices, vertexCount, vertexCount + 1, vertexCount + 2);
+        caAdd3(indices, vertexCount + 2, vertexCount + 3, vertexCount);
+    } else {
+        for (let i = 0; i < 4; ++i) {
+            caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
+        }
+        caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+        caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
     }
-    caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
-    caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
 }
 
 /** set arrowHeight = 0 for no arrow */
@@ -193,19 +201,18 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
         const width = widthValues[0];
         const height = heightValues[0];
         const h = arrowHeight === 0 ? height : arrowHeight;
-        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h);
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h, false);
     } 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);
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height, false);
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height, false);
     }
 
     if (endCap && arrowHeight === 0) {
         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);
+        addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, height, height, true);
     }
 
     const addedVertexCount = (linearSegments + 1) * 8 +

+ 60 - 20
src/mol-geo/geometry/mesh/builder/tube.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -30,9 +30,10 @@ function add3AndScale2(out: Vec3, a: Vec3, b: Vec3, c: Vec3, sa: number, sb: num
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3fromArray = Vec3.fromArray;
 const v3normalize = Vec3.normalize;
-const v3negate = Vec3.negate;
-const v3copy = Vec3.copy;
+const v3scaleAndAdd = Vec3.scaleAndAdd;
 const v3cross = Vec3.cross;
+const v3dot = Vec3.dot;
+const v3unitX = Vec3.unitX;
 const caAdd3 = ChunkedArray.add3;
 
 const CosSinCache = new Map<number, { cos: number[], sin: number[] }>();
@@ -50,13 +51,16 @@ function getCosSin(radialSegments: number) {
     return CosSinCache.get(radialSegments)!;
 }
 
-export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean) {
+export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean, crossSection: 'elliptical' | 'rounded') {
     const { currentGroup, vertices, normals, indices, groups } = state;
 
     let vertexCount = vertices.elementCount;
 
     const { cos, sin } = getCosSin(radialSegments);
 
+    const q1 = radialSegments / 4;
+    const q3 = q1 * 3;
+
     for (let i = 0; i <= linearSegments; ++i) {
         const i3 = i * 3;
         v3fromArray(u, normalVectors, i3);
@@ -65,14 +69,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
 
         const width = widthValues[i];
         const height = heightValues[i];
+        const rounded = crossSection === 'rounded' && height > width;
 
         for (let j = 0; j < radialSegments; ++j) {
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
-            if (radialSegments === 2) {
-                v3copy(normalVector, v);
-                v3normalize(normalVector, normalVector);
-                if (j !== 0 || i % 2 === 0) v3negate(normalVector, normalVector);
+            if (rounded) {
+                add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[j], width * sin[j]);
+                const h = v3dot(v, v3unitX) < 0
+                    ? (j < q1 || j >= q3) ? height - width : -height + width
+                    : (j >= q1 && j < q3) ? -height + width : height - width;
+                v3scaleAndAdd(surfacePoint, surfacePoint, u, h);
+                add2AndScale2(normalVector, u, v, cos[j], sin[j]);
             } else {
+                add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
                 add2AndScale2(normalVector, u, v, width * cos[j], height * sin[j]);
             }
             v3normalize(normalVector, normalVector);
@@ -82,19 +90,37 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         }
     }
 
+    const radialSegmentsHalf = Math.round(radialSegments / 2);
+
     for (let i = 0; i < linearSegments; ++i) {
-        for (let j = 0; j < radialSegments; ++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 < radialSegmentsHalf; ++j) {
+            caAdd3(
+                indices,
+                vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
+                vertexCount + i * radialSegments + j // b
+            );
+            caAdd3(
+                indices,
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
+                vertexCount + (i + 1) * radialSegments + j, // d
+                vertexCount + i * radialSegments + j // b
+            );
+        }
+        for (let j = radialSegmentsHalf; j < radialSegments; ++j) {
             caAdd3(
                 indices,
-                vertexCount + i * radialSegments + (j + 1) % radialSegments,
-                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
-                vertexCount + i * radialSegments + j
+                vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
+                vertexCount + (i + 1) * radialSegments + j, // d
+                vertexCount + i * radialSegments + j // b
             );
             caAdd3(
                 indices,
-                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
-                vertexCount + (i + 1) * radialSegments + j,
-                vertexCount + i * radialSegments + j
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
+                vertexCount + (i + 1) * radialSegments + j, // d
+                vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
             );
         }
     }
@@ -111,11 +137,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
         const width = widthValues[0];
-        const height = heightValues[0];
+        let height = heightValues[0];
+        const rounded = crossSection === 'rounded' && height > width;
+        if (rounded) height -= width;
 
         vertexCount = vertices.elementCount;
         for (let i = 0; i < radialSegments; ++i) {
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            if (rounded) {
+                add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[i], width * sin[i]);
+                v3scaleAndAdd(surfacePoint, surfacePoint, u, (i < q1 || i >= q3) ? height : -height);
+            } else {
+                add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            }
 
             caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
             caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
@@ -141,11 +174,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
         const width = widthValues[linearSegments];
-        const height = heightValues[linearSegments];
+        let height = heightValues[linearSegments];
+        const rounded = crossSection === 'rounded' && height > width;
+        if (rounded) height -= width;
 
         vertexCount = vertices.elementCount;
         for (let i = 0; i < radialSegments; ++i) {
-            add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            if (rounded) {
+                add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[i], width * sin[i]);
+                v3scaleAndAdd(surfacePoint, surfacePoint, u, (i < q1 || i >= q3) ? height : -height);
+            } else {
+                add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
+            }
 
             caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
             caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);

+ 20 - 13
src/mol-repr/structure/visual/polymer-trace-mesh.ts

@@ -27,8 +27,9 @@ import { StructureGroup } from './util/common';
 export const PolymerTraceMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
     aspectRatio: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }),
-    arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }),
-    tubularHelices: PD.Boolean(false),
+    arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Size factor for sheet arrows' }),
+    tubularHelices: PD.Boolean(false, { description: 'Draw alpha helices as tubes' }),
+    helixProfile: PD.Select('elliptical', PD.arrayToOptions(['elliptical', 'rounded', 'square'] as const), { description: 'Protein and nucleic helix trace profile' }),
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo)
@@ -42,7 +43,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
     const polymerElementCount = unit.polymerElements.length;
 
     if (!polymerElementCount) return Mesh.createEmpty(mesh);
-    const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices } = props;
+    const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices, helixProfile } = props;
 
     const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2;
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
@@ -131,9 +132,6 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
                 h0 = w0 * aspectRatio;
                 h1 = w1 * aspectRatio;
                 h2 = w2 * aspectRatio;
-                [w0, h0] = [h0, w0];
-                [w1, h1] = [h1, w1];
-                [w2, h2] = [h2, w2];
             } else {
                 h0 = w0;
                 h1 = w1;
@@ -142,18 +140,26 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
 
             interpolateSizes(state, w0, w1, w2, h0, h1, h2, shift);
 
+            const [normals, binormals] = isNucleicType && !v.isCoarseBackbone ? [binormalVectors, normalVectors] : [normalVectors, binormalVectors];
+            if (isNucleicType && !v.isCoarseBackbone) {
+                // TODO: find a cleaner way to swap normal and binormal for nucleic types
+                for (let i = 0, il = normals.length; i < il; i++) normals[i] *= -1;
+            }
+
             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, segmentCount, heightValues, widthValues, 0);
+                    addRibbon(builderState, curvePoints, normals, binormals, segmentCount, heightValues, widthValues, 0);
                 } else {
-                    addRibbon(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0);
+                    addRibbon(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0);
                 }
             } else if (radialSegments === 4) {
-                addSheet(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0, startCap, endCap);
+                addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
+            } else if (h1 === w1) {
+                addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, 'elliptical');
+            } else if (helixProfile === 'square') {
+                addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
             } else {
-                addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap);
+                addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, helixProfile);
             }
         }
 
@@ -189,7 +195,8 @@ export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTrace
                 newProps.linearSegments !== currentProps.linearSegments ||
                 newProps.radialSegments !== currentProps.radialSegments ||
                 newProps.aspectRatio !== currentProps.aspectRatio ||
-                newProps.arrowFactor !== currentProps.arrowFactor
+                newProps.arrowFactor !== currentProps.arrowFactor ||
+                newProps.helixProfile !== currentProps.helixProfile
             );
 
             const secondaryStructureHash = SecondaryStructureProvider.get(newStructureGroup.structure).version;

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

@@ -93,7 +93,7 @@ function createPolymerTubeMesh(ctx: VisualContext, unit: Unit, structure: Struct
         } else if (radialSegments === 4) {
             addSheet(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0, startCap, endCap);
         } else {
-            addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap);
+            addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, 'elliptical');
         }
 
         ++i;