diff --git a/CHANGELOG.md b/CHANGELOG.md index 720111904f159b51468e4ebf2855ce260059735a..5c7347791352acfc7676256c61fa412bab87d7a9 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/src/mol-geo/geometry/mesh/builder/sheet.ts b/src/mol-geo/geometry/mesh/builder/sheet.ts index 0140d94ce00dcbe16ed0489faed6971f1e8345de..1a8cbfd91cec0ec5782581af80fd6c1a29a96d53 100644 --- a/src/mol-geo/geometry/mesh/builder/sheet.ts +++ b/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 + diff --git a/src/mol-geo/geometry/mesh/builder/tube.ts b/src/mol-geo/geometry/mesh/builder/tube.ts index ad4f1aa1068dd5e5f72df37b08a585760b310195..68a3189770d4b3664389da2bb633b7debfe779ee 100644 --- a/src/mol-geo/geometry/mesh/builder/tube.ts +++ b/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]); diff --git a/src/mol-repr/structure/visual/polymer-trace-mesh.ts b/src/mol-repr/structure/visual/polymer-trace-mesh.ts index 416c3f4be26f0565965219b8213de7c75ec544ce..b407f980c199c131ab27634565761e6b34195bb5 100644 --- a/src/mol-repr/structure/visual/polymer-trace-mesh.ts +++ b/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; diff --git a/src/mol-repr/structure/visual/polymer-tube-mesh.ts b/src/mol-repr/structure/visual/polymer-tube-mesh.ts index 1aee4f505893892deb0ef9f77341b5ba62ef4cd0..af0e7837eee9ab1612d3754958b98f284c973a31 100644 --- a/src/mol-repr/structure/visual/polymer-tube-mesh.ts +++ b/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;