diff --git a/src/mol-data/int/_spec/sorted-ranges.spec.ts b/src/mol-data/int/_spec/sorted-ranges.spec.ts index 307b3c08b497b3f655e490e11c4112e3d8049cce..84075305edfe65c901c316a2b287bbc94f0f7314 100644 --- a/src/mol-data/int/_spec/sorted-ranges.spec.ts +++ b/src/mol-data/int/_spec/sorted-ranges.spec.ts @@ -17,7 +17,7 @@ describe('rangesArray', () => { it(`iterator, ${name}`, () => { const rangesIt = SortedRanges.transientSegments(ranges, set) const { index, start, end } = expectedValues - + let i = 0 while (rangesIt.hasNext) { const segment = rangesIt.move() @@ -41,7 +41,7 @@ describe('rangesArray', () => { testIterator('two ranges', SortedRanges.ofSortedRanges([1, 2, 3, 4]), - OrderedSet.ofBounds(1, 4), + OrderedSet.ofBounds(1, 5), { index: [0, 1], start: [0, 2], end: [2, 4] } ) testIterator('first range', @@ -62,7 +62,7 @@ describe('rangesArray', () => { testIterator('set in second range and beyond', SortedRanges.ofSortedRanges([1, 2, 3, 4]), SortedArray.ofSortedArray([3, 10]), - { index: [1], start: [0], end: [2] } + { index: [1], start: [0], end: [1] } ) testIterator('length 1 range', SortedRanges.ofSortedRanges([1, 1, 3, 4]), diff --git a/src/mol-data/int/impl/sorted-array.ts b/src/mol-data/int/impl/sorted-array.ts index faa32048fca2e968542643436b8ca878dbfccc24..8c81e7940adde281f8950fdd78c71bbbb233cee0 100644 --- a/src/mol-data/int/impl/sorted-array.ts +++ b/src/mol-data/int/impl/sorted-array.ts @@ -65,6 +65,11 @@ export function areEqual(a: Nums, b: Nums) { return true; } +/** + * Returns 0 if `v` is smaller or equal the first element of `xs` + * Returns length of `xs` if `v` is bigger than the last element of `xs` + * Otherwise returns the first index where the value of `xs` is equal or bigger than `v` + */ export function findPredecessorIndex(xs: Nums, v: number) { const len = xs.length; if (v <= xs[0]) return 0; diff --git a/src/mol-data/int/ordered-set.ts b/src/mol-data/int/ordered-set.ts index 4ec45b17f185c1343a44e454c641879dbb58ae92..d8e7f705d5c0d7a16991f24c104970a6b1fbd80d 100644 --- a/src/mol-data/int/ordered-set.ts +++ b/src/mol-data/int/ordered-set.ts @@ -21,6 +21,7 @@ namespace OrderedSet { export const has: <T extends number = number>(set: OrderedSet<T>, x: T) => boolean = Base.has as any; /** Returns the index of `x` in `set` or -1 if not found. */ export const indexOf: <T extends number = number>(set: OrderedSet<T>, x: T) => number = Base.indexOf as any; + /** Returns the value in `set` at index `i`. */ export const getAt: <T extends number = number>(set: OrderedSet<T>, i: number) => T = Base.getAt as any; export const min: <T extends number = number>(set: OrderedSet<T>) => T = Base.min as any; @@ -40,6 +41,11 @@ namespace OrderedSet { /** Returns elements of `a` that are not in `b`, i.e `a` - `b` */ export const subtract: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => OrderedSet<T> = Base.subtract as any; + /** + * Returns 0 if `x` is smaller or equal the first element of `set` + * Returns length of `set` if `x` is bigger than the last element of `set` + * Otherwise returns the first index where the value of `set` is equal or bigger than `x` + */ export const findPredecessorIndex: <T extends number = number>(set: OrderedSet<T>, x: number) => number = Base.findPredecessorIndex as any; export const findPredecessorIndexInInterval: <T extends number = number>(set: OrderedSet<T>, x: T, range: Interval) => number = Base.findPredecessorIndexInInterval as any; export const findRange: <T extends number = number>(set: OrderedSet<T>, min: T, max: T) => Interval = Base.findRange as any; diff --git a/src/mol-data/int/segmentation.ts b/src/mol-data/int/segmentation.ts index 7062733eeaf6ae039febf1767eba5b886a9018cf..8d26c5f1f67e023fbd225e9fe2e9084e741e0d50 100644 --- a/src/mol-data/int/segmentation.ts +++ b/src/mol-data/int/segmentation.ts @@ -18,7 +18,7 @@ namespace Segmentation { export const getSegment: <T extends number = number, I extends number = number>(segs: Segmentation<T, I>, value: T) => number = Impl.getSegment as any; export const projectValue: <T extends number = number, I extends number = number>(segs: Segmentation<T, I>, set: OrderedSet<T>, value: T) => Interval = Impl.projectValue as any; - // Segment iterator that mutates a single segment object to mark all the segments. + /** Segment iterator that mutates a single segment object to mark all the segments. */ export const transientSegments: <T extends number = number, I extends number = number>(segs: Segmentation<T, I>, set: OrderedSet<T>, segment?: Segment) => Impl.SegmentIterator<I> = Impl.segments as any; export type SegmentIterator<I extends number = number> = Impl.SegmentIterator<I> diff --git a/src/mol-data/int/sorted-ranges.ts b/src/mol-data/int/sorted-ranges.ts index 967595e0f670ba6840665ca03ebce519fcdde4bb..754946a5b9221a83bb2f9b5e6dff6d0c49c37a14 100644 --- a/src/mol-data/int/sorted-ranges.ts +++ b/src/mol-data/int/sorted-ranges.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -23,6 +23,46 @@ namespace SortedRanges { } return size } + export function count<T extends number = number>(ranges: SortedRanges<T>) { return ranges.length / 2 } + + export function startAt<T extends number = number>(ranges: SortedRanges<T>, index: number) { + return ranges[index * 2] + } + export function endAt<T extends number = number>(ranges: SortedRanges<T>, index: number) { + return ranges[index * 2 + 1] + 1 + } + + export function minAt<T extends number = number>(ranges: SortedRanges<T>, index: number) { + return ranges[index * 2] + } + export function maxAt<T extends number = number>(ranges: SortedRanges<T>, index: number) { + return ranges[index * 2 + 1] + } + + /** Returns if a value of `set` is included in `ranges` */ + export function has<T extends number = number>(ranges: SortedRanges<T>, set: OrderedSet<T>) { + return firstIntersectionIndex(ranges, set) !== -1 + } + + /** Returns if a value of `set` is included in `ranges` from given index */ + export function hasFrom<T extends number = number>(ranges: SortedRanges<T>, set: OrderedSet<T>, from: number) { + return firstIntersectionIndexFrom(ranges, set, from) !== -1 + } + + export function firstIntersectionIndex<T extends number = number>(ranges: SortedRanges<T>, set: OrderedSet<T>): number { + return firstIntersectionIndexFrom(ranges, set, 0) + } + + export function firstIntersectionIndexFrom<T extends number = number>(ranges: SortedRanges<T>, set: OrderedSet<T>, from: number): number { + if (minAt(ranges, from) > OrderedSet.max(set) || max(ranges) < OrderedSet.min(set)) return -1 + + for (let i = from, il = count(ranges); i < il; ++i) { + const interval = Interval.ofRange(minAt(ranges, i), maxAt(ranges, i)) + if (OrderedSet.areIntersecting(interval, set)) return i + } + + return -1 + } export function transientSegments<T extends number = number, I extends number = number>(ranges: SortedRanges<T>, set: OrderedSet<T>) { return new Iterator<T, I>(ranges, set) @@ -32,53 +72,27 @@ namespace SortedRanges { private value: Segmentation.Segment<I> = { index: 0 as I, start: 0 as T, end: 0 as T } private curIndex = 0 - private maxIndex = 0 - private interval: Interval<T> - private curMin: T = 0 as T hasNext: boolean = false; - updateInterval() { - this.interval = Interval.ofRange(this.ranges[this.curIndex], this.ranges[this.curIndex + 1]) - } - - updateValue() { - this.value.index = this.curIndex / 2 as I - this.value.start = OrderedSet.findPredecessorIndex(this.set, this.ranges[this.curIndex]) - this.value.end = OrderedSet.findPredecessorIndex(this.set, this.ranges[this.curIndex + 1]) + 1 + private updateValue() { + this.value.index = this.curIndex as I + this.value.start = OrderedSet.findPredecessorIndex(this.set, startAt(this.ranges, this.curIndex)) + this.value.end = OrderedSet.findPredecessorIndex(this.set, endAt(this.ranges, this.curIndex)) } move() { if (this.hasNext) { this.updateValue() - while (this.curIndex <= this.maxIndex) { - this.curIndex += 2 - this.curMin = Interval.end(this.interval) - this.updateInterval() - if (Interval.min(this.interval) >= this.curMin && OrderedSet.areIntersecting(this.interval, this.set)) break - } - this.hasNext = this.curIndex <= this.maxIndex + this.curIndex = firstIntersectionIndexFrom(this.ranges, this.set, this.curIndex + 1) + this.hasNext = this.curIndex !== -1 } return this.value; } - getRangeIndex(value: number) { - const index = SortedArray.findPredecessorIndex(this.ranges, value) - return (index % 2 === 1) ? index - 1 : index - } - constructor(private ranges: SortedRanges<T>, private set: OrderedSet<T>) { - // 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) + 1) - 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 && min !== max && this.curIndex <= this.maxIndex + this.curIndex = firstIntersectionIndex(ranges, set) + this.hasNext = this.curIndex !== -1 } } } diff --git a/src/mol-geo/geometry/overpaint-data.ts b/src/mol-geo/geometry/overpaint-data.ts index b97e19a4abfcc6f8f09cb8c9df01cf58b8082781..08e753594ec85ced455e351a65efc360b316d7ac 100644 --- a/src/mol-geo/geometry/overpaint-data.ts +++ b/src/mol-geo/geometry/overpaint-data.ts @@ -25,6 +25,7 @@ export function applyOverpaintColor(array: Uint8Array, start: number, end: numbe export function clearOverpaint(array: Uint8Array, start: number, end: number) { array.fill(0, start * 4, end * 4) + return true } export function createOverpaint(count: number, overpaintData?: OverpaintData): OverpaintData { diff --git a/src/mol-model/structure/export/categories/modified-residues.ts b/src/mol-model/structure/export/categories/modified-residues.ts index 2e9183f88784900b765cde496a00a8255d7df7a3..39eb324f85c6834fde2c6f01c8b11907abeca371 100644 --- a/src/mol-model/structure/export/categories/modified-residues.ts +++ b/src/mol-model/structure/export/categories/modified-residues.ts @@ -28,7 +28,7 @@ const pdbx_struct_mod_residue_fields: CifField<number, StructureElement[]>[] = [ ]; function getModifiedResidues({ structures }: CifExportContext): StructureElement[] { - // TODO: can different models have differnt modified residues? + // TODO: can different models (in the same mmCIF file) have different modified residues? const structure = structures[0], model = structure.model; const map = model.properties.modifiedResidues.parentId; if (!map.size) return []; diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts index 1d48173bf56c23c33555852bf3ba4e43d33a2708..036fd11d0cab18c9d66591b80ae058ced91e8e1d 100644 --- a/src/mol-model/structure/model/types.ts +++ b/src/mol-model/structure/model/types.ts @@ -178,11 +178,11 @@ export const AminoAcidNames = new Set([ export const RnaBaseNames = new Set([ 'A', 'C', 'T', 'G', 'I', 'U' ]) export const DnaBaseNames = new Set([ 'DA', 'DC', 'DT', 'DG', 'DI', 'DU' ]) export const PeptideBaseNames = new Set([ 'APN', 'CPN', 'TPN', 'GPN' ]) -export const PurinBaseNames = new Set([ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ]) +export const PurineBaseNames = new Set([ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ]) export const PyrimidineBaseNames = new Set([ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ]) export const BaseNames = SetUtils.unionMany(RnaBaseNames, DnaBaseNames, PeptideBaseNames) -export const isPurinBase = (compId: string) => PurinBaseNames.has(compId.toUpperCase()) +export const isPurineBase = (compId: string) => PurineBaseNames.has(compId.toUpperCase()) export const isPyrimidineBase = (compId: string) => PyrimidineBaseNames.has(compId.toUpperCase()) /** get the molecule type from component type and id */ diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 2630573e46291f6f11c7ddc05807f52c2afdff48..4b85cdc0fed86bc65f7ba2433634ee14a254b682 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/src/mol-model/structure/structure/element.ts @@ -16,7 +16,7 @@ import Structure from './structure'; import Unit from './unit'; import { Boundary } from './util/boundary'; import { StructureProperties } from '../structure'; -import { sortArray } from '../../../mol-data/util'; +import { sortArray, hashFnv32a, hash2 } from '../../../mol-data/util'; import Expression from '../../../mol-script/language/expression'; interface StructureElement<U = Unit> { @@ -141,12 +141,32 @@ namespace StructureElement { export function remap(loci: Loci, structure: Structure): Loci { if (structure === loci.structure) return loci - return Loci(structure, loci.elements.map(e => ({ - unit: structure.unitMap.get(e.unit.id)!, - indices: e.indices - }))); + const elements: Loci['elements'][0][] = []; + loci.elements.forEach(e => { + const unit = structure.unitMap.get(e.unit.id) + if (!unit) return + + if (SortedArray.areEqual(e.unit.elements, unit.elements)) { + elements.push({ unit, indices: e.indices }) + } else { + // TODO optimize + const indices: UnitIndex[] = [] + OrderedSet.forEach(e.indices, (v) => { + const eI = e.unit.elements[v] + const uI = SortedArray.indexOf(unit.elements, eI) as UnitIndex | -1 + if (uI !== -1) indices.push(uI) + }) + elements.push({ + unit, + indices: SortedArray.ofSortedArray(indices) + }) + } + }); + + return Loci(structure, elements); } + /** Create union of `xs` and `ys` */ export function union(xs: Loci, ys: Loci): Loci { if (xs.elements.length > ys.elements.length) return union(ys, xs); if (xs.elements.length === 0) return ys; @@ -172,6 +192,7 @@ namespace StructureElement { return Loci(xs.structure, elements); } + /** Subtract `ys` from `xs` */ export function subtract(xs: Loci, ys: Loci): Loci { const map = new Map<number, OrderedSet<UnitIndex>>(); for (const e of ys.elements) map.set(e.unit.id, e.indices); @@ -309,24 +330,24 @@ namespace StructureElement { } export function toScriptExpression(loci: Loci) { - if (loci.structure.models.length > 1) { - console.warn('toScriptExpression is only supported for Structure with single model, returning empty expression.'); - return MS.struct.generator.empty(); - } if (loci.elements.length === 0) return MS.struct.generator.empty(); - const sourceIndexMap = new Map<string, UniqueArray<number, number>>(); + const models = loci.structure.models; + const sourceIndexMap = new Map<string, { modelLabel: string, modelIndex: number, xs: UniqueArray<number, number> }>(); const el = StructureElement.create(), p = StructureProperties.atom.sourceIndex; for (const e of loci.elements) { const { indices } = e; const { elements } = e.unit; - const opName = e.unit.conformation.operator.name; + + const key = models.length === 1 + ? e.unit.conformation.operator.name + : `${e.unit.conformation.operator.name} ${e.unit.model.label} ${e.unit.model.modelNum}`; let sourceIndices: UniqueArray<number, number>; - if (sourceIndexMap.has(opName)) sourceIndices = sourceIndexMap.get(opName)!; + if (sourceIndexMap.has(key)) sourceIndices = sourceIndexMap.get(key)!.xs; else { sourceIndices = UniqueArray.create<number, number>(); - sourceIndexMap.set(opName, sourceIndices); + sourceIndexMap.set(key, { modelLabel: e.unit.model.label, modelIndex: e.unit.model.modelNum, xs: sourceIndices }); } el.unit = e.unit; @@ -337,20 +358,81 @@ namespace StructureElement { } } - const byOpName: Expression[] = []; + const opData: OpData[] = []; const keys = sourceIndexMap.keys(); while (true) { const k = keys.next(); if (k.done) break; - byOpName.push(getOpNameQuery(k.value, sourceIndexMap.get(k.value)!.array)); + const e = sourceIndexMap.get(k.value)!; + opData.push(getOpData(k.value, e.xs.array, models.length > 1, e.modelLabel, e.modelIndex)); } + const opGroups = new Map<string, OpData>(); + for (let i = 0, il = opData.length; i < il; ++i) { + const d = opData[i] + const hash = hash2(hashFnv32a(d.atom.ranges), hashFnv32a(d.atom.set)) + const key = `${hash}|${d.entity ? (d.entity.modelLabel + d.entity.modelIndex) : ''}` + if (opGroups.has(key)) { + opGroups.get(key)!.chain.opName.push(...d.chain.opName) + } else { + opGroups.set(key, d) + } + } + + const opQueries: Expression[] = []; + opGroups.forEach(d => { + const { ranges, set } = d.atom + const { opName } = d.chain + + const opProp = MS.struct.atomProperty.core.operatorName() + const siProp = MS.struct.atomProperty.core.sourceIndex(); + const tests: Expression[] = []; + + // TODO: add set.ofRanges constructor to MolQL??? + if (set.length > 0) { + tests[tests.length] = MS.core.set.has([MS.set.apply(null, set), siProp]); + } + for (let rI = 0, _rI = ranges.length / 2; rI < _rI; rI++) { + tests[tests.length] = MS.core.rel.inRange([siProp, ranges[2 * rI], ranges[2 * rI + 1]]); + } + + if (d.entity) { + const { modelLabel, modelIndex } = d.entity + opQueries.push(MS.struct.generator.atomGroups({ + 'atom-test': tests.length > 1 ? MS.core.logic.or(tests) : tests[0], + 'chain-test': opName.length > 1 + ? MS.core.set.has([MS.set.apply(null, opName), opProp]) + : MS.core.rel.eq([opProp, opName[0]]), + 'entity-test': MS.core.logic.and([ + MS.core.rel.eq([MS.struct.atomProperty.core.modelLabel(), modelLabel]), + MS.core.rel.eq([MS.struct.atomProperty.core.modelIndex(), modelIndex]), + ]) + })) + } else { + opQueries.push(MS.struct.generator.atomGroups({ + 'atom-test': tests.length > 1 ? MS.core.logic.or(tests) : tests[0], + 'chain-test': opName.length > 1 + ? MS.core.set.has([MS.set.apply(null, opName), opProp]) + : MS.core.rel.eq([opProp, opName[0]]) + })) + } + }) + return MS.struct.modifier.union([ - byOpName.length === 1 ? byOpName[0] : MS.struct.combinator.merge.apply(null, byOpName) + opQueries.length === 1 + ? opQueries[0] + // Need to union before merge for fast performance + : MS.struct.combinator.merge(opQueries.map(q => MS.struct.modifier.union([ q ]))) ]); } - function getOpNameQuery(opName: string, xs: number[]) { + type OpData = { + atom: { set: number[], ranges: number[] }, + chain: { opName: string[] }, + entity?: { modelLabel: string, modelIndex: number } + } + + function getOpData(opName: string, xs: number[], multimodel: boolean, modelLabel: string, modelIndex: number): OpData { sortArray(xs); const ranges: number[] = []; @@ -373,21 +455,17 @@ namespace StructureElement { } } - const siProp = MS.struct.atomProperty.core.sourceIndex(); - const tests: Expression[] = []; - - // TODO: add set.ofRanges constructor to MolQL??? - if (set.length > 0) { - tests[tests.length] = MS.core.set.has([MS.set.apply(null, set), siProp]); - } - for (let rI = 0, _rI = ranges.length / 2; rI < _rI; rI++) { - tests[tests.length] = MS.core.rel.inRange([siProp, ranges[2 * rI], ranges[2 * rI + 1]]); - } + return multimodel + ? { + atom: { set, ranges }, + chain: { opName: [ opName ] }, + entity: { modelLabel, modelIndex } + } + : { + atom: { set, ranges }, + chain: { opName: [ opName ] }, + } - return MS.struct.generator.atomGroups({ - 'atom-test': tests.length > 1 ? MS.core.logic.or.apply(null, tests) : tests[0], - 'chain-test': MS.core.rel.eq([MS.struct.atomProperty.core.operatorName(), opName]) - }); } } } diff --git a/src/mol-model/structure/structure/properties.ts b/src/mol-model/structure/structure/properties.ts index 17aa1c75a40ec49304eb821a16f9159c912e2bcc..5c6a70a0d22a6e395eb26ed31c53c639cd9c77b6 100644 --- a/src/mol-model/structure/structure/properties.ts +++ b/src/mol-model/structure/structure/properties.ts @@ -113,6 +113,8 @@ const entity = { const unit = { id: StructureElement.property(l => l.unit.id), operator_name: StructureElement.property(l => l.unit.conformation.operator.name), + model_index: StructureElement.property(l => l.unit.model.modelNum), + model_label: StructureElement.property(l => l.unit.model.label), hkl: StructureElement.property(l => l.unit.conformation.operator.hkl), spgrOp: StructureElement.property(l => l.unit.conformation.operator.spgrOp), diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index e2080382764082168d00aca15fbf801eafdb121f..ef528356144093a8dbf9b2d333421c529330eb11 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/src/mol-model/structure/structure/structure.ts @@ -218,6 +218,10 @@ class Structure { return SortedArray.has(this.unitMap.get(e.unit.id).elements, e.element); } + getModelIndex(m: Model) { + return this.model + } + private initUnits(units: ArrayLike<Unit>) { const map = IntMap.Mutable<Unit>(); let elementCount = 0; diff --git a/src/mol-model/structure/structure/unit/links.ts b/src/mol-model/structure/structure/unit/links.ts index 3c115c376c6db742802dc8ec80d2ef5ab37ba2bc..8ea138d4d1d8fc8ef0d9e634513c5bb6a4064c13 100644 --- a/src/mol-model/structure/structure/unit/links.ts +++ b/src/mol-model/structure/structure/unit/links.ts @@ -62,6 +62,29 @@ namespace Link { return true } + export function remapLoci(loci: Loci, structure: Structure): Loci { + if (structure === loci.structure) return loci + + const links: Loci['links'][0][] = []; + loci.links.forEach(l => { + const unitA = structure.unitMap.get(l.aUnit.id) + if (!unitA) return + const unitB = structure.unitMap.get(l.bUnit.id) + if (!unitB) return + + const elementA = l.aUnit.elements[l.aIndex] + const indexA = SortedArray.indexOf(unitA.elements, elementA) as StructureElement.UnitIndex | -1 + if (indexA === -1) return + const elementB = l.bUnit.elements[l.bIndex] + const indexB = SortedArray.indexOf(unitB.elements, elementB) as StructureElement.UnitIndex | -1 + if (indexB === -1) return + + links.push(Location(unitA, indexA, unitB, indexB)) + }); + + return Loci(structure, links); + } + export function toStructureElementLoci(loci: Loci): StructureElement.Loci { const elements: StructureElement.Loci['elements'][0][] = [] const map = new Map<number, number[]>() diff --git a/src/mol-plugin/skin/base/icons.scss b/src/mol-plugin/skin/base/icons.scss index 2c4c0590c5916943e5db8569171199a3ecd82eee..7c642ee711538fcf36bd3a881feb6d163d119e6f 100644 --- a/src/mol-plugin/skin/base/icons.scss +++ b/src/mol-plugin/skin/base/icons.scss @@ -213,3 +213,7 @@ .msp-icon-floppy:before { content: "\e8d0"; } + +.msp-icon-tape:before { + content: "\e8c8"; +} diff --git a/src/mol-plugin/state/transforms/helpers.ts b/src/mol-plugin/state/transforms/helpers.ts index 509c96410d860437597a43007b448c761a8e971e..a68fe1cd0c5a7ba4fdbfeb577dd096c730a7181d 100644 --- a/src/mol-plugin/state/transforms/helpers.ts +++ b/src/mol-plugin/state/transforms/helpers.ts @@ -25,11 +25,11 @@ function scriptToLoci(structure: Structure, script: Script) { return StructureSelection.toLoci2(result) } -export function getStructureOverpaint(structure: Structure, scriptLayers: { script: Script, color: Color }[], alpha: number): Overpaint { +export function getStructureOverpaint(structure: Structure, scriptLayers: { script: Script, color: Color, clear: boolean }[], alpha: number): Overpaint { const layers: Overpaint.Layer[] = [] for (let i = 0, il = scriptLayers.length; i < il; ++i) { - const { script, color } = scriptLayers[i] - layers.push({ loci: scriptToLoci(structure, script), color }) + const { script, color, clear } = scriptLayers[i] + layers.push({ loci: scriptToLoci(structure, script), color, clear }) } return { layers, alpha } } diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index 90136b1fd25b6f7c3fa797c8b6ebe513ec9948bc..35ede9697349cd7eae3a13c5d2dcd7023dc8c5c0 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -300,9 +300,9 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({ canAutoUpdate() { return true; }, - apply({ a, params, spine }) { - const rootStructure = spine.getRootOfType(SO.Molecule.Structure)!.data; + apply({ a, params }) { const structure = a.data.source.data; + const rootStructure = structure.parent || structure; const unitTransforms = new StructureUnitTransforms(rootStructure); explodeStructure(structure, unitTransforms, params.t); return new SO.Molecule.Structure.Representation3DState({ @@ -312,8 +312,9 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({ source: a }, { label: `Explode T = ${params.t.toFixed(2)}` }); }, - update({ a, b, newParams, oldParams, spine }) { - const rootStructure = spine.getRootOfType(SO.Molecule.Structure)!.data; + update({ a, b, newParams, oldParams }) { + const structure = a.data.source.data; + const rootStructure = structure.parent || structure; if (b.data.info !== rootStructure) return StateTransformer.UpdateResult.Recreate; if (oldParams.t === newParams.t) return StateTransformer.UpdateResult.Unchanged; const unitTransforms = b.data.state.unitTransforms!; @@ -332,25 +333,15 @@ const OverpaintStructureRepresentation3D = PluginStateTransform.BuiltIn({ to: SO.Molecule.Structure.Representation3DState, params: { layers: PD.ObjectList({ - script: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :residue-test (= atom.resname LYS))' }), - color: PD.Color(ColorNames.blueviolet) - }, e => `${Color.toRgbString(e.color)}`, { - defaultValue: [ - { - script: { - language: 'mol-script', - expression: '(sel.atom.atom-groups :residue-test (= atom.resname LYS))' - }, - color: ColorNames.blueviolet - }, - { - script: { - language: 'mol-script', - expression: '(sel.atom.atom-groups :residue-test (= atom.resname ALA))' - }, - color: ColorNames.chartreuse - } - ] + script: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.all)' }), + color: PD.Color(ColorNames.blueviolet), + clear: PD.Boolean(false) + }, e => `${e.clear ? 'Clear' : Color.toRgbString(e.color)}`, { + defaultValue: [{ + script: { language: 'mol-script', expression: '(sel.atom.all)' }, + color: ColorNames.blueviolet, + clear: false + }] }), alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity' }), } @@ -390,7 +381,7 @@ const TransparencyStructureRepresentation3D = PluginStateTransform.BuiltIn({ from: SO.Molecule.Structure.Representation3D, to: SO.Molecule.Structure.Representation3DState, params: { - script: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :chain-test (= atom.label_asym_id A))' }), + script: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.all)' }), value: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }), variant: PD.Select('single', [['single', 'Single-layer'], ['multi', 'Multi-layer']]) } diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 2758c3d29871a14f3964791b9324948fd2933e79..31c64b94e133d76048cf17aed55e7694e8b4510e 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -1,7 +1,8 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import * as React from 'react'; @@ -9,12 +10,15 @@ import { PluginCommands } from '../../mol-plugin/command'; import { UpdateTrajectory } from '../../mol-plugin/state/actions/structure'; import { PluginUIComponent } from './base'; import { LociLabelEntry } from '../../mol-plugin/util/loci-label-manager'; -import { IconButton } from './controls/common'; +import { IconButton, Icon } from './controls/common'; import { PluginStateObject } from '../../mol-plugin/state/objects'; import { StateTransforms } from '../../mol-plugin/state/transforms'; import { StateTransformer } from '../../mol-state'; import { ModelFromTrajectory } from '../../mol-plugin/state/transforms/model'; import { AnimationControls } from './state/animation'; +import { StructureOverpaintControls } from './structure/overpaint'; +import { StructureRepresentationControls } from './structure/representation'; +import { StructureSelectionControls } from './structure/selection'; export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> { state = { show: false, label: '' } @@ -225,7 +229,7 @@ export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty: const isAnimating = this.state.isAnimating; return <div className='msp-animation-viewport-controls'> - <IconButton icon={isAnimating || isPlaying ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Select Animation'} + <IconButton icon={isAnimating || isPlaying ? 'stop' : 'tape'} title={isAnimating ? 'Stop' : 'Select Animation'} onClick={isAnimating || isPlaying ? this.stop : this.toggleExpanded} disabled={isAnimating|| isPlaying ? false : this.state.isUpdating || this.state.isPlaying || this.state.isEmpty} /> {(this.state.isExpanded && !this.state.isUpdating) && <div className='msp-animation-viewport-controls-select'> @@ -249,4 +253,16 @@ export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyA {this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)} </div>; } +} + +export class StructureToolsWrapper extends PluginUIComponent { + render() { + return <div> + <div className='msp-section-header'><Icon name='code' /> Structure Tools</div> + + <StructureSelectionControls /> + <StructureOverpaintControls /> + <StructureRepresentationControls /> + </div>; + } } \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 615a35103d95d108f0119246232854868d70d5d2..e432dc420209d350293cd1480a6b3de64f9756ac 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -12,7 +12,7 @@ import { LogEntry } from '../../mol-util/log-entry'; import * as React from 'react'; import { PluginContext } from '../context'; import { PluginReactContext, PluginUIComponent } from './base'; -import { LociLabelControl, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls } from './controls'; +import { LociLabelControl, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls, StructureToolsWrapper } from './controls'; import { StateSnapshots } from './state'; import { StateObjectActions } from './state/actions'; import { StateTree } from './state/tree'; @@ -109,6 +109,7 @@ export class ControlsWrapper extends PluginUIComponent { <CurrentObject /> {/* <AnimationControlsWrapper /> */} {/* <CameraSnapshots /> */} + <StructureToolsWrapper /> <StateSnapshots /> </div>; } diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx index 82cbdd76bee45b604aba4ba7e1903a80ff289975..e1a3b8cbd059cd7103d66046a87bfce969d1bf41 100644 --- a/src/mol-plugin/ui/sequence.tsx +++ b/src/mol-plugin/ui/sequence.tsx @@ -10,7 +10,7 @@ import { PluginUIComponent } from './base'; import { StateTreeSpine } from '../../mol-state/tree/spine'; import { PluginStateObject as SO } from '../state/objects'; import { Sequence } from './sequence/sequence'; -import { Structure, StructureElement, StructureProperties as SP } from '../../mol-model/structure'; +import { Structure, StructureElement, StructureProperties as SP, Unit } from '../../mol-model/structure'; import { SequenceWrapper } from './sequence/wrapper'; import { PolymerSequenceWrapper } from './sequence/polymer'; import { StructureElementSelectionManager } from '../util/structure-element-selection'; @@ -75,7 +75,12 @@ function getChainOptions(structure: Structure, entityId: string) { const id = unit.invariantId if (seen.has(id)) continue - let label = `${SP.chain.label_asym_id(l)}: ${SP.chain.auth_asym_id(l)}` + let label = '' + if (Unit.isAtomic(unit)) { + label = `${SP.chain.label_asym_id(l)}: ${SP.chain.auth_asym_id(l)}` + } else { + label = `${SP.coarse.asym_id(l)}` + } if (SP.entity.type(l) === 'water') { const count = water.get(label) || 1 water.set(label, count + 1) @@ -135,7 +140,7 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> { this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => { const current = this.spine.current; - if (!current || current.sourceRef !== ref || current.state !== state) return; + if (!current || current.sourceRef !== ref) return; this.setState(this.getInitialState()) }); } diff --git a/src/mol-plugin/ui/sequence/polymer.ts b/src/mol-plugin/ui/sequence/polymer.ts index 14e8bf64d921a55fe673ebb3e2188ea01587388d..d31ff0c4caef1326ed2016545b6af968f053e1d6 100644 --- a/src/mol-plugin/ui/sequence/polymer.ts +++ b/src/mol-plugin/ui/sequence/polymer.ts @@ -72,7 +72,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> { this.missing = data.unit.model.properties.missingResidues this.modelNum = data.unit.model.modelNum - this.asymId = SP.chain.label_asym_id(l) + this.asymId = Unit.isAtomic(data.unit) ? SP.chain.label_asym_id(l) : SP.coarse.asym_id(l) const missing: number[] = [] for (let i = 0; i < length; ++i) { diff --git a/src/mol-plugin/ui/structure/overpaint.tsx b/src/mol-plugin/ui/structure/overpaint.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c158f2801f564f0f155caadacce44b05ae4298c5 --- /dev/null +++ b/src/mol-plugin/ui/structure/overpaint.tsx @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react'; +import { PluginUIComponent } from '../base'; +import { PluginStateObject } from '../../../mol-plugin/state/objects'; +import { StateTransforms } from '../../../mol-plugin/state/transforms'; +import { StateSelection, StateObjectCell, StateTransform, StateBuilder } from '../../../mol-state'; +import { ParamDefinition as PD} from '../../../mol-util/param-definition'; +import { ColorNames } from '../../../mol-util/color/tables'; +import { ParameterControls } from '../controls/parameters'; +import { Structure } from '../../../mol-model/structure'; +import { isEmptyLoci } from '../../../mol-model/loci'; +import { PluginContext } from '../../context'; +import { getExpression } from './util'; + + +type OverpaintEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, rootStructure: Structure, overpaint?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.OverpaintStructureRepresentation3D>>) => void +const OverpaintManagerTag = 'overpaint-controls' + +export class StructureOverpaintControls extends PluginUIComponent<{}, { params: PD.Values<ReturnType<typeof StructureOverpaintControls.getParams>> }> { + state = { params: PD.getDefaultValues(StructureOverpaintControls.getParams(this.plugin)) } + + static getParams = (plugin: PluginContext) => { + const { types } = plugin.structureRepresentation.registry + return { + color: PD.Color(ColorNames.cyan), + type: PD.MultiSelect(types.map(t => t[0]), types) + } + } + + componentDidMount() { + + } + + private async eachRepr(callback: OverpaintEachReprCallback) { + const state = this.plugin.state.dataState; + const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3D)); + + const update = state.build(); + for (const r of reprs) { + const overpaint = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.OverpaintStructureRepresentation3D, r.transform.ref).withTag(OverpaintManagerTag)); + + const structure = r.obj!.data.source.data + const rootStructure = structure.parent || structure + + callback(update, r, rootStructure, overpaint[0]) + } + + await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })); + } + + set = async (clear: boolean) => { + await this.eachRepr((update, repr, rootStructure, overpaint) => { + if (!this.state.params.type.includes(repr.params!.values.type.name)) return + + const loci = this.plugin.helpers.structureSelection.get(rootStructure) + if (isEmptyLoci(loci) || loci.elements.length === 0) return + const expression = getExpression(loci) + + const layer = { + script: { language: 'mol-script', expression }, + color: this.state.params.color, + clear + } + + if (overpaint) { + update.to(overpaint).update({ layers: [ ...overpaint.params!.values.layers, layer ], alpha: 1 }) + } else { + update.to(repr.transform.ref) + .apply(StateTransforms.Representation.OverpaintStructureRepresentation3D, { layers: [ layer ], alpha: 1 }, { tags: OverpaintManagerTag }); + } + }) + } + + add = async () => { + this.set(false) + } + + clear = async () => { + this.set(true) + } + + clearAll = async () => { + await this.eachRepr((update, repr, rootStructure, overpaint) => { + if (overpaint) update.delete(overpaint.transform.ref) + }) + } + + render() { + return <div className='msp-transform-wrapper'> + <div className='msp-transform-header'> + <button className='msp-btn msp-btn-block'>Current Selection Overpaint</button> + </div> + <div> + <ParameterControls params={StructureOverpaintControls.getParams(this.plugin)} values={this.state.params} onChange={p => { + const params = { ...this.state.params, [p.name]: p.value }; + this.setState({ params }); + }}/> + + <div className='msp-btn-row-group'> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Add</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clearAll}>Clear All</button> + </div> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/structure/representation.tsx b/src/mol-plugin/ui/structure/representation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..54e06dfce717987bc7a47d76234ecdfae2dae352 --- /dev/null +++ b/src/mol-plugin/ui/structure/representation.tsx @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react'; +import { PluginUIComponent } from '../base'; +import { PluginStateObject } from '../../../mol-plugin/state/objects'; +import { StateTransforms } from '../../../mol-plugin/state/transforms'; +import { StateTransformer, StateSelection, StateObjectCell, StateTransform, StateBuilder } from '../../../mol-state'; +import { ParamDefinition as PD} from '../../../mol-util/param-definition'; +import { ParameterControls } from '../controls/parameters'; +import { StructureElement, QueryContext, StructureSelection } from '../../../mol-model/structure'; +import { isEmptyLoci } from '../../../mol-model/loci'; +import { PluginContext } from '../../context'; +import { getExpression } from './util'; +import { parseMolScript } from '../../../mol-script/language/parser'; +import { transpileMolScript } from '../../../mol-script/script/mol-script/symbols'; +import { compile } from '../../../mol-script/runtime/query/compiler'; +import { StructureRepresentation3DHelpers } from '../../state/transforms/representation'; + +type RepresentationEachStructureCallback = (update: StateBuilder.Root, structure: StateObjectCell<PluginStateObject.Molecule.Structure, StateTransform<StateTransformer<any, PluginStateObject.Molecule.Structure, any>>>) => void +const RepresentationManagerTag = 'representation-controls' + +function getRepresentationManagerTag(type: string) { + return `${RepresentationManagerTag}-${type}` +} + +function getCombinedLoci(mode: 'add' | 'remove' | 'only' | 'all', loci: StructureElement.Loci, currentLoci: StructureElement.Loci): StructureElement.Loci { + switch (mode) { + case 'add': return StructureElement.Loci.union(loci, currentLoci) + case 'remove': return StructureElement.Loci.subtract(currentLoci, loci) + case 'only': return loci + case 'all': return StructureElement.Loci.all(loci.structure) + } +} + +export class StructureRepresentationControls extends PluginUIComponent<{}, { params: PD.Values<ReturnType<typeof StructureRepresentationControls.getParams>> }> { + state = { params: PD.getDefaultValues(StructureRepresentationControls.getParams(this.plugin)) } + + static getParams = (plugin: PluginContext) => { + const { types } = plugin.structureRepresentation.registry + return { + type: PD.Select(types[0][0], types) + } + } + + componentDidMount() { + + } + + private async eachStructure(callback: RepresentationEachStructureCallback) { + const state = this.plugin.state.dataState; + const structures = state.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure)); + + const update = state.build(); + for (const s of structures) { + callback(update, s) + } + + await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })); + } + + set = async (mode: 'add' | 'remove' | 'only' | 'all') => { + const state = this.plugin.state.dataState + const { type } = this.state.params + + await this.eachStructure((update, structure) => { + const s = structure.obj!.data + const _loci = this.plugin.helpers.structureSelection.get(s) + const loci = isEmptyLoci(_loci) ? StructureElement.Loci(s, []) : _loci + + const selections = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure, structure.transform.ref).withTag(getRepresentationManagerTag(type))); + + if (selections.length > 0) { + const parsed = parseMolScript(selections[0].params!.values.query.expression) + if (parsed.length === 0) return + + const query = transpileMolScript(parsed[0]) + const compiled = compile(query) + const result = compiled(new QueryContext(structure.obj!.data)) + const currentLoci = StructureSelection.toLoci2(result) + + const combinedLoci = getCombinedLoci(mode, loci, currentLoci) + + update.to(selections[0]).update({ + ...selections[0].params!.values, + query: { language: 'mol-script', expression: getExpression(combinedLoci) } + }) + } else { + const combinedLoci = getCombinedLoci(mode, loci, StructureElement.Loci(loci.structure, [])) + + update.to(structure.transform.ref) + .apply( + StateTransforms.Model.UserStructureSelection, + { + query: { language: 'mol-script', expression: getExpression(combinedLoci) }, + label: type + }, + { tags: [ RepresentationManagerTag, getRepresentationManagerTag(type) ] } + ) + .apply( + StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParams(this.plugin, type as any, s) + ) + } + }) + } + + show = async () => { this.set('add') } + hide = async () => { this.set('remove') } + only = async () => { this.set('only') } + showAll = async () => { this.set('all') } + + hideAll = async () => { + const { type } = this.state.params + const state = this.plugin.state.dataState; + const update = state.build(); + + state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure).withTag(getRepresentationManagerTag(type))).forEach(structure => update.delete(structure.transform.ref)); + + await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true })); + } + + render() { + return <div className='msp-transform-wrapper'> + <div className='msp-transform-header'> + <button className='msp-btn msp-btn-block'>Current Selection Representation</button> + </div> + <div> + <ParameterControls params={StructureRepresentationControls.getParams(this.plugin)} values={this.state.params} onChange={p => { + const params = { ...this.state.params, [p.name]: p.value }; + this.setState({ params }); + }}/> + + <div className='msp-btn-row-group'> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.show}>Show</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.hide}>Hide</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.only}>Only</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.showAll}>Show All</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.hideAll}>Hide All</button> + </div> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/structure/selection.tsx b/src/mol-plugin/ui/structure/selection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ea60e71c2582596f255cbe2a99d7026f0cd7a5d9 --- /dev/null +++ b/src/mol-plugin/ui/structure/selection.tsx @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react'; +import { PluginUIComponent } from '../base'; +import { MolScriptBuilder as MS } from '../../../mol-script/language/builder'; +import { StateSelection } from '../../../mol-state'; +import { PluginStateObject } from '../../state/objects'; +import { QueryContext, StructureSelection, QueryFn, Queries as _Queries } from '../../../mol-model/structure'; +import { compile } from '../../../mol-script/runtime/query/compiler'; +import { ButtonsType } from '../../../mol-util/input/input-observer'; +import { EmptyLoci } from '../../../mol-model/loci'; + +const Queries = { + all: () => compile<StructureSelection>(MS.struct.generator.all()), + polymers: () => _Queries.internal.atomicSequence(), + water: () => _Queries.internal.water(), + ligands: () => _Queries.internal.atomicHet(), + coarse: () => _Queries.internal.spheres(), +} + +export class StructureSelectionControls extends PluginUIComponent<{}, {}> { + state = {} + + select = (query: QueryFn<StructureSelection>) => { + const state = this.plugin.state.dataState + const structures = state.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure)) + const { structureSelection } = this.plugin.helpers + + structureSelection.clear() + for (const so of structures) { + const s = so.obj!.data + const result = query(new QueryContext(s)) + const loci = StructureSelection.toLoci2(result) + + // TODO use better API when available + this.plugin.interactivity.lociSelections.apply({ + current: { loci }, + buttons: ButtonsType.Flag.Secondary, + modifiers: { shift: false, alt: false, control: true, meta: false } + }) + } + } + + clear = () => { + // TODO use better API when available + this.plugin.interactivity.lociSelections.apply({ + current: { loci: EmptyLoci }, + buttons: ButtonsType.Flag.Secondary, + modifiers: { shift: false, alt: false, control: true, meta: false } + }) + } + + render() { + return <div className='msp-transform-wrapper'> + <div className='msp-transform-header'> + <button className='msp-btn msp-btn-block'>Current Selection</button> + </div> + <div> + <div className='msp-btn-row-group'> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(Queries.all())}>All</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.clear()}>None</button> + </div> + <div className='msp-btn-row-group'> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(Queries.polymers())}>Polymers</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(Queries.ligands())}>Ligands</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(Queries.water())}>Water</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(Queries.coarse())}>Coarse</button> + </div> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/structure/util.ts b/src/mol-plugin/ui/structure/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca17b74fe8825f36142e789862c146b48650c2a9 --- /dev/null +++ b/src/mol-plugin/ui/structure/util.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { StructureElement } from '../../../mol-model/structure'; +import { EmptyLoci, isEmptyLoci } from '../../../mol-model/loci'; +import { MolScriptBuilder } from '../../../mol-script/language/builder'; +import { formatMolScript } from '../../../mol-script/language/expression-formatter'; + +export function getExpression(loci: StructureElement.Loci | EmptyLoci) { + const scriptExpression = isEmptyLoci(loci) + ? MolScriptBuilder.struct.generator.empty() + : StructureElement.Loci.toScriptExpression(loci) + return formatMolScript(scriptExpression) +} \ No newline at end of file diff --git a/src/mol-plugin/util/interactivity.ts b/src/mol-plugin/util/interactivity.ts index 87031ce0e1c0f76fe4f3189267579a5fd662ba1e..bce545a963a815be2635ba2e8336bafa57f66b4d 100644 --- a/src/mol-plugin/util/interactivity.ts +++ b/src/mol-plugin/util/interactivity.ts @@ -163,6 +163,7 @@ namespace Interactivity { } } + // TODO create better API that is independent of a `ClickEvent` apply(e: ClickEvent) { const { current, buttons, modifiers } = e const normalized: Loci<ModelLoci> = this.normalizedLoci(current) diff --git a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts index 7124442af98c2d3847ca631ec7982364b17e318b..711e98eb429477d22f20cc1ce97e8212823dfe26 100644 --- a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts +++ b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts @@ -120,6 +120,7 @@ function eachCarbohydrateLink(loci: Loci, structure: Structure, apply: (interval let changed = false if (Link.isLoci(loci)) { if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = Link.remapLoci(loci, structure) const { getLinkIndex } = structure.carbohydrates for (const l of loci.links) { const idx = getLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex]) @@ -129,6 +130,7 @@ function eachCarbohydrateLink(loci: Loci, structure: Structure, apply: (interval } } else if (StructureElement.isLoci(loci)) { if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) // TODO mark link only when both of the link elements are in a StructureElement.Loci const { getElementIndex, getLinkIndices, elements } = structure.carbohydrates for (const e of loci.elements) { diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts index 5c35ab7216125842e4cccc7a43963e91f5543651..b5adcfe5c757b0c2cfa0c53f962283722c7e1492 100644 --- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts +++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts @@ -197,6 +197,7 @@ function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: In let changed = false if (!StructureElement.isLoci(loci)) return false if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) for (const e of loci.elements) { // TODO make more efficient by handling/grouping `e.indices` by residue index // TODO only call apply when the full alt-residue of the unit is part of `e` diff --git a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts index 30aae0eb7795003e57fe2a236054605a2ebf8a4c..b540d9d49a79f89f98d0d1f3ff0e7bd24a66d467 100644 --- a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts +++ b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts @@ -134,6 +134,7 @@ function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: In let changed = false if (Link.isLoci(loci)) { if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = Link.remapLoci(loci, structure) for (const l of loci.links) { const idx = getTerminalLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex]) if (idx !== undefined) { @@ -142,6 +143,7 @@ function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: In } } else if (StructureElement.isLoci(loci)) { if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) // TODO mark link only when both of the link elements are in a StructureElement.Loci const { getElementIndex, getTerminalLinkIndices, elements } = structure.carbohydrates for (const e of loci.elements) { diff --git a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts index 155ca3c165cd57a55e49277423f8aa57e68a8d16..b90daa355fa7a586ed872f7d689db3cc334318d5 100644 --- a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts +++ b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts @@ -108,6 +108,7 @@ function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Inter let changed = false if (Link.isLoci(loci)) { if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = Link.remapLoci(loci, structure) for (const b of loci.links) { const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit) if (indices) { diff --git a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts index 37f0bb0ba619ecbb911ccc7928097534a55827b9..204015f873524fc7e1d8a6f83fd264ca189a2a6b 100644 --- a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts +++ b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts @@ -98,6 +98,7 @@ function eachLink(loci: Loci, structure: Structure, apply: (interval: Interval) let changed = false if (Link.isLoci(loci)) { if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = Link.remapLoci(loci, structure) for (const b of loci.links) { const idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit) if (idx !== -1) { @@ -106,6 +107,7 @@ function eachLink(loci: Loci, structure: Structure, apply: (interval: Interval) } } else if (StructureElement.isLoci(loci)) { if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) // TODO mark link only when both of the link elements are in a StructureElement.Loci for (const e of loci.elements) { OrderedSet.forEach(e.indices, v => { diff --git a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts index af8f091cd3620a2716a0172a14b15ec53fd38b62..55fe6843a1468b8be77a3b04168ad96e43e38d62 100644 --- a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts +++ b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts @@ -122,6 +122,7 @@ function eachLink(loci: Loci, structureGroup: StructureGroup, apply: (interval: if (Link.isLoci(loci)) { const { structure, group } = structureGroup if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = Link.remapLoci(loci, structure) const unit = group.units[0] if (!Unit.isAtomic(unit)) return false const groupCount = unit.links.edgeCount * 2 @@ -137,6 +138,7 @@ function eachLink(loci: Loci, structureGroup: StructureGroup, apply: (interval: } else if (StructureElement.isLoci(loci)) { const { structure, group } = structureGroup if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) const unit = group.units[0] if (!Unit.isAtomic(unit)) return false const groupCount = unit.links.edgeCount * 2 diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts index 885e01805c39cc7a2ec860e6d23035650b537729..55c8dfa68e7329e6d071048260f871e9ba58bfa1 100644 --- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts +++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts @@ -14,7 +14,7 @@ import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; import { Segmentation } from '../../../mol-data/int'; import { CylinderProps } from '../../../mol-geo/primitive/cylinder'; -import { isNucleic, isPurinBase, isPyrimidineBase } from '../../../mol-model/structure/model/types'; +import { isNucleic, isPurineBase, isPyrimidineBase } from '../../../mol-model/structure/model/types'; import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder'; import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual'; import { NucleotideLocationIterator, getNucleotideElementLoci, eachNucleotideElement } from './util/nucleotide'; @@ -78,7 +78,21 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St let idx1: ElementIndex | -1 = -1, idx2: ElementIndex | -1 = -1, idx3: ElementIndex | -1 = -1, idx4: ElementIndex | -1 = -1, idx5: ElementIndex | -1 = -1, idx6: ElementIndex | -1 = -1 let width = 4.5, height = 4.5, depth = 2.5 * sizeFactor - if (isPurinBase(compId)) { + let isPurine = isPurineBase(compId) + let isPyrimidine = isPyrimidineBase(compId) + + if (!isPurine && !isPyrimidine) { + // detect Purine or Pyrimidin based on geometry + const idxC4 = atomicIndex.findAtomOnResidue(residueIndex, 'C4') + const idxN9 = atomicIndex.findAtomOnResidue(residueIndex, 'N9') + if (idxC4 !== -1 && idxN9 !== -1 && Vec3.distance(pos(idxC4, p1), pos(idxN9, p2)) < 1.6) { + isPurine = true + } else { + isPyrimidine = true + } + } + + if (isPurine) { height = 4.5 idx1 = atomicIndex.findAtomOnResidue(residueIndex, 'N1') idx2 = atomicIndex.findAtomOnResidue(residueIndex, 'C4') @@ -86,13 +100,17 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St idx4 = atomicIndex.findAtomOnResidue(residueIndex, 'C2') idx5 = atomicIndex.findAtomOnResidue(residueIndex, 'N9') idx6 = traceElementIndex[residueIndex] - } else if (isPyrimidineBase(compId)) { + } else if (isPyrimidine) { height = 3.0 idx1 = atomicIndex.findAtomOnResidue(residueIndex, 'N3') idx2 = atomicIndex.findAtomOnResidue(residueIndex, 'C6') idx3 = atomicIndex.findAtomOnResidue(residueIndex, 'C4') idx4 = atomicIndex.findAtomOnResidue(residueIndex, 'C2') idx5 = atomicIndex.findAtomOnResidue(residueIndex, 'N1') + if (idx5 === -1) { + // modified ring, e.g. DZ + idx5 = atomicIndex.findAtomOnResidue(residueIndex, 'C1') + } idx6 = traceElementIndex[residueIndex] } diff --git a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts index 992b107cf2ee1e219370aee2d3f1b6c0fb20434f..871fe5364818ef593b93ed25e1634f56a7545377 100644 --- a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts +++ b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts @@ -14,7 +14,7 @@ import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; import { Segmentation } from '../../../mol-data/int'; import { CylinderProps } from '../../../mol-geo/primitive/cylinder'; -import { isNucleic, isPurinBase, isPyrimidineBase } from '../../../mol-model/structure/model/types'; +import { isNucleic, isPurineBase, isPyrimidineBase } from '../../../mol-model/structure/model/types'; import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder'; import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere'; import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual'; @@ -101,15 +101,37 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str builderState.currentGroup = i - if (isPurinBase(compId)) { + let isPurine = isPurineBase(compId) + let isPyrimidine = isPyrimidineBase(compId) + + if (!isPurine && !isPyrimidine) { + // detect Purine or Pyrimidin based on geometry + const idxC4 = atomicIndex.findAtomOnResidue(residueIndex, 'C4') + const idxN9 = atomicIndex.findAtomOnResidue(residueIndex, 'N9') + if (idxC4 !== -1 && idxN9 !== -1 && Vec3.distance(pos(idxC4, pC4), pos(idxN9, pN9)) < 1.6) { + isPurine = true + } else { + isPyrimidine = true + } + } + + if (isPurine) { idxTrace = traceElementIndex[residueIndex] idxN1 = atomicIndex.findAtomOnResidue(residueIndex, 'N1') idxC2 = atomicIndex.findAtomOnResidue(residueIndex, 'C2') idxN3 = atomicIndex.findAtomOnResidue(residueIndex, 'N3') idxC4 = atomicIndex.findAtomOnResidue(residueIndex, 'C4') idxC5 = atomicIndex.findAtomOnResidue(residueIndex, 'C5') + if (idxC5 === -1) { + // modified ring, e.g. DP + idxC5 = atomicIndex.findAtomOnResidue(residueIndex, 'N5') + } idxC6 = atomicIndex.findAtomOnResidue(residueIndex, 'C6') idxN7 = atomicIndex.findAtomOnResidue(residueIndex, 'N7') + if (idxN7 === -1) { + // modified ring, e.g. DP + idxN7 = atomicIndex.findAtomOnResidue(residueIndex, 'C7') + } idxC8 = atomicIndex.findAtomOnResidue(residueIndex, 'C8') idxN9 = atomicIndex.findAtomOnResidue(residueIndex, 'N9') @@ -131,9 +153,13 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str MeshBuilder.addTriangleFan(builderState, positionsRing5_6, fanIndicesTopRing5_6) MeshBuilder.addTriangleFan(builderState, positionsRing5_6, fanIndicesBottomRing5_6) } - } else if (isPyrimidineBase(compId)) { + } else if (isPyrimidine) { idxTrace = traceElementIndex[residueIndex] idxN1 = atomicIndex.findAtomOnResidue(residueIndex, 'N1') + if (idxN1 === -1) { + // modified ring, e.g. DZ + idxN1 = atomicIndex.findAtomOnResidue(residueIndex, 'C1') + } idxC2 = atomicIndex.findAtomOnResidue(residueIndex, 'C2') idxN3 = atomicIndex.findAtomOnResidue(residueIndex, 'N3') idxC4 = atomicIndex.findAtomOnResidue(residueIndex, 'C4') diff --git a/src/mol-repr/structure/visual/util/element.ts b/src/mol-repr/structure/visual/util/element.ts index ec1a76717826923089a8fad264e99288d3c05742..e492772ed8429eb1c37bf32a7265ebda46f3345a 100644 --- a/src/mol-repr/structure/visual/util/element.ts +++ b/src/mol-repr/structure/visual/util/element.ts @@ -73,6 +73,7 @@ export function eachElement(loci: Loci, structureGroup: StructureGroup, apply: ( if (!StructureElement.isLoci(loci)) return false const { structure, group } = structureGroup if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) const elementCount = group.elements.length for (const e of loci.elements) { const unitIdx = group.unitIndexMap.get(e.unit.id) diff --git a/src/mol-repr/structure/visual/util/nucleotide.ts b/src/mol-repr/structure/visual/util/nucleotide.ts index dd3d32c503b3d28b347b95799a03906b14346ec9..5406facc48d095ae677a67a8642349011c59acb2 100644 --- a/src/mol-repr/structure/visual/util/nucleotide.ts +++ b/src/mol-repr/structure/visual/util/nucleotide.ts @@ -46,6 +46,7 @@ export function eachNucleotideElement(loci: Loci, structureGroup: StructureGroup if (!StructureElement.isLoci(loci)) return false const { structure, group } = structureGroup if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) const unit = group.units[0] if (!Unit.isAtomic(unit)) return false const { nucleotideElements, model, elements } = unit diff --git a/src/mol-repr/structure/visual/util/polymer.ts b/src/mol-repr/structure/visual/util/polymer.ts index 325e631ca49d9e920bb6fa73bf83221c8e02edd2..b185f488350c3ea99ec4eacd04e32d1f1cb39419 100644 --- a/src/mol-repr/structure/visual/util/polymer.ts +++ b/src/mol-repr/structure/visual/util/polymer.ts @@ -96,6 +96,7 @@ export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, a if (!StructureElement.isLoci(loci)) return false const { structure, group } = structureGroup if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) const { polymerElements, model, elements } = group.units[0] const { index, offsets } = model.atomicHierarchy.residueAtomSegments const { traceElementIndex } = model.atomicHierarchy.derived.residue @@ -157,6 +158,7 @@ export function eachPolymerGapElement(loci: Loci, structureGroup: StructureGroup if (Link.isLoci(loci)) { const { structure, group } = structureGroup if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = Link.remapLoci(loci, structure) const groupCount = group.units[0].gapElements.length for (const b of loci.links) { const unitIdx = group.unitIndexMap.get(b.aUnit.id) @@ -171,6 +173,7 @@ export function eachPolymerGapElement(loci: Loci, structureGroup: StructureGroup } else if (StructureElement.isLoci(loci)) { const { structure, group } = structureGroup if (!Structure.areParentsEquivalent(loci.structure, structure)) return false + loci = StructureElement.Loci.remap(loci, structure) const groupCount = group.units[0].gapElements.length for (const e of loci.elements) { const unitIdx = group.unitIndexMap.get(e.unit.id) diff --git a/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts b/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts index c510f7c3b61ea818b3561948d2156aa23dc60c41..5c7b3b535de15bb94b680c1ed937543b44509a77 100644 --- a/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts +++ b/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts @@ -5,7 +5,7 @@ */ import { Unit, StructureElement, ElementIndex, ResidueIndex, Structure } from '../../../../../mol-model/structure'; -import { Segmentation } from '../../../../../mol-data/int'; +import { Segmentation, SortedArray } from '../../../../../mol-data/int'; import { MoleculeType, SecondaryStructureType } from '../../../../../mol-model/structure/model/types'; import Iterator from '../../../../../mol-data/iterator'; import { Vec3 } from '../../../../../mol-math/linear-algebra'; @@ -69,12 +69,14 @@ const tmpVecB = Vec3() export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> { private value: PolymerTraceElement - private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex> + private polymerIt: SortedRanges.Iterator<ElementIndex, number> private residueIt: Segmentation.SegmentIterator<ResidueIndex> - private polymerSegment: Segmentation.Segment<ResidueIndex> + private polymerSegment: Segmentation.Segment<number> private cyclicPolymerMap: Map<ResidueIndex, ResidueIndex> private secondaryStructureType: SecondaryStructure['type'] private secondaryStructureGetIndex: SecondaryStructure['getIndex'] + private residueSegmentBeg: ResidueIndex + private residueSegmentEnd: ResidueIndex private residueSegmentMin: ResidueIndex private residueSegmentMax: ResidueIndex private prevSecStrucType: SecondaryStructureType @@ -84,6 +86,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> private currCoarseBackbone: boolean private nextCoarseBackbone: boolean private state: AtomicPolymerTraceIteratorState = AtomicPolymerTraceIteratorState.nextPolymer + private polymerRanges: SortedArray<ElementIndex> private residueAtomSegments: Segmentation<ElementIndex, ResidueIndex> private traceElementIndex: ArrayLike<ElementIndex> private directionFromElementIndex: ArrayLike<ElementIndex | -1> @@ -114,10 +117,12 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> } } - private updateResidueSegmentRange(polymerSegment: Segmentation.Segment<ResidueIndex>) { + private updateResidueSegmentRange(polymerSegment: Segmentation.Segment<number>) { const { index } = this.residueAtomSegments - this.residueSegmentMin = index[this.unit.elements[polymerSegment.start]] - this.residueSegmentMax = index[this.unit.elements[polymerSegment.end - 1]] + this.residueSegmentBeg = index[this.unit.elements[polymerSegment.start]] + this.residueSegmentEnd = index[this.unit.elements[polymerSegment.end - 1]] + this.residueSegmentMin = index[this.polymerRanges[polymerSegment.index * 2]] + this.residueSegmentMax = index[this.polymerRanges[polymerSegment.index * 2 + 1] - 1] } private getResidueIndex(residueIndex: number) { @@ -153,9 +158,13 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> } private setFromToVector(out: Vec3, residueIndex: ResidueIndex) { - this.pos(tmpVecA, this.directionFromElementIndex[residueIndex]) - this.pos(tmpVecB, this.directionToElementIndex[residueIndex]) - Vec3.sub(out, tmpVecB, tmpVecA) + if (this.value.isCoarseBackbone) { + Vec3.set(out, 1, 0, 0) + } else { + this.pos(tmpVecA, this.directionFromElementIndex[residueIndex]) + this.pos(tmpVecB, this.directionToElementIndex[residueIndex]) + Vec3.sub(out, tmpVecB, tmpVecA) + } } private setDirection(out: Vec3, v1: Vec3, v2: Vec3, v3: Vec3) { @@ -177,9 +186,9 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> if (residueIt.hasNext) { this.state = AtomicPolymerTraceIteratorState.nextResidue this.currSecStrucType = SecStrucTypeNA - this.nextSecStrucType = this.getSecStruc(this.residueSegmentMin) + this.nextSecStrucType = this.getSecStruc(this.residueSegmentBeg) this.currCoarseBackbone = false - this.nextCoarseBackbone = this.directionFromElementIndex[this.residueSegmentMin] === -1 || this.directionToElementIndex[this.residueSegmentMin] === -1 + this.nextCoarseBackbone = this.directionFromElementIndex[this.residueSegmentBeg] === -1 || this.directionToElementIndex[this.residueSegmentBeg] === -1 break } } @@ -201,9 +210,10 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> value.isCoarseBackbone = this.currCoarseBackbone value.coarseBackboneFirst = this.prevCoarseBackbone !== this.currCoarseBackbone value.coarseBackboneLast = this.currCoarseBackbone !== this.nextCoarseBackbone - value.first = residueIndex === this.residueSegmentMin - value.last = residueIndex === this.residueSegmentMax + value.first = residueIndex === this.residueSegmentBeg + value.last = residueIndex === this.residueSegmentEnd value.moleculeType = this.moleculeType[residueIndex] + value.isCoarseBackbone = this.directionFromElementIndex[residueIndex] === -1 || this.directionToElementIndex[residueIndex] === -1 const residueIndexPrev3 = this.getResidueIndex(residueIndex - 3) const residueIndexPrev2 = this.getResidueIndex(residueIndex - 2) @@ -212,39 +222,22 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> const residueIndexNext2 = this.getResidueIndex(residueIndex + 2) const residueIndexNext3 = this.getResidueIndex(residueIndex + 3) - if (value.first) { - value.centerPrev.element = this.traceElementIndex[residueIndexPrev1] - value.center.element = this.traceElementIndex[residueIndex] - - this.pos(this.p0, this.traceElementIndex[residueIndexPrev3]) - this.pos(this.p1, this.traceElementIndex[residueIndexPrev2]) - this.pos(this.p2, this.traceElementIndex[residueIndexPrev1]) - this.pos(this.p3, this.traceElementIndex[residueIndex]) - this.pos(this.p4, this.traceElementIndex[residueIndexNext1]) - this.pos(this.p5, this.traceElementIndex[residueIndexNext2]) - - this.setFromToVector(this.d01, residueIndexPrev1) - this.setFromToVector(this.d12, residueIndex) - this.setFromToVector(this.d23, residueIndexNext1) - } else { - value.centerPrev.element = value.center.element - value.center.element = value.centerNext.element - - Vec3.copy(this.p0, this.p1) - Vec3.copy(this.p1, this.p2) - Vec3.copy(this.p2, this.p3) - Vec3.copy(this.p3, this.p4) - Vec3.copy(this.p4, this.p5) - Vec3.copy(this.p5, this.p6) - - Vec3.copy(this.d01, this.d12) - Vec3.copy(this.d12, this.d23) - Vec3.copy(this.d23, this.d34) - } + value.centerPrev.element = this.traceElementIndex[residueIndexPrev1] + value.center.element = this.traceElementIndex[residueIndex] value.centerNext.element = this.traceElementIndex[residueIndexNext1] + + this.pos(this.p0, this.traceElementIndex[residueIndexPrev3]) + this.pos(this.p1, this.traceElementIndex[residueIndexPrev2]) + this.pos(this.p2, this.traceElementIndex[residueIndexPrev1]) + this.pos(this.p3, this.traceElementIndex[residueIndex]) + this.pos(this.p4, this.traceElementIndex[residueIndexNext1]) + this.pos(this.p5, this.traceElementIndex[residueIndexNext2]) this.pos(this.p6, this.traceElementIndex[residueIndexNext3]) + + this.setFromToVector(this.d01, residueIndexPrev1) + this.setFromToVector(this.d12, residueIndex) + this.setFromToVector(this.d23, residueIndexNext1) this.setFromToVector(this.d34, residueIndexNext2) - value.isCoarseBackbone = this.directionFromElementIndex[residueIndex] === -1 || this.directionToElementIndex[residueIndex] === -1 this.setControlPoint(value.p0, this.p0, this.p1, this.p2, residueIndexPrev2) this.setControlPoint(value.p1, this.p1, this.p2, this.p3, residueIndexPrev1) @@ -268,12 +261,13 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> constructor(private unit: Unit.Atomic, structure: Structure) { this.atomicConformation = unit.model.atomicConformation this.residueAtomSegments = unit.model.atomicHierarchy.residueAtomSegments + this.polymerRanges = unit.model.atomicHierarchy.polymerRanges this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex as ArrayLike<ElementIndex> // can assume it won't be -1 for polymer residues this.directionFromElementIndex = unit.model.atomicHierarchy.derived.residue.directionFromElementIndex this.directionToElementIndex = unit.model.atomicHierarchy.derived.residue.directionToElementIndex this.moleculeType = unit.model.atomicHierarchy.derived.residue.moleculeType this.cyclicPolymerMap = unit.model.atomicHierarchy.cyclicPolymerMap - this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements) + this.polymerIt = SortedRanges.transientSegments(this.polymerRanges, unit.elements) this.residueIt = Segmentation.transientSegments(this.residueAtomSegments, unit.elements); this.value = createPolymerTraceElement(unit) this.hasNext = this.residueIt.hasNext && this.polymerIt.hasNext @@ -359,6 +353,8 @@ export class CoarsePolymerTraceIterator implements Iterator<PolymerTraceElement> constructor(private unit: Unit.Spheres | Unit.Gaussians, structure: Structure) { this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements); this.value = createPolymerTraceElement(unit) + Vec3.set(this.value.d12, 1, 0, 0) + Vec3.set(this.value.d23, 1, 0, 0) switch (unit.kind) { case Unit.Kind.Spheres: this.conformation = unit.model.coarseConformation.spheres; break case Unit.Kind.Gaussians: this.conformation = unit.model.coarseConformation.gaussians; break diff --git a/src/mol-repr/visual.ts b/src/mol-repr/visual.ts index 1b6f2530b7bf6f6b4ba3146045092d3c0ca8495f..263dcaada8036131c5f521b7534bcc7c6a7353f5 100644 --- a/src/mol-repr/visual.ts +++ b/src/mol-repr/visual.ts @@ -82,15 +82,17 @@ namespace Visual { // ensure texture has right size createOverpaint(overpaint.layers.length ? count : 0, renderObject.values) - // clear if requested + // clear all if requested if (clear) clearOverpaint(tOverpaint.ref.value.array, 0, count) for (let i = 0, il = overpaint.layers.length; i < il; ++i) { - const { loci, color } = overpaint.layers[i] + const { loci, color, clear } = overpaint.layers[i] const apply = (interval: Interval) => { const start = Interval.start(interval) const end = Interval.end(interval) - return applyOverpaintColor(tOverpaint.ref.value.array, start, end, color, overpaint.alpha) + return clear + ? clearOverpaint(tOverpaint.ref.value.array, start, end) + : applyOverpaintColor(tOverpaint.ref.value.array, start, end, color, overpaint.alpha) } lociApply(loci, apply) } diff --git a/src/mol-script/language/symbol-table/structure-query.ts b/src/mol-script/language/symbol-table/structure-query.ts index 3a6ed18461deb903c8d0fc3acfec873e61ad8bf6..b129ba31909cceaf9586e0eab21723e277ffdf3e 100644 --- a/src/mol-script/language/symbol-table/structure-query.ts +++ b/src/mol-script/language/symbol-table/structure-query.ts @@ -254,6 +254,8 @@ const atomProperty = { sourceIndex: atomProp(Type.Num, 'Index of the atom/element in the input file.'), operatorName: atomProp(Type.Str, 'Name of the symmetry operator applied to this element.'), + modelIndex: atomProp(Type.Num, 'Index of the model in the input file.'), + modelLabel: atomProp(Type.Str, 'Label/header of the model in the input file.') }, topology: { diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts index e2d75927f1b7e559f70b67f4650f59aa5f7d77a3..ee5e68247bfed35e8c6bd476824246240ec86352 100644 --- a/src/mol-script/runtime/query/table.ts +++ b/src/mol-script/runtime/query/table.ts @@ -247,6 +247,8 @@ const symbols = [ D(MolScript.structureQuery.atomProperty.core.z, atomProp(StructureProperties.atom.z)), D(MolScript.structureQuery.atomProperty.core.sourceIndex, atomProp(StructureProperties.atom.sourceIndex)), D(MolScript.structureQuery.atomProperty.core.operatorName, atomProp(StructureProperties.unit.operator_name)), + D(MolScript.structureQuery.atomProperty.core.modelIndex, atomProp(StructureProperties.unit.model_index)), + D(MolScript.structureQuery.atomProperty.core.modelLabel, atomProp(StructureProperties.unit.model_label)), D(MolScript.structureQuery.atomProperty.core.atomKey, (ctx, _) => cantorPairing(ctx.element.unit.id, ctx.element.element)), // TODO: diff --git a/src/mol-script/script/mol-script/symbols.ts b/src/mol-script/script/mol-script/symbols.ts index 9893d843b4cdfec6a42314a131f0a3d7c8efcfdc..eaa70e1332f235b08ae73c021fdf2620897ccd1c 100644 --- a/src/mol-script/script/mol-script/symbols.ts +++ b/src/mol-script/script/mol-script/symbols.ts @@ -101,6 +101,7 @@ export const SymbolTable = [ Alias(MolScript.structureQuery.generator.queryInSelection, 'sel.atom.query-in-selection'), Alias(MolScript.structureQuery.generator.rings, 'sel.atom.rings'), Alias(MolScript.structureQuery.generator.empty, 'sel.atom.empty'), + Alias(MolScript.structureQuery.generator.all, 'sel.atom.all'), // Macro(MSymbol('sel.atom.atoms', Arguments.Dictionary({ // 0: Argument(Type.Bool, { isOptional: true, defaultValue: true, description: 'Test applied to each atom.' }) @@ -198,6 +199,8 @@ export const SymbolTable = [ Alias(MolScript.structureQuery.atomProperty.core.z, 'atom.z'), Alias(MolScript.structureQuery.atomProperty.core.sourceIndex, 'atom.src-index'), Alias(MolScript.structureQuery.atomProperty.core.operatorName, 'atom.op-name'), + Alias(MolScript.structureQuery.atomProperty.core.modelIndex, 'atom.model-index'), + Alias(MolScript.structureQuery.atomProperty.core.modelLabel, 'atom.model-label'), Alias(MolScript.structureQuery.atomProperty.core.atomKey, 'atom.key'), Alias(MolScript.structureQuery.atomProperty.core.bondCount, 'atom.bond-count'), diff --git a/src/mol-theme/overpaint.ts b/src/mol-theme/overpaint.ts index 9f566a056773e2b873b64dd393ce1128dc82cbe3..d885a5de8273907f5606fb76c5b910f75842d17e 100644 --- a/src/mol-theme/overpaint.ts +++ b/src/mol-theme/overpaint.ts @@ -12,7 +12,7 @@ export { Overpaint } type Overpaint = { layers: ReadonlyArray<Overpaint.Layer>, readonly alpha: number } namespace Overpaint { - export type Layer = { readonly loci: Loci, readonly color: Color } + export type Layer = { readonly loci: Loci, readonly color: Color, readonly clear: boolean } export const Empty: Overpaint = { layers: [], alpha: 1 } export function areEqual(oA: Overpaint, oB: Overpaint) { @@ -20,6 +20,7 @@ namespace Overpaint { if (oA.layers.length !== oB.layers.length) return false if (oA.alpha !== oB.alpha) return false for (let i = 0, il = oA.layers.length; i < il; ++i) { + if (oA.layers[i].clear !== oB.layers[i].clear) return false if (oA.layers[i].color !== oB.layers[i].color) return false if (!Loci.areEqual(oA.layers[i].loci, oB.layers[i].loci)) return false }