validation-report-clashes.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  7. import { Unit, Structure, StructureElement, Bond } from '../../../mol-model/structure';
  8. import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
  9. import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
  10. import { Vec3 } from '../../../mol-math/linear-algebra';
  11. import { PickingId } from '../../../mol-geo/geometry/picking';
  12. import { EmptyLoci, Loci, DataLoci } from '../../../mol-model/loci';
  13. import { Interval } from '../../../mol-data/int';
  14. import { RepresentationContext, RepresentationParamsGetter, Representation } from '../../../mol-repr/representation';
  15. import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider, ComplexRepresentation } from '../../../mol-repr/structure/representation';
  16. import { UnitKind, UnitKindOptions } from '../../../mol-repr/structure/visual/util/common';
  17. import { VisualContext } from '../../../mol-repr/visual';
  18. import { createLinkCylinderMesh, LinkCylinderParams, LinkCylinderStyle } from '../../../mol-repr/structure/visual/util/link';
  19. import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
  20. import { VisualUpdateState } from '../../../mol-repr/util';
  21. import { LocationIterator } from '../../../mol-geo/util/location-iterator';
  22. import { ClashesProvider, IntraUnitClashes, InterUnitClashes } from '../validation-report';
  23. import { CustomProperty } from '../../common/custom-property';
  24. import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual';
  25. import { Color } from '../../../mol-util/color';
  26. import { MarkerActions } from '../../../mol-util/marker-action';
  27. import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
  28. import { Sphere3D } from '../../../mol-math/geometry';
  29. import { bondLabel } from '../../../mol-theme/label';
  30. //
  31. function createIntraUnitClashCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitClashParams>, mesh?: Mesh) {
  32. if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
  33. const clashes = ClashesProvider.get(structure).value!.intraUnit.get(unit.id)
  34. const { edgeCount, a, b, edgeProps } = clashes
  35. const { magnitude } = edgeProps
  36. const { sizeFactor } = props
  37. if (!edgeCount) return Mesh.createEmpty(mesh)
  38. const { elements } = unit
  39. const pos = unit.conformation.invariantPosition
  40. const builderProps = {
  41. linkCount: edgeCount * 2,
  42. position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
  43. pos(elements[a[edgeIndex]], posA)
  44. pos(elements[b[edgeIndex]], posB)
  45. },
  46. style: (edgeIndex: number) => LinkCylinderStyle.Disk,
  47. radius: (edgeIndex: number) => magnitude[edgeIndex] * sizeFactor,
  48. }
  49. return createLinkCylinderMesh(ctx, builderProps, props, mesh)
  50. }
  51. export const IntraUnitClashParams = {
  52. ...UnitsMeshParams,
  53. ...LinkCylinderParams,
  54. linkCap: PD.Boolean(true),
  55. sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.01 }),
  56. }
  57. export type IntraUnitClashParams = typeof IntraUnitClashParams
  58. export function IntraUnitClashVisual(materialId: number): UnitsVisual<IntraUnitClashParams> {
  59. return UnitsMeshVisual<IntraUnitClashParams>({
  60. defaultProps: PD.getDefaultValues(IntraUnitClashParams),
  61. createGeometry: createIntraUnitClashCylinderMesh,
  62. createLocationIterator: createIntraClashIterator,
  63. getLoci: getIntraClashLoci,
  64. eachLocation: eachIntraClash,
  65. setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitClashParams>, currentProps: PD.Values<IntraUnitClashParams>) => {
  66. state.createGeometry = (
  67. newProps.sizeFactor !== currentProps.sizeFactor ||
  68. newProps.radialSegments !== currentProps.radialSegments ||
  69. newProps.linkScale !== currentProps.linkScale ||
  70. newProps.linkSpacing !== currentProps.linkSpacing ||
  71. newProps.linkCap !== currentProps.linkCap
  72. )
  73. }
  74. }, materialId)
  75. }
  76. function getIntraClashBoundingSphere(unit: Unit.Atomic, clashes: IntraUnitClashes, elements: number[], boundingSphere: Sphere3D) {
  77. return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => {
  78. unit.conformation.position(unit.elements[clashes.a[elements[i]]], pA)
  79. unit.conformation.position(unit.elements[clashes.b[elements[i]]], pB)
  80. }, boundingSphere)
  81. }
  82. function getIntraClashLabel(structure: Structure, unit: Unit.Atomic, clashes: IntraUnitClashes, elements: number[]) {
  83. const idx = elements[0]
  84. if (idx === undefined) return ''
  85. const { edgeProps: { id, magnitude, distance } } = clashes
  86. const mag = magnitude[idx].toFixed(2)
  87. const dist = distance[idx].toFixed(2)
  88. return [
  89. `Clash id: ${id[idx]} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
  90. bondLabel(Bond.Location(structure, unit, clashes.a[idx], structure, unit, clashes.b[idx]))
  91. ].join('</br>')
  92. }
  93. function IntraClashLoci(structure: Structure, unit: Unit.Atomic, clashes: IntraUnitClashes, elements: number[]) {
  94. return DataLoci('intra-clashes', { unit, clashes }, elements,
  95. (boundingSphere: Sphere3D) => getIntraClashBoundingSphere(unit, clashes, elements, boundingSphere),
  96. () => getIntraClashLabel(structure, unit, clashes, elements))
  97. }
  98. function getIntraClashLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
  99. const { objectId, instanceId, groupId } = pickingId
  100. if (id === objectId) {
  101. const { structure, group } = structureGroup
  102. const unit = group.units[instanceId]
  103. if (Unit.isAtomic(unit)) {
  104. const clashes = ClashesProvider.get(structure).value!.intraUnit.get(unit.id)
  105. return IntraClashLoci(structure, unit, clashes, [groupId])
  106. }
  107. }
  108. return EmptyLoci
  109. }
  110. function eachIntraClash(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
  111. let changed = false
  112. // TODO
  113. return changed
  114. }
  115. function createIntraClashIterator(structureGroup: StructureGroup): LocationIterator {
  116. const { structure, group } = structureGroup
  117. const unit = group.units[0]
  118. const clashes = ClashesProvider.get(structure).value!.intraUnit.get(unit.id)
  119. const { a } = clashes
  120. const groupCount = clashes.edgeCount * 2
  121. const instanceCount = group.units.length
  122. const location = StructureElement.Location.create(structure)
  123. const getLocation = (groupIndex: number, instanceIndex: number) => {
  124. const unit = group.units[instanceIndex]
  125. location.unit = unit
  126. location.element = unit.elements[a[groupIndex]]
  127. return location
  128. }
  129. return LocationIterator(groupCount, instanceCount, getLocation)
  130. }
  131. //
  132. function createInterUnitClashCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitClashParams>, mesh?: Mesh) {
  133. const clashes = ClashesProvider.get(structure).value!.interUnit
  134. const { edges, edgeCount } = clashes
  135. const { sizeFactor } = props
  136. if (!edgeCount) return Mesh.createEmpty(mesh)
  137. const builderProps = {
  138. linkCount: edgeCount,
  139. position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
  140. const b = edges[edgeIndex]
  141. const uA = b.unitA, uB = b.unitB
  142. uA.conformation.position(uA.elements[b.indexA], posA)
  143. uB.conformation.position(uB.elements[b.indexB], posB)
  144. },
  145. style: (edgeIndex: number) => LinkCylinderStyle.Disk,
  146. radius: (edgeIndex: number) => edges[edgeIndex].props.magnitude * sizeFactor
  147. }
  148. return createLinkCylinderMesh(ctx, builderProps, props, mesh)
  149. }
  150. export const InterUnitClashParams = {
  151. ...ComplexMeshParams,
  152. ...LinkCylinderParams,
  153. linkCap: PD.Boolean(true),
  154. sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.01 }),
  155. }
  156. export type InterUnitClashParams = typeof InterUnitClashParams
  157. export function InterUnitClashVisual(materialId: number): ComplexVisual<InterUnitClashParams> {
  158. return ComplexMeshVisual<InterUnitClashParams>({
  159. defaultProps: PD.getDefaultValues(InterUnitClashParams),
  160. createGeometry: createInterUnitClashCylinderMesh,
  161. createLocationIterator: createInterClashIterator,
  162. getLoci: getInterClashLoci,
  163. eachLocation: eachInterClash,
  164. setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitClashParams>, currentProps: PD.Values<InterUnitClashParams>) => {
  165. state.createGeometry = (
  166. newProps.sizeFactor !== currentProps.sizeFactor ||
  167. newProps.radialSegments !== currentProps.radialSegments ||
  168. newProps.linkScale !== currentProps.linkScale ||
  169. newProps.linkSpacing !== currentProps.linkSpacing ||
  170. newProps.linkCap !== currentProps.linkCap
  171. )
  172. }
  173. }, materialId)
  174. }
  175. function getInterClashBoundingSphere(clashes: InterUnitClashes, elements: number[], boundingSphere: Sphere3D) {
  176. return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => {
  177. const c = clashes.edges[elements[i]]
  178. c.unitA.conformation.position(c.unitA.elements[c.indexA], pA)
  179. c.unitB.conformation.position(c.unitB.elements[c.indexB], pB)
  180. }, boundingSphere)
  181. }
  182. function getInterClashLabel(structure: Structure, clashes: InterUnitClashes, elements: number[]) {
  183. const idx = elements[0]
  184. if (idx === undefined) return ''
  185. const c = clashes.edges[idx]
  186. const mag = c.props.magnitude.toFixed(2)
  187. const dist = c.props.distance.toFixed(2)
  188. return [
  189. `Clash id: ${c.props.id} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
  190. bondLabel(Bond.Location(structure, c.unitA, c.indexA, structure, c.unitB, c.indexB))
  191. ].join('</br>')
  192. }
  193. function InterClashLoci(structure: Structure, clashes: InterUnitClashes, elements: number[]) {
  194. return DataLoci('inter-clashes', clashes, elements,
  195. (boundingSphere: Sphere3D) => getInterClashBoundingSphere(clashes, elements, boundingSphere),
  196. () => getInterClashLabel(structure, clashes, elements))
  197. }
  198. function getInterClashLoci(pickingId: PickingId, structure: Structure, id: number) {
  199. const { objectId, groupId } = pickingId
  200. if (id === objectId) {
  201. const clashes = ClashesProvider.get(structure).value!.interUnit
  202. return InterClashLoci(structure, clashes, [groupId])
  203. }
  204. return EmptyLoci
  205. }
  206. function eachInterClash(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
  207. let changed = false
  208. // TODO
  209. return changed
  210. }
  211. function createInterClashIterator(structure: Structure): LocationIterator {
  212. const clashes = ClashesProvider.get(structure).value!.interUnit
  213. const groupCount = clashes.edgeCount
  214. const instanceCount = 1
  215. const location = StructureElement.Location.create(structure)
  216. const getLocation = (groupIndex: number) => {
  217. const clash = clashes.edges[groupIndex]
  218. location.unit = clash.unitA
  219. location.element = clash.unitA.elements[clash.indexA]
  220. return location
  221. }
  222. return LocationIterator(groupCount, instanceCount, getLocation, true)
  223. }
  224. //
  225. const ClashesVisuals = {
  226. 'intra-clash': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitClashParams>) => UnitsRepresentation('Intra-unit clash cylinder', ctx, getParams, IntraUnitClashVisual),
  227. 'inter-clash': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitClashParams>) => ComplexRepresentation('Inter-unit clash cylinder', ctx, getParams, InterUnitClashVisual),
  228. }
  229. export const ClashesParams = {
  230. ...IntraUnitClashParams,
  231. ...InterUnitClashParams,
  232. unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions),
  233. visuals: PD.MultiSelect(['intra-clash', 'inter-clash'], PD.objectToOptions(ClashesVisuals))
  234. }
  235. export type ClashesParams = typeof ClashesParams
  236. export function getClashesParams(ctx: ThemeRegistryContext, structure: Structure) {
  237. return PD.clone(ClashesParams)
  238. }
  239. export type ClashesRepresentation = StructureRepresentation<ClashesParams>
  240. export function ClashesRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ClashesParams>): ClashesRepresentation {
  241. const repr = Representation.createMulti('Clashes', ctx, getParams, StructureRepresentationStateBuilder, ClashesVisuals as unknown as Representation.Def<Structure, ClashesParams>)
  242. repr.setState({ markerActions: MarkerActions.Highlighting })
  243. return repr
  244. }
  245. export const ClashesRepresentationProvider: StructureRepresentationProvider<ClashesParams> = {
  246. label: 'RCSB Clashes',
  247. description: 'Displays clashes between atoms as disks.',
  248. factory: ClashesRepresentation,
  249. getParams: getClashesParams,
  250. defaultValues: PD.getDefaultValues(ClashesParams),
  251. defaultColorTheme: { name: 'uniform', props: { value: Color(0xFA28FF) } },
  252. defaultSizeTheme: { name: 'physical' },
  253. isApplicable: (structure: Structure) => structure.elementCount > 0,
  254. ensureCustomProperties: (ctx: CustomProperty.Context, structure: Structure) => {
  255. return ClashesProvider.attach(ctx, structure)
  256. }
  257. }