representation.ts 14 KB

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