diff --git a/src/mol-data/int/sorted-ranges.ts b/src/mol-data/int/sorted-ranges.ts index 032f73ff145468fd63804eafba710b728fc9330a..362db41241b705ed6fa806c89c8ec5536bede9dd 100644 --- a/src/mol-data/int/sorted-ranges.ts +++ b/src/mol-data/int/sorted-ranges.ts @@ -68,14 +68,17 @@ namespace SortedRanges { } constructor(private ranges: SortedRanges<T>, private set: OrderedSet<T>) { - if (ranges.length) { + // TODO cleanup, refactor to make it clearer + const min = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.min(set)) + const max = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.max(set)) + if (ranges.length && min !== max) { this.curIndex = this.getRangeIndex(OrderedSet.min(set)) this.maxIndex = Math.min(ranges.length - 2, this.getRangeIndex(OrderedSet.max(set))) this.curMin = this.ranges[this.curIndex] this.updateInterval() } - this.hasNext = ranges.length > 0 && this.curIndex <= this.maxIndex + this.hasNext = ranges.length > 0 && min !== max && this.curIndex <= this.maxIndex } } } diff --git a/src/mol-geo/primitive/sheet.ts b/src/mol-geo/primitive/sheet.ts index d6b81e8f921173df33b0951a9542ef27661aeee4..194a0871a9ff056135c59a2f4256cbbeef35f556 100644 --- a/src/mol-geo/primitive/sheet.ts +++ b/src/mol-geo/primitive/sheet.ts @@ -7,6 +7,7 @@ import { Vec3 } from 'mol-math/linear-algebra'; import { ChunkedArray } from 'mol-data/util'; +import { MeshBuilderState } from '../shape/mesh-builder'; const tA = Vec3.zero() const tB = Vec3.zero() @@ -19,29 +20,39 @@ const positionVector = Vec3.zero() const normalVector = Vec3.zero() const torsionVector = Vec3.zero() -export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowWidth: number, vertices: ChunkedArray<number, 3>, normals: ChunkedArray<number, 3>, indices: ChunkedArray<number, 3>, ids: ChunkedArray<number, 1>, currentId: number) { +const arrowVerticalVector = Vec3.zero() +const p1 = Vec3.zero() +const p2 = Vec3.zero() +const p3 = Vec3.zero() +const p4 = Vec3.zero() +const p5 = Vec3.zero() +const p6 = Vec3.zero() +const p7 = Vec3.zero() +const p8 = Vec3.zero() - const vertexCount = vertices.elementCount +export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean, state: MeshBuilderState) { + const { vertices, normals, indices } = state + + let vertexCount = vertices.elementCount let offsetLength = 0 - if (arrowWidth > 0) { + if (arrowHeight > 0) { Vec3.fromArray(tA, controlPoints, 0) Vec3.fromArray(tB, controlPoints, linearSegments * 3) - offsetLength = arrowWidth / Vec3.magnitude(Vec3.sub(tV, tB, tA)) + offsetLength = arrowHeight / Vec3.magnitude(Vec3.sub(tV, tB, tA)) } for (let i = 0; i <= linearSegments; ++i) { - const actualWidth = arrowWidth === 0 ? width : arrowWidth * (1 - i / linearSegments); - + const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments); const i3 = i * 3 Vec3.fromArray(verticalVector, normalVectors, i3) - Vec3.scale(verticalVector, verticalVector, actualWidth); + Vec3.scale(verticalVector, verticalVector, actualHeight); Vec3.fromArray(horizontalVector, binormalVectors, i3) - Vec3.scale(horizontalVector, horizontalVector, height); + Vec3.scale(horizontalVector, horizontalVector, width); - if (arrowWidth > 0) { + if (arrowHeight > 0) { Vec3.fromArray(tA, normalVectors, i3) Vec3.fromArray(tB, binormalVectors, i3) Vec3.scale(normalOffset, Vec3.cross(normalOffset, tA, tB), offsetLength) @@ -51,49 +62,41 @@ export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Help Vec3.fromArray(normalVector, normalVectors, i3) Vec3.fromArray(torsionVector, binormalVectors, i3) - Vec3.add(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) + Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) Vec3.copy(tB, normalVector) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); - Vec3.add(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) + Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); - Vec3.add(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) - Vec3.add(tB, Vec3.scale(tB, Vec3.copy(tB, torsionVector), -1), normalOffset) + // Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA + Vec3.add(tB, Vec3.negate(tB, torsionVector), normalOffset) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); - Vec3.sub(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) + Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); - Vec3.sub(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) - Vec3.scale(tB, Vec3.copy(tB, normalVector), -1) + // Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA + Vec3.negate(tB, normalVector) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); - Vec3.sub(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) + Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); - Vec3.sub(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) - Vec3.add(tB, Vec3.copy(tB, torsionVector), normalOffset) + // Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) // reuse tA + Vec3.add(tB, torsionVector, normalOffset) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); - Vec3.add(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector) + Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]) ChunkedArray.add3(normals, tB[0], tB[1], tB[2]) - ChunkedArray.add(ids, currentId); } for (let i = 0; i < linearSegments; ++i) { @@ -112,4 +115,94 @@ export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Help ); } } + + if (startCap) { + const offset = 0 + vertexCount = vertices.elementCount + + Vec3.fromArray(verticalVector, normalVectors, offset) + Vec3.scale(verticalVector, verticalVector, height); + + Vec3.fromArray(horizontalVector, binormalVectors, offset) + Vec3.scale(horizontalVector, horizontalVector, width); + + Vec3.fromArray(positionVector, controlPoints, offset) + + Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector) + Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector) + Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector) + Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector) + + ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]) + ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]) + ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]) + ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]) + + Vec3.cross(normalVector, horizontalVector, verticalVector) + + if (arrowHeight === 0) { + for (let i = 0; i < 4; ++i) { + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]) + } + + ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount); + ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2); + } else { + Vec3.fromArray(arrowVerticalVector, normalVectors, offset) + Vec3.scale(arrowVerticalVector, verticalVector, arrowHeight); + + Vec3.add(p5, Vec3.add(p5, positionVector, horizontalVector), arrowVerticalVector) + Vec3.sub(p6, Vec3.add(p6, positionVector, horizontalVector), arrowVerticalVector) + Vec3.sub(p7, Vec3.sub(p7, positionVector, horizontalVector), arrowVerticalVector) + Vec3.add(p8, Vec3.sub(p8, positionVector, horizontalVector), arrowVerticalVector) + + ChunkedArray.add3(vertices, p5[0], p5[1], p5[2]) + ChunkedArray.add3(vertices, p6[0], p6[1], p6[2]) + ChunkedArray.add3(vertices, p7[0], p7[1], p7[2]) + ChunkedArray.add3(vertices, p8[0], p8[1], p8[2]) + + for (let i = 0; i < 8; ++i) { + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]) + } + + ChunkedArray.add3(indices, vertexCount + 7, vertexCount, vertexCount + 4); + ChunkedArray.add3(indices, vertexCount + 7, vertexCount + 3, vertexCount); + ChunkedArray.add3(indices, vertexCount + 5, vertexCount + 1, vertexCount + 6); + ChunkedArray.add3(indices, vertexCount + 1, vertexCount + 2, vertexCount + 6); + } + } + + if (endCap && arrowHeight === 0) { + const offset = linearSegments * 3 + vertexCount = vertices.elementCount + + Vec3.fromArray(verticalVector, normalVectors, offset) + Vec3.scale(verticalVector, verticalVector, height); + + Vec3.fromArray(horizontalVector, binormalVectors, offset) + Vec3.scale(horizontalVector, horizontalVector, width); + + Vec3.fromArray(positionVector, controlPoints, offset) + + Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector) + Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector) + Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector) + Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector) + + ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]) + ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]) + ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]) + ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]) + + Vec3.cross(normalVector, horizontalVector, verticalVector) + + for (let i = 0; i < 4; ++i) { + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]) + } + + ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount); + ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2); + } + + return (linearSegments + 1) * 8 + (startCap ? (arrowHeight === 0 ? 4 : 8) : 0) + (endCap && arrowHeight === 0 ? 4 : 0) } \ No newline at end of file diff --git a/src/mol-geo/primitive/tube.ts b/src/mol-geo/primitive/tube.ts index 21ab1f12e92388a008debb002c9393b0416457ff..ade73172b2ed986f3d836ff4d13ffe7a2b8aa2f7 100644 --- a/src/mol-geo/primitive/tube.ts +++ b/src/mol-geo/primitive/tube.ts @@ -7,17 +7,21 @@ import { Vec3 } from 'mol-math/linear-algebra'; import { ChunkedArray } from 'mol-data/util'; +import { MeshBuilderState } from '../shape/mesh-builder'; const normalVector = Vec3.zero() const binormalVector = Vec3.zero() +const controlPoint = Vec3.zero() const tempPos = Vec3.zero() const a = Vec3.zero() const b = Vec3.zero() const u = Vec3.zero() const v = Vec3.zero() -export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, vertices: ChunkedArray<number, 3>, normals: ChunkedArray<number, 3>, indices: ChunkedArray<number, 3>, ids: ChunkedArray<number, 1>, currentId: number) { - const vertexCount = vertices.elementCount +export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean, state: MeshBuilderState) { + const { vertices, normals, indices } = state + + let vertexCount = vertices.elementCount const di = 1 / linearSegments for (let i = 0; i <= linearSegments; ++i) { @@ -30,7 +34,7 @@ export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpe const w = ff * width, h = ff * height; for (let j = 0; j < radialSegments; ++j) { - let t = 2 * Math.PI * j / radialSegments; + const t = 2 * Math.PI * j / radialSegments; Vec3.copy(a, u) Vec3.copy(b, v) @@ -54,7 +58,6 @@ export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpe ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]); ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]); - ChunkedArray.add(ids, currentId); } } @@ -62,16 +65,90 @@ export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpe for (let j = 0; j < radialSegments; ++j) { ChunkedArray.add3( indices, - (vertexCount + i * radialSegments + (j + 1) % radialSegments), - (vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments), - (vertexCount + i * radialSegments + j) + vertexCount + i * radialSegments + (j + 1) % radialSegments, + vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, + vertexCount + i * radialSegments + j ); ChunkedArray.add3( indices, - (vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments), - (vertexCount + (i + 1) * radialSegments + j), - (vertexCount + i * radialSegments + j) + vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, + vertexCount + (i + 1) * radialSegments + j, + vertexCount + i * radialSegments + j ); } } + + if (startCap) { + const offset = 0 + vertexCount = vertices.elementCount + Vec3.fromArray(u, normalVectors, offset) + Vec3.fromArray(v, binormalVectors, offset) + Vec3.fromArray(controlPoint, controlPoints, offset) + Vec3.cross(normalVector, u, v) + + ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]); + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]); + + for (let i = 0; i < radialSegments; ++i) { + const t = 2 * Math.PI * i / radialSegments; + + Vec3.copy(a, u) + Vec3.copy(b, v) + Vec3.add( + tempPos, + Vec3.scale(a, a, height * Math.cos(t)), + Vec3.scale(b, b, width * Math.sin(t)) + ) + Vec3.add(tempPos, controlPoint, tempPos) + + ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]); + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]); + + ChunkedArray.add3( + indices, + vertexCount, + vertexCount + i + 1, + vertexCount + (i + 1) % radialSegments + 1 + ); + } + } + + if (endCap) { + const offset = linearSegments * 3 + vertexCount = vertices.elementCount + Vec3.fromArray(u, normalVectors, offset) + Vec3.fromArray(v, binormalVectors, offset) + Vec3.fromArray(controlPoint, controlPoints, offset) + Vec3.cross(normalVector, u, v) + + ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]); + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]); + + for (let i = 0; i < radialSegments; ++i) { + const t = 2 * Math.PI * i / radialSegments; + + Vec3.copy(a, u) + Vec3.copy(b, v) + Vec3.add( + tempPos, + Vec3.scale(a, a, height * Math.cos(t)), + Vec3.scale(b, b, width * Math.sin(t)) + ) + Vec3.add(tempPos, controlPoint, tempPos) + + ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]); + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]); + + if (i < radialSegments - 2) { + ChunkedArray.add3( + indices, + vertexCount + i + 1, + vertexCount + (i + 1) % radialSegments + 1, + vertexCount + ); + } + } + } + + return (linearSegments + 1) * radialSegments + (startCap ? radialSegments + 1 : 0) + (endCap ? radialSegments + 1 : 0) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/cartoon.ts b/src/mol-geo/representation/structure/cartoon.ts index 445b6f92c25728ccd5f6cd9a2ea705557e45e3d6..5f11c26f8c907b1f81bd5268682f70a832ce0ddc 100644 --- a/src/mol-geo/representation/structure/cartoon.ts +++ b/src/mol-geo/representation/structure/cartoon.ts @@ -8,45 +8,54 @@ import { StructureRepresentation, StructureUnitsRepresentation } from '.'; import { PickingId } from '../../util/picking'; import { Structure } from 'mol-model/structure'; import { Task } from 'mol-task'; -import { Loci } from 'mol-model/loci'; +import { Loci, isEmptyLoci } from 'mol-model/loci'; import { MarkerAction } from '../../util/marker-data'; import { PolymerTraceVisual, DefaultPolymerTraceProps } from './visual/polymer-trace-mesh'; +import { PolymerGapVisual, DefaultPolymerGapProps } from './visual/polymer-gap-cylinder'; export const DefaultCartoonProps = { - ...DefaultPolymerTraceProps + ...DefaultPolymerTraceProps, + ...DefaultPolymerGapProps } export type CartoonProps = Partial<typeof DefaultCartoonProps> export function CartoonRepresentation(): StructureRepresentation<CartoonProps> { const traceRepr = StructureUnitsRepresentation(PolymerTraceVisual) + const gapRepr = StructureUnitsRepresentation(PolymerGapVisual) return { get renderObjects() { - return [ ...traceRepr.renderObjects ] + return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects ] }, get props() { - return { ...traceRepr.props } + return { ...traceRepr.props, ...gapRepr.props } }, create: (structure: Structure, props: CartoonProps = {} as CartoonProps) => { const p = Object.assign({}, DefaultCartoonProps, props) return Task.create('CartoonRepresentation', async ctx => { await traceRepr.create(structure, p).runInContext(ctx) + await gapRepr.create(structure, p).runInContext(ctx) }) }, update: (props: CartoonProps) => { const p = Object.assign({}, props) return Task.create('Updating CartoonRepresentation', async ctx => { await traceRepr.update(p).runInContext(ctx) + await gapRepr.update(p).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { - return traceRepr.getLoci(pickingId) + const traceLoci = traceRepr.getLoci(pickingId) + const gapLoci = gapRepr.getLoci(pickingId) + return isEmptyLoci(traceLoci) ? gapLoci : traceLoci }, mark: (loci: Loci, action: MarkerAction) => { traceRepr.mark(loci, action) + gapRepr.mark(loci, action) }, destroy() { traceRepr.destroy() + gapRepr.destroy() } } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e095c7f13d4bbc89d7fa94468625fd2ccc74830 --- /dev/null +++ b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ValueCell } from 'mol-util/value-cell' + +import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object' +import { Unit } from 'mol-model/structure'; +import { DefaultStructureProps, UnitsVisual } from '..'; +import { RuntimeContext } from 'mol-task' +import { createTransforms, createColors } from './util/common'; +import { deepEqual } from 'mol-util'; +import { MeshValues } from 'mol-gl/renderable'; +import { getMeshData } from '../../../util/mesh-data'; +import { Mesh } from '../../../shape/mesh'; +import { PickingId } from '../../../util/picking'; +import { createMarkers, MarkerAction } from '../../../util/marker-data'; +import { Loci } from 'mol-model/loci'; +import { SizeTheme } from '../../../theme'; +import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util'; +import { MeshBuilder } from '../../../shape/mesh-builder'; +import { getPolymerGapCount, PolymerGapIterator } from './util/polymer'; +import { getElementLoci, markElement } from './util/element'; +import { Vec3 } from 'mol-math/linear-algebra'; + +async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) { + const polymerGapCount = getPolymerGapCount(unit) + if (!polymerGapCount) return Mesh.createEmpty(mesh) + console.log('polymerGapCount', polymerGapCount) + + // TODO better vertex count estimates + const builder = MeshBuilder.create(polymerGapCount * 30, polymerGapCount * 30 / 2, mesh) + + const { elements } = unit + const pos = unit.conformation.invariantPosition + const pA = Vec3.zero() + const pB = Vec3.zero() + + let i = 0 + const polymerGapIt = PolymerGapIterator(unit) + while (polymerGapIt.hasNext) { + // TODO size theme + const { centerA, centerB } = polymerGapIt.move() + if (centerA.element === centerB.element) { + builder.setId(centerA.element) + pos(elements[centerA.element], pA) + builder.addIcosahedron(pA, 0.6, 0) + } else { + pos(elements[centerA.element], pA) + pos(elements[centerB.element], pB) + builder.setId(centerA.element) + builder.addFixedCountDashedCylinder(pA, pB, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 }) + builder.setId(centerB.element) + builder.addFixedCountDashedCylinder(pB, pA, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 }) + } + + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Gap mesh', current: i, max: polymerGapCount }); + } + ++i + } + + return builder.getMesh() +} + +export const DefaultPolymerGapProps = { + ...DefaultMeshProps, + ...DefaultStructureProps, + sizeTheme: { name: 'physical', factor: 1 } as SizeTheme, + detail: 0, + unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[] +} +export type PolymerGapProps = Partial<typeof DefaultPolymerGapProps> + +export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> { + let renderObject: MeshRenderObject + let currentProps: typeof DefaultPolymerGapProps + let mesh: Mesh + let currentGroup: Unit.SymmetryGroup + + return { + get renderObject () { return renderObject }, + async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerGapProps = {}) { + currentProps = Object.assign({}, DefaultPolymerGapProps, props) + currentGroup = group + + const { colorTheme, unitKinds } = { ...DefaultPolymerGapProps, ...props } + const instanceCount = group.units.length + const elementCount = group.elements.length + const unit = group.units[0] + + mesh = unitKinds.includes(unit.kind) + ? await createPolymerGapCylinderMesh(ctx, unit, mesh) + : Mesh.createEmpty(mesh) + // console.log(mesh) + + const transforms = createTransforms(group) + const color = createColors(group, elementCount, colorTheme) + const marker = createMarkers(instanceCount * elementCount) + + const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount } + + const values: MeshValues = { + ...getMeshData(mesh), + ...color, + ...marker, + aTransform: transforms, + elements: mesh.indexBuffer, + ...createMeshValues(currentProps, counts), + aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3)) + } + const state = createRenderableState(currentProps) + + renderObject = createMeshRenderObject(values, state) + }, + async update(ctx: RuntimeContext, props: PolymerGapProps) { + const newProps = Object.assign({}, currentProps, props) + + if (!renderObject) return false + + let updateColor = false + + if (newProps.detail !== currentProps.detail) { + const unit = currentGroup.units[0] + mesh = await createPolymerGapCylinderMesh(ctx, unit, mesh) + ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3) + updateColor = true + } + + if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) { + updateColor = true + } + + if (updateColor) { + const elementCount = currentGroup.elements.length + if (ctx.shouldUpdate) await ctx.update('Computing trace colors'); + createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values) + } + + updateMeshValues(renderObject.values, newProps) + updateRenderableState(renderObject.state, newProps) + + currentProps = newProps + return true + }, + getLoci(pickingId: PickingId) { + return getElementLoci(renderObject.id, currentGroup, pickingId) + }, + mark(loci: Loci, action: MarkerAction) { + markElement(renderObject.values.tMarker, currentGroup, loci, action) + }, + destroy() { + // TODO + } + } +} diff --git a/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts b/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts index 817b07e5d4d9974cc91f0808d85069c24fdc1e43..011100c0fad27bcb587f3215bfc8dc0df82d024a 100644 --- a/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts +++ b/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts @@ -191,14 +191,14 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, mesh?: Me // TODO size theme if (SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)) { - width = 1.0; height = 0.15 - const arrowWidth = v.secStrucChange ? 1.7 : 0 - builder.addSheet(controlPoints, normalVectors, binormalVectors, linearSegments, width, height, arrowWidth) + width = 0.15; height = 1.0 + const arrowHeight = v.secStrucChange ? 1.7 : 0 + builder.addSheet(controlPoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, true, true) } else { if (SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Helix)) { width = 0.2; height = 1.0 } - builder.addTube(controlPoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1) + builder.addTube(controlPoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, true, true) } if (i % 10000 === 0 && ctx.shouldUpdate) { diff --git a/src/mol-geo/representation/structure/visual/util/polymer.ts b/src/mol-geo/representation/structure/visual/util/polymer.ts index 0739cd7c95cea1ddc8ccd68ca7a592b3edf1ab8a..149443278810e73418d8f04449c7a24a7d4c28f9 100644 --- a/src/mol-geo/representation/structure/visual/util/polymer.ts +++ b/src/mol-geo/representation/structure/visual/util/polymer.ts @@ -20,6 +20,14 @@ export function getPolymerRanges(unit: Unit): SortedRanges<ElementIndex> { } } +export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> { + switch (unit.kind) { + case Unit.Kind.Atomic: return unit.model.atomicHierarchy.gapRanges + case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.gapRanges + case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.gapRanges + } +} + export function getPolymerElementCount(unit: Unit) { let count = 0 const { elements } = unit @@ -48,6 +56,17 @@ export function getPolymerElementCount(unit: Unit) { return count } +export function getPolymerGapCount(unit: Unit) { + let count = 0 + const { elements } = unit + const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements) + while (gapIt.hasNext) { + const { start, end } = gapIt.move() + if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count + } + return count +} + function getResidueTypeAtomId(moleculeType: MoleculeType, atomType: 'trace' | 'direction') { switch (moleculeType) { case MoleculeType.protein: @@ -134,9 +153,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa private getElementIndex(residueIndex: ResidueIndex, atomType: 'trace' | 'direction') { const index = getElementIndexForResidueTypeAtomId(this.unit.model, residueIndex, atomType) - // // TODO handle case when it returns -1 - // return SortedArray.indexOf(this.unit.elements, index) as ElementIndex - + // TODO handle case when it returns -1 const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex if (elementIndex === -1) { console.log('-1', residueIndex, atomType, index) @@ -222,6 +239,79 @@ export class CoarsePolymerBackboneIterator implements Iterator<PolymerBackbonePa } } +/** Iterates over gaps, i.e. the stem residues/coarse elements adjacent to gaps */ +export function PolymerGapIterator(unit: Unit): Iterator<PolymerGapPair> { + switch (unit.kind) { + case Unit.Kind.Atomic: return new AtomicPolymerGapIterator(unit) + case Unit.Kind.Spheres: + case Unit.Kind.Gaussians: + return new CoarsePolymerGapIterator(unit) + } +} + +interface PolymerGapPair { + centerA: StructureElement + centerB: StructureElement +} + +function createPolymerGapPair (unit: Unit) { + return { + centerA: StructureElement.create(unit), + centerB: StructureElement.create(unit), + } +} + +export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> { + private value: PolymerGapPair + private gapIt: SortedRanges.Iterator<ElementIndex, ResidueIndex> + hasNext: boolean = false; + + private getElementIndex(residueIndex: ResidueIndex, atomType: 'trace' | 'direction') { + const index = getElementIndexForResidueTypeAtomId(this.unit.model, residueIndex, atomType) + // TODO handle case when it returns -1 + const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex + if (elementIndex === -1) { + console.log('-1', residueIndex, atomType, index) + } + return elementIndex === -1 ? 0 as ElementIndex : elementIndex + } + + move() { + const { elements, residueIndex } = this.unit + const gapSegment = this.gapIt.move(); + this.value.centerA.element = this.getElementIndex(residueIndex[elements[gapSegment.start]], 'trace') + this.value.centerB.element = this.getElementIndex(residueIndex[elements[gapSegment.end - 1]], 'trace') + this.hasNext = this.gapIt.hasNext + return this.value; + } + + constructor(private unit: Unit.Atomic) { + this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements); + this.value = createPolymerGapPair(unit) + this.hasNext = this.gapIt.hasNext + } +} + +export class CoarsePolymerGapIterator implements Iterator<PolymerGapPair> { + private value: PolymerGapPair + private gapIt: SortedRanges.Iterator<ElementIndex, ElementIndex> + hasNext: boolean = false; + + move() { + const gapSegment = this.gapIt.move(); + this.value.centerA.element = this.unit.elements[gapSegment.start] + this.value.centerB.element = this.unit.elements[gapSegment.end - 1] + this.hasNext = this.gapIt.hasNext + return this.value; + } + + constructor(private unit: Unit.Spheres | Unit.Gaussians) { + this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements); + this.value = createPolymerGapPair(unit) + this.hasNext = this.gapIt.hasNext + } +} + /** * Iterates over individual residues/coarse elements in polymers of a unit while * providing information about the neighbourhood in the underlying model for drawing splines diff --git a/src/mol-geo/shape/mesh-builder.ts b/src/mol-geo/shape/mesh-builder.ts index b900784dc836c063f799a34e30fc87f0813fc990..306670182c020c003780b71cc021b0b3ed7d6bf3 100644 --- a/src/mol-geo/shape/mesh-builder.ts +++ b/src/mol-geo/shape/mesh-builder.ts @@ -16,12 +16,18 @@ import { getNormalMatrix } from '../util'; import { addSheet } from '../primitive/sheet'; import { addTube } from '../primitive/tube'; -type Primitive = { +interface Primitive { vertices: Float32Array normals: Float32Array indices: Uint32Array } +export interface MeshBuilderState { + vertices: ChunkedArray<number, 3> + normals: ChunkedArray<number, 3> + indices: ChunkedArray<number, 3> +} + export interface MeshBuilder { add(t: Mat4, _vertices: Float32Array, _normals: Float32Array, _indices?: Uint32Array): void addBox(t: Mat4, props?: BoxProps): void @@ -29,8 +35,8 @@ export interface MeshBuilder { addDoubleCylinder(start: Vec3, end: Vec3, lengthScale: number, shift: Vec3, props: CylinderProps): void addFixedCountDashedCylinder(start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: CylinderProps): void addIcosahedron(center: Vec3, radius: number, detail: number): void - addTube(controlPoints: Helpers.NumberArray, torsionVectors: Helpers.NumberArray, normalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number): void - addSheet(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowWidth: number): void + addTube(centers: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean): void + addSheet(centers: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean): void setId(id: number): void getMesh(): Mesh } @@ -56,10 +62,7 @@ function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number) { // direction so the triangles of adjacent cylinder will line up if (Vec3.dot(tmpCylinderMatDir, up) < 0) Vec3.scale(tmpCylinderMatDir, tmpCylinderMatDir, -1) Vec3.makeRotation(m, up, tmpCylinderMatDir) - // Mat4.fromTranslation(tmpCylinderMatTrans, tmpCylinderCenter) - // Mat4.mul(m, tmpCylinderMatTrans, m) - Mat4.setTranslation(m, tmpCylinderCenter) - return m + return Mat4.setTranslation(m, tmpCylinderCenter) } function getCylinder(props: CylinderProps) { @@ -75,8 +78,7 @@ function getCylinder(props: CylinderProps) { const tmpIcosahedronMat = Mat4.identity() function setIcosahedronMat(m: Mat4, center: Vec3) { - Mat4.setTranslation(m, center) - return m + return Mat4.setTranslation(m, center) } function getIcosahedron(props: IcosahedronProps) { @@ -96,6 +98,7 @@ export namespace MeshBuilder { const vertices = ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.vertexBuffer.ref.value : initialCount); const normals = ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.normalBuffer.ref.value : initialCount); const indices = ChunkedArray.create(Uint32Array, 3, chunkSize * 3, mesh ? mesh.indexBuffer.ref.value : initialCount * 3); + const state: MeshBuilderState = { vertices, normals, indices }; const ids = ChunkedArray.create(Float32Array, 1, chunkSize, mesh ? mesh.idBuffer.ref.value : initialCount); const offsets = ChunkedArray.create(Uint32Array, 1, chunkSize, mesh ? mesh.offsetBuffer.ref.value : initialCount); @@ -179,11 +182,13 @@ export namespace MeshBuilder { setIcosahedronMat(tmpIcosahedronMat, center) add(tmpIcosahedronMat, vertices, normals, indices) }, - addTube: (controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number) => { - addTube(controlPoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, waveFactor, vertices, normals, indices, ids, currentId) + addTube: (centers: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean) => { + const addedVertexCount = addTube(centers, normals, binormals, linearSegments, radialSegments, width, height, waveFactor, startCap, endCap, state) + for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(ids, currentId); }, - addSheet: (controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowWidth: number) => { - addSheet(controlPoints, normalVectors, binormalVectors, linearSegments, width, height, arrowWidth, vertices, normals, indices, ids, currentId) + addSheet: (controls: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean) => { + const addedVertexCount = addSheet(controls, normals, binormals, linearSegments, width, height, arrowHeight, startCap, endCap, state) + for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(ids, currentId); }, setId: (id: number) => { if (currentId !== id) { diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts index 1d87731d122260fc6f00dad06d6c1d38f775ed57..d95ab25e5eca3fb1751c6c89811e7d45a14e4df6 100644 --- a/src/mol-math/linear-algebra/3d/vec3.ts +++ b/src/mol-math/linear-algebra/3d/vec3.ts @@ -194,6 +194,16 @@ namespace Vec3 { return Vec3.scale(out, Vec3.normalize(out, a), l) } + /** + * Negates the components of a vec3 + */ + export function negate(out: Vec3, a: Vec3) { + out[0] = -a[0]; + out[1] = -a[1]; + out[2] = -a[2]; + return out; + } + /** * Returns the inverse of the components of a Vec3 */ @@ -384,7 +394,7 @@ namespace Vec3 { export function exactEquals(a: Vec3, b: Vec3) { return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; } - + /** * Returns whether or not the vectors have approximately the same elements in the same position. */ diff --git a/src/mol-model/structure/model/properties/utils/atomic-ranges.ts b/src/mol-model/structure/model/properties/utils/atomic-ranges.ts index 73d7053736c092b95bbbdc279870b8b86864d789..cf4134d084d468e06491c76fd856577ffe374e38 100644 --- a/src/mol-model/structure/model/properties/utils/atomic-ranges.ts +++ b/src/mol-model/structure/model/properties/utils/atomic-ranges.ts @@ -12,6 +12,8 @@ import { ChemicalComponent } from '../chemical-component'; import { MoleculeType, isPolymer } from '../../types'; import { ElementIndex } from '../../indexing'; +// TODO add gaps at the ends of the chains by comparing to the polymer sequence data + export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chemicalComponentMap: Map<string, ChemicalComponent>): AtomicRanges { const polymerRanges: number[] = [] const gapRanges: number[] = [] @@ -20,6 +22,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem const { label_seq_id, label_comp_id } = data.residues let prevSeqId: number + let prevStart: number let prevEnd: number let startIndex: number @@ -27,6 +30,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem const chainSegment = chainIt.move(); residueIt.setSegment(chainSegment); prevSeqId = -1 + prevStart = -1 prevEnd = -1 startIndex = -1 @@ -40,7 +44,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem if (startIndex !== -1) { if (seqId !== prevSeqId + 1) { polymerRanges.push(startIndex, prevEnd - 1) - gapRanges.push(prevEnd - 1, residueSegment.start) + gapRanges.push(prevStart, residueSegment.end - 1) startIndex = residueSegment.start } else if (!residueIt.hasNext) { polymerRanges.push(startIndex, residueSegment.end - 1) @@ -54,13 +58,15 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem startIndex = -1 } } - + + prevStart = residueSegment.start prevEnd = residueSegment.end prevSeqId = seqId } } - console.log(polymerRanges, gapRanges) + console.log('polymerRanges', polymerRanges) + console.log('gapRanges', gapRanges) return { polymerRanges: SortedRanges.ofSortedRanges(polymerRanges as ElementIndex[]), diff --git a/src/mol-model/structure/model/properties/utils/coarse-ranges.ts b/src/mol-model/structure/model/properties/utils/coarse-ranges.ts index 4e3cc0101cab5990dfd7a30bd027e6d00421a2c4..1621dffa10e5c8325c0f9c1adbc549405bfd7ecb 100644 --- a/src/mol-model/structure/model/properties/utils/coarse-ranges.ts +++ b/src/mol-model/structure/model/properties/utils/coarse-ranges.ts @@ -11,6 +11,7 @@ import { ChemicalComponent } from '../chemical-component'; import { ElementIndex } from '../../indexing'; // TODO assumes all coarse elements are part of a polymer +// TODO add gaps at the ends of the chains by comparing to the polymer sequence data export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: Map<string, ChemicalComponent>): CoarseRanges { const polymerRanges: number[] = [] @@ -33,6 +34,7 @@ export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: M } else { if (prevSeqEnd !== seq_id_begin.value(i) - 1) { polymerRanges.push(startIndex, i - 1) + gapRanges.push(i - 1, i) startIndex = i } } diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts index 394163562bb5c75cca4afa23cd06cbac117e1508..6c06fbb4f572a00029bd8c497c0ca8a761cad2d0 100644 --- a/src/mol-view/stage.ts +++ b/src/mol-view/stage.ts @@ -80,9 +80,9 @@ export class Stage { // this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water // this.loadPdbid('4v5a') // ribosome // this.loadPdbid('3j3q') // ... - // this.loadPdbid('3sn6') // discontinuous chains + this.loadPdbid('3sn6') // discontinuous chains // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`) - this.loadMmcifUrl(`../../examples/1cbs_updated.cif`) + // this.loadMmcifUrl(`../../examples/1cbs_updated.cif`) // this.loadMmcifUrl(`../../examples/1crn.cif`) // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`) // ok