assembly-symmetry.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. /**
  2. * Copyright (c) 2018-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 { AssemblySymmetryValue, AssemblySymmetryProvider, AssemblySymmetry } from '../assembly-symmetry';
  8. import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
  9. import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
  10. import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
  11. import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
  12. import { RuntimeContext } from '../../../mol-task';
  13. import { Shape } from '../../../mol-model/shape';
  14. import { ColorNames } from '../../../mol-util/color/names';
  15. import { ShapeRepresentation } from '../../../mol-repr/shape/representation';
  16. import { MarkerActions } from '../../../mol-util/marker-action';
  17. import { Prism, PrismCage } from '../../../mol-geo/primitive/prism';
  18. import { Wedge, WedgeCage } from '../../../mol-geo/primitive/wedge';
  19. import { Primitive, transformPrimitive } from '../../../mol-geo/primitive/primitive';
  20. import { memoize1 } from '../../../mol-util/memoize';
  21. import { polygon } from '../../../mol-geo/primitive/polygon';
  22. import { ColorMap, Color } from '../../../mol-util/color';
  23. import { TableLegend } from '../../../mol-util/legend';
  24. import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
  25. import { Cage, transformCage, cloneCage } from '../../../mol-geo/primitive/cage';
  26. import { OctahedronCage } from '../../../mol-geo/primitive/octahedron';
  27. import { TetrahedronCage } from '../../../mol-geo/primitive/tetrahedron';
  28. import { IcosahedronCage } from '../../../mol-geo/primitive/icosahedron';
  29. import { degToRad, radToDeg } from '../../../mol-math/misc';
  30. import { Mutable } from '../../../mol-util/type-helpers';
  31. import { equalEps } from '../../../mol-math/linear-algebra/3d/common';
  32. import { Structure } from '../../../mol-model/structure';
  33. import { isInteger } from '../../../mol-util/number';
  34. const OrderColors = ColorMap({
  35. '2': ColorNames.deepskyblue,
  36. '3': ColorNames.lime,
  37. 'N': ColorNames.red,
  38. })
  39. const OrderColorsLegend = TableLegend(Object.keys(OrderColors).map(name => {
  40. return [name, (OrderColors as any)[name] as Color] as [string, Color]
  41. }))
  42. function axesColorHelp(value: { name: string, params: {} }) {
  43. return value.name === 'byOrder'
  44. ? { description: 'Color axes by their order', legend: OrderColorsLegend }
  45. : {}
  46. }
  47. const SharedParams = {
  48. ...Mesh.Params,
  49. scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
  50. }
  51. const AxesParams = {
  52. ...SharedParams,
  53. axesColor: PD.MappedStatic('byOrder', {
  54. byOrder: PD.EmptyGroup(),
  55. uniform: PD.Group({
  56. colorValue: PD.Color(ColorNames.orange),
  57. }, { isFlat: true })
  58. }, { help: axesColorHelp }),
  59. }
  60. type AxesParams = typeof AxesParams
  61. const CageParams = {
  62. ...SharedParams,
  63. cageColor: PD.Color(ColorNames.orange),
  64. }
  65. type CageParams = typeof CageParams
  66. const AssemblySymmetryVisuals = {
  67. 'axes': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, AxesParams>) => ShapeRepresentation(getAxesShape, Mesh.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) }),
  68. 'cage': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CageParams>) => ShapeRepresentation(getCageShape, Mesh.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) }),
  69. }
  70. export const AssemblySymmetryParams = {
  71. ...AxesParams,
  72. ...CageParams,
  73. visuals: PD.MultiSelect(['axes', 'cage'], PD.objectToOptions(AssemblySymmetryVisuals)),
  74. }
  75. export type AssemblySymmetryParams = typeof AssemblySymmetryParams
  76. export type AssemblySymmetryProps = PD.Values<AssemblySymmetryParams>
  77. //
  78. function getAssemblyName(s: Structure) {
  79. const id = s.units[0].conformation.operator.assembly?.id || ''
  80. return isInteger(id) ? `Assembly ${id}` : id
  81. }
  82. const t = Mat4.identity()
  83. const tmpV = Vec3()
  84. const tmpCenter = Vec3()
  85. const tmpScale = Vec3()
  86. const getOrderPrimitive = memoize1((order: number): Primitive | undefined => {
  87. if (order < 2) {
  88. return Prism(polygon(48, false))
  89. } else if (order === 2) {
  90. const lens = Prism(polygon(48, false))
  91. const m = Mat4.identity()
  92. Mat4.scale(m, m, Vec3.create(1, 0.35, 1))
  93. transformPrimitive(lens, m)
  94. return lens
  95. } else if (order === 3) {
  96. return Wedge()
  97. } else {
  98. return Prism(polygon(order, false))
  99. }
  100. })
  101. function getAxesMesh(data: AssemblySymmetryValue, props: PD.Values<AxesParams>, mesh?: Mesh) {
  102. const { scale } = props
  103. const { rotation_axes } = data
  104. if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
  105. const { start, end } = rotation_axes[0]
  106. const radius = (Vec3.distance(start, end) / 500) * scale
  107. Vec3.set(tmpScale, radius * 7, radius * 7, radius * 0.4)
  108. const cylinderProps = { radiusTop: radius, radiusBottom: radius }
  109. const builderState = MeshBuilder.createState(256, 128, mesh)
  110. builderState.currentGroup = 0
  111. Vec3.scale(tmpCenter, Vec3.add(tmpCenter, start, end), 0.5)
  112. for (let i = 0, il = rotation_axes.length; i < il; ++i) {
  113. const { order, start, end } = rotation_axes[i]
  114. builderState.currentGroup = i
  115. addCylinder(builderState, start, end, 1, cylinderProps)
  116. const primitive = getOrderPrimitive(order)
  117. if (primitive) {
  118. Vec3.scale(tmpCenter, Vec3.add(tmpCenter, start, end), 0.5)
  119. if (Vec3.dot(Vec3.unitY, Vec3.sub(tmpV, start, tmpCenter)) === 0) {
  120. Mat4.targetTo(t, start, tmpCenter, Vec3.unitY)
  121. } else {
  122. Mat4.targetTo(t, start, tmpCenter, Vec3.unitX)
  123. }
  124. Mat4.scale(t, t, tmpScale)
  125. Mat4.setTranslation(t, start)
  126. MeshBuilder.addPrimitive(builderState, t, primitive)
  127. Mat4.setTranslation(t, end)
  128. MeshBuilder.addPrimitive(builderState, t, primitive)
  129. }
  130. }
  131. return MeshBuilder.getMesh(builderState)
  132. }
  133. function getAxesShape(ctx: RuntimeContext, data: Structure, props: AssemblySymmetryProps, shape?: Shape<Mesh>) {
  134. const assemblySymmetry = AssemblySymmetryProvider.get(data).value!
  135. const geo = getAxesMesh(assemblySymmetry, props, shape && shape.geometry);
  136. const getColor = (groupId: number) => {
  137. if (props.axesColor.name === 'byOrder') {
  138. const { rotation_axes } = assemblySymmetry
  139. const order = rotation_axes![groupId]?.order
  140. if (order === 2) return OrderColors[2]
  141. else if (order === 3) return OrderColors[3]
  142. else return OrderColors.N
  143. } else {
  144. return props.axesColor.params.colorValue
  145. }
  146. }
  147. const getLabel = (groupId: number) => {
  148. const { type, symbol, kind, rotation_axes } = assemblySymmetry
  149. const order = rotation_axes![groupId]?.order
  150. return [
  151. `<small>${data.model.entryId}</small>`,
  152. `<small>${getAssemblyName(data)}</small>`,
  153. `Axis ${groupId + 1} with Order ${order} of ${type} ${kind} (${symbol})`
  154. ].join(' | ')
  155. }
  156. return Shape.create('Axes', data, geo, getColor, () => 1, getLabel)
  157. }
  158. //
  159. const getSymbolCage = memoize1((symbol: string): Cage | undefined => {
  160. if (symbol.startsWith('D') || symbol.startsWith('C')) {
  161. // z axis is prism axis, x/y axes cut through edge midpoints
  162. const fold = parseInt(symbol.substr(1))
  163. if (fold === 2) {
  164. return PrismCage(polygon(4, false))
  165. } else if (fold === 3) {
  166. return WedgeCage()
  167. } else if (fold > 3) {
  168. return PrismCage(polygon(fold, false))
  169. }
  170. } else if (symbol === 'O') {
  171. // x/y/z axes cut through order 4 vertices
  172. return OctahedronCage()
  173. } else if (symbol === 'I') {
  174. // z axis cut through order 5 vertex
  175. // x axis cut through edge midpoint
  176. const cage = IcosahedronCage()
  177. const m = Mat4.identity()
  178. Mat4.rotate(m, m, degToRad(31.7), Vec3.unitX)
  179. return transformCage(cloneCage(cage), m)
  180. } else if (symbol === 'T') {
  181. // x/y/z axes cut through edge midpoints
  182. return TetrahedronCage()
  183. }
  184. })
  185. function getSymbolScale(symbol: string) {
  186. if (symbol.startsWith('D') || symbol.startsWith('C')) {
  187. return 0.75
  188. } else if (symbol === 'O') {
  189. return 1.2
  190. } else if (symbol === 'I') {
  191. return 0.25
  192. } else if (symbol === 'T') {
  193. return 0.8
  194. }
  195. return 1
  196. }
  197. function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.RotationAxes, size: number, structure: Structure) {
  198. const eye = Vec3()
  199. const target = Vec3()
  200. const up = Vec3()
  201. let pair: Mutable<AssemblySymmetry.RotationAxes> | undefined = undefined
  202. if (symbol.startsWith('C')) {
  203. pair = [axes[0]]
  204. } else if (symbol.startsWith('D')) {
  205. const fold = parseInt(symbol.substr(1))
  206. if (fold === 2) {
  207. pair = axes.filter(a => a.order === 2)
  208. } else if (fold >= 3) {
  209. const aN = axes.filter(a => a.order === fold)[0]
  210. const a2 = axes.filter(a => a.order === 2)[0]
  211. pair = [aN, a2]
  212. }
  213. } else if (symbol === 'O') {
  214. pair = axes.filter(a => a.order === 4)
  215. } else if (symbol === 'I') {
  216. const a5 = axes.filter(a => a.order === 5)[0]
  217. const a5dir = Vec3.sub(Vec3(), a5.end, a5.start)
  218. pair = [a5]
  219. for (const a of axes.filter(a => a.order === 3)) {
  220. let d = radToDeg(Vec3.angle(Vec3.sub(up, a.end, a.start), a5dir))
  221. if (equalEps(d, 100.81, 0.1) || equalEps(d, 79.19, 0.1)) {
  222. pair[1] = a
  223. break
  224. }
  225. }
  226. } else if (symbol === 'T') {
  227. pair = axes.filter(a => a.order === 2)
  228. }
  229. Mat4.setIdentity(t)
  230. if (pair) {
  231. const [aA, aB] = pair
  232. Vec3.scale(eye, Vec3.add(eye, aA.end, aA.start), 0.5)
  233. Vec3.copy(target, aA.end)
  234. if (aB) {
  235. Vec3.sub(up, aB.end, aB.start)
  236. const d = Vec3.dot(eye, up)
  237. if (d < 0) Vec3.negate(up, up)
  238. Mat4.targetTo(t, eye, target, up)
  239. Mat4.scaleUniformly(t, t, size * getSymbolScale(symbol))
  240. } else {
  241. if (Vec3.dot(Vec3.unitY, Vec3.sub(tmpV, aA.end, aA.start)) === 0) {
  242. Vec3.copy(up, Vec3.unitY)
  243. } else {
  244. Vec3.copy(up, Vec3.unitX)
  245. }
  246. const sizeXY = (structure.lookup3d.boundary.sphere.radius * 2) * 0.8
  247. Mat4.targetTo(t, eye, target, up)
  248. Mat4.scale(t, t, Vec3.create(sizeXY, sizeXY, size))
  249. }
  250. }
  251. }
  252. function getCageMesh(data: Structure, props: PD.Values<CageParams>, mesh?: Mesh) {
  253. const assemblySymmetry = AssemblySymmetryProvider.get(data).value!
  254. const { scale } = props
  255. const { rotation_axes, symbol } = assemblySymmetry
  256. if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
  257. const cage = getSymbolCage(symbol)
  258. if (!cage) return Mesh.createEmpty(mesh)
  259. const { start, end } = rotation_axes[0]
  260. const size = Vec3.distance(start, end)
  261. const radius = (size / 500) * scale
  262. const builderState = MeshBuilder.createState(256, 128, mesh)
  263. builderState.currentGroup = 0
  264. setSymbolTransform(t, symbol, rotation_axes, size, data)
  265. Vec3.scale(tmpCenter, Vec3.add(tmpCenter, start, end), 0.5)
  266. Mat4.setTranslation(t, tmpCenter)
  267. MeshBuilder.addCage(builderState, t, cage, radius, 1, 8)
  268. return MeshBuilder.getMesh(builderState)
  269. }
  270. function getCageShape(ctx: RuntimeContext, data: Structure, props: AssemblySymmetryProps, shape?: Shape<Mesh>) {
  271. const assemblySymmetry = AssemblySymmetryProvider.get(data).value!
  272. const geo = getCageMesh(data, props, shape && shape.geometry);
  273. const getColor = (groupId: number) => {
  274. return props.cageColor
  275. }
  276. const getLabel = (groupId: number) => {
  277. const { type, symbol, kind } = assemblySymmetry
  278. data.model.entryId
  279. return [
  280. `<small>${data.model.entryId}</small>`,
  281. `<small>${getAssemblyName(data)}</small>`,
  282. `Cage of ${type} ${kind} (${symbol})`
  283. ].join(' | ')
  284. }
  285. return Shape.create('Cage', data, geo, getColor, () => 1, getLabel)
  286. }
  287. //
  288. export type AssemblySymmetryRepresentation = Representation<Structure, AssemblySymmetryParams>
  289. export function AssemblySymmetryRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, AssemblySymmetryParams>): AssemblySymmetryRepresentation {
  290. return Representation.createMulti('Assembly Symmetry', ctx, getParams, Representation.StateBuilder, AssemblySymmetryVisuals as unknown as Representation.Def<Structure, AssemblySymmetryParams>)
  291. }