labels.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { PluginContext } from 'mol-plugin/context';
  7. import { PluginBehavior } from '../behavior';
  8. import { ParamDefinition as PD } from 'mol-util/param-definition'
  9. import { Mat4, Vec3 } from 'mol-math/linear-algebra';
  10. import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
  11. import { StateObjectCell, State, StateSelection } from 'mol-state';
  12. import { RuntimeContext } from 'mol-task';
  13. import { Shape } from 'mol-model/shape';
  14. import { Text } from 'mol-geo/geometry/text/text';
  15. import { ShapeRepresentation } from 'mol-repr/shape/representation';
  16. import { ColorNames } from 'mol-util/color/tables';
  17. import { TextBuilder } from 'mol-geo/geometry/text/text-builder';
  18. import { Unit, StructureElement, StructureProperties } from 'mol-model/structure';
  19. import { SetUtils } from 'mol-util/set';
  20. import { arrayEqual } from 'mol-util';
  21. import { MoleculeType } from 'mol-model/structure/model/types';
  22. import { getElementMoleculeType } from 'mol-model/structure/util';
  23. // TODO
  24. // - support more object types than structures
  25. // - tether label to the element nearest to the bounding sphere center
  26. // - [Started] multiple levels of labels: structure, polymer, ligand
  27. // - show structure/unit label only when there is a representation with sufficient overlap
  28. // - support highlighting
  29. // - better support saccharides (use data available after re-mediation)
  30. // - size based on min bbox dimension (to avoid huge labels for very long but narrow polymers)
  31. // - fixed size labels (invariant to zoom) [needs feature in text geo]
  32. // - ??? max label length
  33. // - ??? multi line labels [needs feature in text geo]
  34. // - ??? use prevalent (how to define) color of representations of a structure to color the label
  35. // - completely different approach (render not as 3d objects): overlay free layout in screenspace with occlusion info from bboxes
  36. export type SceneLabelsLevels = 'structure' | 'polymer' | 'ligand'
  37. export const SceneLabelsParams = {
  38. ...Text.Params,
  39. background: PD.Boolean(true),
  40. backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
  41. backgroundColor: PD.Color(ColorNames.snow),
  42. backgroundOpacity: PD.Numeric(0.9, { min: 0, max: 1, step: 0.01 }),
  43. levels: PD.MultiSelect([] as SceneLabelsLevels[], [
  44. ['structure', 'structure'], ['polymer', 'polymer'], ['ligand', 'ligand']
  45. ] as [SceneLabelsLevels, string][]),
  46. }
  47. export type SceneLabelsParams = typeof SceneLabelsParams
  48. export type SceneLabelsProps = PD.Values<typeof SceneLabelsParams>
  49. interface LabelsData {
  50. transforms: Mat4[]
  51. texts: string[]
  52. positions: Vec3[]
  53. sizes: number[]
  54. depths: number[]
  55. }
  56. function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: Text) {
  57. const { texts, positions, depths } = data
  58. const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
  59. for (let i = 0, il = texts.length; i < il; ++i) {
  60. const p = positions[i]
  61. textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
  62. }
  63. return textBuilder.getText()
  64. }
  65. export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
  66. name: 'scene-labels',
  67. category: 'representation',
  68. display: { name: 'Scene Labels', group: 'Labels' },
  69. canAutoUpdate: () => true,
  70. ctor: class extends PluginBehavior.Handler<SceneLabelsProps> {
  71. private data: LabelsData = {
  72. transforms: [Mat4.identity()],
  73. texts: [],
  74. positions: [],
  75. sizes: [],
  76. depths: []
  77. }
  78. private repr: ShapeRepresentation<LabelsData, Text, SceneLabelsParams>
  79. private geo = Text.createEmpty()
  80. private structures = new Set<SO.Molecule.Structure>()
  81. constructor(protected ctx: PluginContext, protected params: SceneLabelsProps) {
  82. super(ctx, params)
  83. this.repr = ShapeRepresentation(this.getLabelsShape, Text.Utils)
  84. ctx.events.state.object.created.subscribe(this.triggerUpdate)
  85. ctx.events.state.object.removed.subscribe(this.triggerUpdate)
  86. ctx.events.state.object.updated.subscribe(this.triggerUpdate)
  87. ctx.events.state.cell.stateUpdated.subscribe(this.triggerUpdate)
  88. }
  89. private triggerUpdate = async () => {
  90. await this.update(this.params)
  91. }
  92. private getColor = () => ColorNames.dimgrey
  93. private getSize = (groupId: number) => this.data.sizes[groupId]
  94. private getLabel = () => ''
  95. private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => {
  96. this.geo = getLabelsText(data, props, this.geo)
  97. return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
  98. }
  99. /** Update structures to be labeled, returns true if changed */
  100. private updateStructures(p: SceneLabelsProps) {
  101. const state = this.ctx.state.dataState
  102. const structures = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
  103. const rootStructures = new Set<SO.Molecule.Structure>()
  104. for (const s of structures) {
  105. const rootStructure = getRootStructure(s, state)
  106. if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
  107. if (!state.cellStates.get(s.transform.ref).isHidden) {
  108. rootStructures.add(rootStructure.obj)
  109. }
  110. }
  111. if (!SetUtils.areEqual(rootStructures, this.structures)) {
  112. this.structures = rootStructures
  113. return true
  114. } else {
  115. return false
  116. }
  117. }
  118. private updateLabels(p: SceneLabelsProps) {
  119. const l = StructureElement.create()
  120. const { texts, positions, sizes, depths } = this.data
  121. texts.length = 0
  122. positions.length = 0
  123. sizes.length = 0
  124. depths.length = 0
  125. this.structures.forEach(structure => {
  126. if (p.levels.includes('structure')) {
  127. texts.push(`${structure.data.model.label}`)
  128. positions.push(structure.data.boundary.sphere.center)
  129. sizes.push(structure.data.boundary.sphere.radius / 10)
  130. depths.push(structure.data.boundary.sphere.radius)
  131. }
  132. for (let i = 0, il = structure.data.units.length; i < il; ++i) {
  133. let label = ''
  134. const u = structure.data.units[i]
  135. l.unit = u
  136. l.element = u.elements[0]
  137. if (p.levels.includes('polymer') && u.polymerElements.length) {
  138. label = `${StructureProperties.entity.pdbx_description(l).join(', ')} (${getAsymId(u)(l)})`
  139. }
  140. if (p.levels.includes('ligand') && !u.polymerElements.length) {
  141. const moleculeType = getElementMoleculeType(u, u.elements[0])
  142. if (moleculeType === MoleculeType.other || moleculeType === MoleculeType.saccharide) {
  143. label = `${StructureProperties.entity.pdbx_description(l).join(', ')} (${getAsymId(u)(l)})`
  144. }
  145. }
  146. if (label) {
  147. texts.push(label)
  148. const { center, radius } = u.lookup3d.boundary.sphere
  149. const transformedCenter = Vec3.transformMat4(Vec3.zero(), center, u.conformation.operator.matrix)
  150. positions.push(transformedCenter)
  151. sizes.push(Math.max(2, radius / 10))
  152. depths.push(radius)
  153. }
  154. }
  155. })
  156. }
  157. register(): void { }
  158. async update(p: SceneLabelsProps) {
  159. // console.log('update')
  160. let updated = false
  161. if (this.updateStructures(p) || !arrayEqual(this.params.levels, p.levels)) {
  162. // console.log('update with data')
  163. this.updateLabels(p)
  164. await this.repr.createOrUpdate(p, this.data).run()
  165. updated = true
  166. } else if (!PD.areEqual(SceneLabelsParams, this.params, p)) {
  167. // console.log('update props only')
  168. await this.repr.createOrUpdate(p).run()
  169. updated = true
  170. }
  171. if (updated) {
  172. Object.assign(this.params, p)
  173. this.ctx.canvas3d.add(this.repr)
  174. }
  175. return updated;
  176. }
  177. unregister() {
  178. }
  179. },
  180. params: () => SceneLabelsParams
  181. });
  182. //
  183. function getRootStructure(root: StateObjectCell, state: State) {
  184. let parent: StateObjectCell | undefined
  185. while (true) {
  186. const _parent = StateSelection.findAncestorOfType(state.tree, state.cells, root.transform.ref, [PluginStateObject.Molecule.Structure])
  187. if (_parent) {
  188. parent = _parent
  189. root = _parent
  190. } else {
  191. break
  192. }
  193. }
  194. return parent ? parent :
  195. SO.Molecule.Structure.is(root.obj) ? root : undefined
  196. }
  197. function getAsymId(unit: Unit): StructureElement.Property<string> {
  198. switch (unit.kind) {
  199. case Unit.Kind.Atomic:
  200. return StructureProperties.chain.auth_asym_id
  201. case Unit.Kind.Spheres:
  202. case Unit.Kind.Gaussians:
  203. return StructureProperties.coarse.asym_id
  204. }
  205. }