UNPKG

molstar

Version:

A comprehensive macromolecular library.

383 lines (382 loc) 19.6 kB
/** * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Michal Malý <michal.maly@ibt.cas.cz> * @author Jiří Černý <jiri.cerny@ibt.cas.cz> */ import { NtCTubeProvider } from './property'; import { NtCTubeSegmentsIterator } from './util'; import { NtCTubeTypes as NTT } from './types'; import { Dnatco } from '../property'; import { DnatcoUtil } from '../util'; import { Interval } from '../../../mol-data/int'; import { BaseGeometry } from '../../../mol-geo/geometry/base'; import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; import { addFixedCountDashedCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube'; import { LocationIterator } from '../../../mol-geo/util/location-iterator'; import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d'; import { Vec3 } from '../../../mol-math/linear-algebra'; import { smoothstep } from '../../../mol-math/interpolate'; import { NullLocation } from '../../../mol-model/location'; import { EmptyLoci } from '../../../mol-model/loci'; import { Structure, StructureElement, Unit } from '../../../mol-model/structure'; import { structureUnion } from '../../../mol-model/structure/query/utils/structure-set'; import { Representation } from '../../../mol-repr/representation'; import { StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation'; import { UnitsMeshParams, UnitsMeshVisual } from '../../../mol-repr/structure/units-visual'; import { createCurveSegmentState } from '../../../mol-repr/structure/visual/util/polymer'; import { getStructureQuality } from '../../../mol-repr/util'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; const v3add = Vec3.add; const v3copy = Vec3.copy; const v3cross = Vec3.cross; const v3fromArray = Vec3.fromArray; const v3matchDirection = Vec3.matchDirection; const v3normalize = Vec3.normalize; const v3orthogonalize = Vec3.orthogonalize; const v3scale = Vec3.scale; const v3slerp = Vec3.slerp; const v3spline = Vec3.spline; const v3sub = Vec3.sub; const v3toArray = Vec3.toArray; const NtCTubeMeshParams = { ...UnitsMeshParams, linearSegments: PD.Numeric(4, { min: 2, max: 8, step: 1 }, BaseGeometry.CustomQualityParamInfo), radialSegments: PD.Numeric(22, { min: 4, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo), residueMarkerWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }), segmentBoundaryWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }), }; const LinearSegmentCount = { highest: 6, higher: 6, high: 4, medium: 4, low: 3, lower: 3, lowest: 2, }; const RadialSegmentCount = { highest: 32, higher: 26, high: 22, medium: 18, low: 14, lower: 10, lowest: 6, }; const _curvePoint = Vec3(); const _tanA = Vec3(); const _tanB = Vec3(); const _firstTangentVec = Vec3(); const _lastTangentVec = Vec3(); const _firstNormalVec = Vec3(); const _lastNormalVec = Vec3(); const _tmpNormal = Vec3(); const _tangentVec = Vec3(); const _normalVec = Vec3(); const _binormalVec = Vec3(); const _prevNormal = Vec3(); const _nextNormal = Vec3(); function interpolatePointsAndTangents(state, p0, p1, p2, p3, tRange) { const { curvePoints, tangentVectors, linearSegments } = state; const tension = 0.5; const r = tRange[1] - tRange[0]; for (let j = 0; j <= linearSegments; ++j) { const t = j * r / linearSegments + tRange[0]; v3spline(_curvePoint, p0, p1, p2, p3, t, tension); v3spline(_tanA, p0, p1, p2, p3, t - 0.01, tension); v3spline(_tanB, p0, p1, p2, p3, t + 0.01, tension); v3toArray(_curvePoint, curvePoints, j * 3); v3normalize(_tangentVec, v3sub(_tangentVec, _tanA, _tanB)); v3toArray(_tangentVec, tangentVectors, j * 3); } } function interpolateNormals(state, firstDirection, lastDirection) { const { curvePoints, tangentVectors, normalVectors, binormalVectors } = state; const n = curvePoints.length / 3; v3fromArray(_firstTangentVec, tangentVectors, 0); v3fromArray(_lastTangentVec, tangentVectors, (n - 1) * 3); v3orthogonalize(_firstNormalVec, _firstTangentVec, firstDirection); v3orthogonalize(_lastNormalVec, _lastTangentVec, lastDirection); v3matchDirection(_lastNormalVec, _lastNormalVec, _firstNormalVec); v3copy(_prevNormal, _firstNormalVec); const n1 = n - 1; for (let i = 0; i < n; ++i) { const j = smoothstep(0, n1, i) * n1; const t = i === 0 ? 0 : 1 / (n - j); v3fromArray(_tangentVec, tangentVectors, i * 3); v3orthogonalize(_normalVec, _tangentVec, v3slerp(_tmpNormal, _prevNormal, _lastNormalVec, t)); v3toArray(_normalVec, normalVectors, i * 3); v3copy(_prevNormal, _normalVec); v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec)); v3toArray(_binormalVec, binormalVectors, i * 3); } for (let i = 1; i < n1; ++i) { v3fromArray(_prevNormal, normalVectors, (i - 1) * 3); v3fromArray(_normalVec, normalVectors, i * 3); v3fromArray(_nextNormal, normalVectors, (i + 1) * 3); v3scale(_normalVec, v3add(_normalVec, _prevNormal, v3add(_normalVec, _nextNormal, _normalVec)), 1 / 3); v3toArray(_normalVec, normalVectors, i * 3); v3fromArray(_tangentVec, tangentVectors, i * 3); v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec)); v3toArray(_binormalVec, binormalVectors, i * 3); } } function interpolate(state, p0, p1, p2, p3, firstDir, lastDir, tRange = [0, 1]) { interpolatePointsAndTangents(state, p0, p1, p2, p3, tRange); interpolateNormals(state, firstDir, lastDir); } function createNtCTubeSegmentsIterator(structureGroup) { var _a, _b; const { structure, group } = structureGroup; const instanceCount = group.units.length; const data = (_b = (_a = NtCTubeProvider.get(structure.model)) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.data; if (!data) return LocationIterator(0, 1, 1, () => NullLocation); const numBlocks = data.data.steps.length * 4; const getLocation = (groupId, instanceId) => { if (groupId > numBlocks) return NullLocation; const stepIdx = Math.floor(groupId / 4); const step = data.data.steps[stepIdx]; const r = groupId % 4; const kind = r === 0 ? 'upper' : r === 1 ? 'lower' : r === 2 ? 'residue-boundary' : 'segment-boundary'; return NTT.Location({ step, kind }); }; return LocationIterator(totalMeshGroupsCount(data.data.steps) + 1, instanceCount, 1, getLocation); } function segmentCount(structure, props) { const quality = props.quality; if (quality === 'custom') return { linear: props.linearSegments, radial: props.radialSegments }; else if (quality === 'auto') { const autoQuality = getStructureQuality(structure); return { linear: LinearSegmentCount[autoQuality], radial: RadialSegmentCount[autoQuality] }; } else return { linear: LinearSegmentCount[quality], radial: RadialSegmentCount[quality] }; } function stepBoundingSphere(step, struLoci) { const one = DnatcoUtil.residueToLoci(step.auth_asym_id_1, step.auth_seq_id_1, step.label_alt_id_1, step.PDB_ins_code_1, struLoci, 'auth'); const two = DnatcoUtil.residueToLoci(step.auth_asym_id_2, step.auth_seq_id_2, step.label_alt_id_2, step.PDB_ins_code_2, struLoci, 'auth'); if (StructureElement.Loci.is(one) && StructureElement.Loci.is(two)) { const union = structureUnion(struLoci.structure, [StructureElement.Loci.toStructure(one), StructureElement.Loci.toStructure(two)]); return union.boundary.sphere; } return void 0; } function totalMeshGroupsCount(steps) { // Each segment has two blocks, Residue Boundary marker and a Segment Boundary marker return steps.length * 4 - 1; // Subtract one because the last Segment Boundary marker is not drawn } function createNtCTubeMesh(ctx, unit, structure, theme, props, mesh) { if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh); const prop = NtCTubeProvider.get(structure.model).value; if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh); const { data } = prop.data; if (data.steps.length === 0) return Mesh.createEmpty(mesh); const MarkerLinearSegmentCount = 2; const segCount = segmentCount(structure, props); const vertexCount = Math.floor((segCount.linear * 4 * data.steps.length / structure.model.atomicHierarchy.chains._rowCount) * segCount.radial); const chunkSize = Math.floor(vertexCount / 3); const diameter = 1.0 * theme.size.props.value; const mb = MeshBuilder.createState(vertexCount, chunkSize, mesh); const state = createCurveSegmentState(segCount.linear); const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state; for (let idx = 0; idx <= segCount.linear; idx++) { widthValues[idx] = diameter; heightValues[idx] = diameter; } const [normals, binormals] = [binormalVectors, normalVectors]; // Needed so that the tube is not drawn from inside out const markerState = createCurveSegmentState(MarkerLinearSegmentCount); const { curvePoints: mCurvePoints, normalVectors: mNormalVectors, binormalVectors: mBinormalVectors, widthValues: mWidthValues, heightValues: mHeightValues } = markerState; for (let idx = 0; idx <= MarkerLinearSegmentCount; idx++) { mWidthValues[idx] = diameter; mHeightValues[idx] = diameter; } const [mNormals, mBinormals] = [mBinormalVectors, mNormalVectors]; const firstDir = Vec3(); const lastDir = Vec3(); const markerDir = Vec3(); const residueMarkerWidth = props.residueMarkerWidth / 2; const it = new NtCTubeSegmentsIterator(structure, unit); while (it.hasNext) { const segment = it.move(); if (!segment) continue; const { p_1, p0, p1, p2, p3, p4, pP } = segment; const FirstBlockId = segment.stepIdx * 4; const SecondBlockId = FirstBlockId + 1; const ResidueMarkerId = FirstBlockId + 2; const SegmentBoundaryMarkerId = FirstBlockId + 3; const { rmShift, rmPos } = calcResidueMarkerShift(p2, p3, pP); if (segment.firstInChain) { v3normalize(firstDir, v3sub(firstDir, p2, p1)); v3normalize(lastDir, v3sub(lastDir, rmPos, p2)); } else { v3copy(firstDir, lastDir); v3normalize(lastDir, v3sub(lastDir, rmPos, p2)); } // C5' -> O3' block interpolate(state, p0, p1, p2, p3, firstDir, lastDir); mb.currentGroup = FirstBlockId; addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, segment.firstInChain || segment.followsGap, false, 'rounded'); // O3' -> C5' block v3copy(firstDir, lastDir); v3normalize(markerDir, v3sub(markerDir, p3, rmPos)); v3normalize(lastDir, v3sub(lastDir, p4, p3)); // From O3' to the residue marker interpolate(state, p1, p2, p3, p4, firstDir, markerDir, [0, rmShift - residueMarkerWidth]); mb.currentGroup = SecondBlockId; addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded'); // Residue marker interpolate(markerState, p1, p2, p3, p4, markerDir, markerDir, [rmShift - residueMarkerWidth, rmShift + residueMarkerWidth]); mb.currentGroup = ResidueMarkerId; addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded'); if (segment.capEnd) { // From the residue marker to C5' of the end interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1]); mb.currentGroup = SecondBlockId; addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, true, 'rounded'); } else { // From the residue marker to C5' of the step boundary marker interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1 - props.segmentBoundaryWidth]); mb.currentGroup = SecondBlockId; addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded'); // Step boundary marker interpolate(markerState, p1, p2, p3, p4, lastDir, lastDir, [1 - props.segmentBoundaryWidth, 1]); mb.currentGroup = SegmentBoundaryMarkerId; addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded'); } if (segment.followsGap) { const cylinderProps = { radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial, }; mb.currentGroup = FirstBlockId; addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, false, cylinderProps); } } const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1.05); const m = MeshBuilder.getMesh(mb); m.setBoundingSphere(boundingSphere); return m; } const _rmvCO = Vec3(); const _rmvPO = Vec3(); const _rmPos = Vec3(); const _HalfPi = Math.PI / 2; function calcResidueMarkerShift(pO, pC, pP) { v3sub(_rmvCO, pC, pO); v3sub(_rmvPO, pP, pO); // Project position of P atom on the O3' -> C5' vector const beta = Vec3.angle(_rmvPO, _rmvCO); const alpha = _HalfPi - Math.abs(beta); const lengthMO = Math.cos(alpha) * Vec3.magnitude(_rmvPO); const shift = lengthMO / Vec3.magnitude(_rmvCO); v3scale(_rmvCO, _rmvCO, shift); v3add(_rmPos, _rmvCO, pO); return { rmShift: shift, rmPos: _rmPos }; } function getNtCTubeSegmentLoci(pickingId, structureGroup, id) { var _a, _b, _c; const { groupId, objectId, instanceId } = pickingId; if (objectId !== id) return EmptyLoci; const { structure } = structureGroup; const unit = structureGroup.group.units[instanceId]; if (!Unit.isAtomic(unit)) return EmptyLoci; const data = (_c = (_b = (_a = NtCTubeProvider.get(structure.model)) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.data) !== null && _c !== void 0 ? _c : undefined; if (!data) return EmptyLoci; const MeshGroupsCount = totalMeshGroupsCount(data.data.steps); if (groupId > MeshGroupsCount) return EmptyLoci; const stepIdx = Math.floor(groupId / 4); const bs = stepBoundingSphere(data.data.steps[stepIdx], Structure.toStructureElementLoci(structure)); /* * NOTE 1) Each step is drawn with 4 mesh groups. We need to divide/multiply by 4 to convert between steps and mesh groups. * NOTE 2) Molstar will create a mesh only for the asymmetric unit. When the entire biological assembly * is displayed, Molstar just copies and transforms the mesh. This means that even though the mesh * might be displayed multiple times, groupIds of the individual blocks in the mesh will be the same. * If there are multiple copies of a mesh, Molstar needs to be able to tell which block belongs to which copy of the mesh. * To do that, Molstar adds an offset to groupIds of the copied meshes. Offset is calculated as follows: * * offset = NumberOfBlocks * UnitIndex * * "NumberOfBlocks" is the number of valid Location objects got from LocationIterator *or* the greatest groupId set by * the mesh generator - whichever is smaller. * * UnitIndex is the index of the Unit the mesh belongs to, starting from 0. (See "unitMap" in the Structure object). * We can also get this index from the value "instanceId" of the "pickingId" object. * * If this offset is not applied, picking a piece of one of the copied meshes would actually pick that piece in the original mesh. * This is particularly apparent with highlighting - hovering over items in a copied mesh incorrectly highlights those items in the source mesh. * * Molstar can take advantage of the fact that ElementLoci has a reference to the Unit object attached to it. Since we cannot attach ElementLoci * to a step, we need to calculate the offseted groupId here and pass it as part of the DataLoci. */ const offsetGroupId = stepIdx * 4 + (MeshGroupsCount + 1) * instanceId; return NTT.Loci(data.data.steps, [stepIdx], [offsetGroupId], bs); } function eachNtCTubeSegment(loci, structureGroup, apply) { if (NTT.isLoci(loci)) { const offsetGroupId = loci.elements[0]; return apply(Interval.ofBounds(offsetGroupId, offsetGroupId + 4)); } return false; } function NtCTubeVisual(materialId) { return UnitsMeshVisual({ defaultProps: PD.getDefaultValues(NtCTubeMeshParams), createGeometry: createNtCTubeMesh, createLocationIterator: createNtCTubeSegmentsIterator, getLoci: getNtCTubeSegmentLoci, eachLocation: eachNtCTubeSegment, setUpdateState: (state, newProps, currentProps) => { state.createGeometry = (newProps.quality !== currentProps.quality || newProps.residueMarkerWidth !== currentProps.residueMarkerWidth || newProps.segmentBoundaryWidth !== currentProps.segmentBoundaryWidth || newProps.doubleSided !== currentProps.doubleSided || newProps.alpha !== currentProps.alpha || newProps.linearSegments !== currentProps.linearSegments || newProps.radialSegments !== currentProps.radialSegments); } }, materialId); } const NtCTubeVisuals = { 'ntc-tube-symbol': (ctx, getParams) => UnitsRepresentation('NtC Tube Mesh', ctx, getParams, NtCTubeVisual), }; export const NtCTubeParams = { ...NtCTubeMeshParams }; export function getNtCTubeParams(ctx, structure) { return PD.clone(NtCTubeParams); } export function NtCTubeRepresentation(ctx, getParams) { return Representation.createMulti('NtC Tube', ctx, getParams, StructureRepresentationStateBuilder, NtCTubeVisuals); } export const NtCTubeRepresentationProvider = StructureRepresentationProvider({ name: 'ntc-tube', label: 'NtC Tube', description: 'Displays schematic representation of NtC conformers', factory: NtCTubeRepresentation, getParams: getNtCTubeParams, defaultValues: PD.getDefaultValues(NtCTubeParams), defaultColorTheme: { name: 'ntc-tube' }, defaultSizeTheme: { name: 'uniform', props: { value: 2.0 } }, isApplicable: (structure) => structure.models.every(m => Dnatco.isApplicable(m)), ensureCustomProperties: { attach: async (ctx, structure) => structure.models.forEach(m => NtCTubeProvider.attach(ctx, m, void 0, true)), detach: (data) => data.models.forEach(m => NtCTubeProvider.ref(m, false)), }, });