123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
- import { PluginContext } from 'mol-plugin/context';
- import { PluginBehavior } from '../behavior';
- import { ParamDefinition as PD } from 'mol-util/param-definition'
- import { Mat4, Vec3 } from 'mol-math/linear-algebra';
- import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
- import { StateObjectCell, State, StateSelection } from 'mol-state';
- import { RuntimeContext } from 'mol-task';
- import { Shape } from 'mol-model/shape';
- import { Text } from 'mol-geo/geometry/text/text';
- import { ShapeRepresentation } from 'mol-repr/shape/representation';
- import { ColorNames } from 'mol-util/color/tables';
- import { TextBuilder } from 'mol-geo/geometry/text/text-builder';
- import { Unit, StructureElement, StructureProperties } from 'mol-model/structure';
- import { SetUtils } from 'mol-util/set';
- import { arrayEqual } from 'mol-util';
- import { MoleculeType } from 'mol-model/structure/model/types';
- import { getElementMoleculeType } from 'mol-model/structure/util';
- // TODO
- // - support more object types than structures
- // - tether label to the element nearest to the bounding sphere center
- // - [Started] multiple levels of labels: structure, polymer, ligand
- // - show structure/unit label only when there is a representation with sufficient overlap
- // - support highlighting
- // - better support saccharides (use data available after re-mediation)
- // - size based on min bbox dimension (to avoid huge labels for very long but narrow polymers)
- // - fixed size labels (invariant to zoom) [needs feature in text geo]
- // - ??? max label length
- // - ??? multi line labels [needs feature in text geo]
- // - ??? use prevalent (how to define) color of representations of a structure to color the label
- // - completely different approach (render not as 3d objects): overlay free layout in screenspace with occlusion info from bboxes
- export type SceneLabelsLevels = 'structure' | 'polymer' | 'ligand'
- export const SceneLabelsParams = {
- ...Text.Params,
- background: PD.Boolean(true),
- backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
- backgroundColor: PD.Color(ColorNames.snow),
- backgroundOpacity: PD.Numeric(0.9, { min: 0, max: 1, step: 0.01 }),
- levels: PD.MultiSelect([] as SceneLabelsLevels[], [
- ['structure', 'structure'], ['polymer', 'polymer'], ['ligand', 'ligand']
- ] as [SceneLabelsLevels, string][]),
- }
- export type SceneLabelsParams = typeof SceneLabelsParams
- export type SceneLabelsProps = PD.Values<typeof SceneLabelsParams>
- interface LabelsData {
- transforms: Mat4[]
- texts: string[]
- positions: Vec3[]
- sizes: number[]
- depths: number[]
- }
- function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: Text) {
- const { texts, positions, depths } = data
- const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
- for (let i = 0, il = texts.length; i < il; ++i) {
- const p = positions[i]
- textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
- }
- return textBuilder.getText()
- }
- export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
- name: 'scene-labels',
- category: 'representation',
- display: { name: 'Scene Labels', group: 'Labels' },
- canAutoUpdate: () => true,
- ctor: class extends PluginBehavior.Handler<SceneLabelsProps> {
- private data: LabelsData = {
- transforms: [Mat4.identity()],
- texts: [],
- positions: [],
- sizes: [],
- depths: []
- }
- private repr: ShapeRepresentation<LabelsData, Text, SceneLabelsParams>
- private geo = Text.createEmpty()
- private structures = new Set<SO.Molecule.Structure>()
- constructor(protected ctx: PluginContext, protected params: SceneLabelsProps) {
- super(ctx, params)
- this.repr = ShapeRepresentation(this.getLabelsShape, Text.Utils)
- ctx.events.state.object.created.subscribe(this.triggerUpdate)
- ctx.events.state.object.removed.subscribe(this.triggerUpdate)
- ctx.events.state.object.updated.subscribe(this.triggerUpdate)
- ctx.events.state.cell.stateUpdated.subscribe(this.triggerUpdate)
- }
- private triggerUpdate = async () => {
- await this.update(this.params)
- }
- private getColor = () => ColorNames.dimgrey
- private getSize = (groupId: number) => this.data.sizes[groupId]
- private getLabel = () => ''
- private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => {
- this.geo = getLabelsText(data, props, this.geo)
- return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
- }
- /** Update structures to be labeled, returns true if changed */
- private updateStructures(p: SceneLabelsProps) {
- const state = this.ctx.state.dataState
- const structures = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
- const rootStructures = new Set<SO.Molecule.Structure>()
- for (const s of structures) {
- const rootStructure = getRootStructure(s, state)
- if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
- if (!state.cellStates.get(s.transform.ref).isHidden) {
- rootStructures.add(rootStructure.obj)
- }
- }
- if (!SetUtils.areEqual(rootStructures, this.structures)) {
- this.structures = rootStructures
- return true
- } else {
- return false
- }
- }
- private updateLabels(p: SceneLabelsProps) {
- const l = StructureElement.create()
- const { texts, positions, sizes, depths } = this.data
- texts.length = 0
- positions.length = 0
- sizes.length = 0
- depths.length = 0
- this.structures.forEach(structure => {
- if (p.levels.includes('structure')) {
- texts.push(`${structure.data.model.label}`)
- positions.push(structure.data.boundary.sphere.center)
- sizes.push(structure.data.boundary.sphere.radius / 10)
- depths.push(structure.data.boundary.sphere.radius)
- }
- for (let i = 0, il = structure.data.units.length; i < il; ++i) {
- let label = ''
- const u = structure.data.units[i]
- l.unit = u
- l.element = u.elements[0]
- if (p.levels.includes('polymer') && u.polymerElements.length) {
- label = `${StructureProperties.entity.pdbx_description(l).join(', ')} (${getAsymId(u)(l)})`
- }
- if (p.levels.includes('ligand') && !u.polymerElements.length) {
- const moleculeType = getElementMoleculeType(u, u.elements[0])
- if (moleculeType === MoleculeType.other || moleculeType === MoleculeType.saccharide) {
- label = `${StructureProperties.entity.pdbx_description(l).join(', ')} (${getAsymId(u)(l)})`
- }
- }
- if (label) {
- texts.push(label)
- const { center, radius } = u.lookup3d.boundary.sphere
- const transformedCenter = Vec3.transformMat4(Vec3.zero(), center, u.conformation.operator.matrix)
- positions.push(transformedCenter)
- sizes.push(Math.max(2, radius / 10))
- depths.push(radius)
- }
- }
- })
- }
- register(): void { }
- async update(p: SceneLabelsProps) {
- // console.log('update')
- let updated = false
- if (this.updateStructures(p) || !arrayEqual(this.params.levels, p.levels)) {
- // console.log('update with data')
- this.updateLabels(p)
- await this.repr.createOrUpdate(p, this.data).run()
- updated = true
- } else if (!PD.areEqual(SceneLabelsParams, this.params, p)) {
- // console.log('update props only')
- await this.repr.createOrUpdate(p).run()
- updated = true
- }
- if (updated) {
- Object.assign(this.params, p)
- this.ctx.canvas3d.add(this.repr)
- }
- return updated;
- }
- unregister() {
- }
- },
- params: () => SceneLabelsParams
- });
- //
- function getRootStructure(root: StateObjectCell, state: State) {
- let parent: StateObjectCell | undefined
- while (true) {
- const _parent = StateSelection.findAncestorOfType(state.tree, state.cells, root.transform.ref, [PluginStateObject.Molecule.Structure])
- if (_parent) {
- parent = _parent
- root = _parent
- } else {
- break
- }
- }
- return parent ? parent :
- SO.Molecule.Structure.is(root.obj) ? root : undefined
- }
- function getAsymId(unit: Unit): StructureElement.Property<string> {
- switch (unit.kind) {
- case Unit.Kind.Atomic:
- return StructureProperties.chain.auth_asym_id
- case Unit.Kind.Spheres:
- case Unit.Kind.Gaussians:
- return StructureProperties.coarse.asym_id
- }
- }
|