angle.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /**
  2. * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { Loci } from '../../../mol-model/loci';
  7. import { RuntimeContext } from '../../../mol-task';
  8. import { Lines } from '../../../mol-geo/geometry/lines/lines';
  9. import { Text } from '../../../mol-geo/geometry/text/text';
  10. import { ParamDefinition as PD } from '../../../mol-util/param-definition';
  11. import { ColorNames } from '../../../mol-util/color/names';
  12. import { ShapeRepresentation } from '../representation';
  13. import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation';
  14. import { Shape } from '../../../mol-model/shape';
  15. import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
  16. import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
  17. import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
  18. import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
  19. import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
  20. import { radToDeg, arcLength } from '../../../mol-math/misc';
  21. import { Circle } from '../../../mol-geo/primitive/circle';
  22. import { transformPrimitive } from '../../../mol-geo/primitive/primitive';
  23. import { MarkerActions, MarkerAction } from '../../../mol-util/marker-action';
  24. import { angleLabel } from '../../../mol-theme/label';
  25. import { Sphere3D } from '../../../mol-math/geometry';
  26. import { MeasurementRepresentationCommonTextParams } from './common';
  27. export interface AngleData {
  28. triples: Loci.Bundle<3>[]
  29. }
  30. const SharedParams = {
  31. color: PD.Color(ColorNames.lightgreen),
  32. arcScale: PD.Numeric(0.7, { min: 0.01, max: 1, step: 0.01 })
  33. };
  34. const LinesParams = {
  35. ...Lines.Params,
  36. ...SharedParams,
  37. lineSizeAttenuation: PD.Boolean(true),
  38. linesSize: PD.Numeric(0.04, { min: 0.01, max: 5, step: 0.01 }),
  39. dashLength: PD.Numeric(0.04, { min: 0.01, max: 0.2, step: 0.01 }),
  40. };
  41. const VectorsParams = {
  42. ...LinesParams
  43. };
  44. type VectorsParams = typeof VectorsParams
  45. const ArcParams = {
  46. ...LinesParams
  47. };
  48. type ArcParams = typeof ArcParams
  49. const SectorParams = {
  50. ...Mesh.Params,
  51. ...SharedParams,
  52. ignoreLight: PD.Boolean(true),
  53. sectorOpacity: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }),
  54. };
  55. type SectorParams = typeof SectorParams
  56. const TextParams = {
  57. ...Text.Params,
  58. ...MeasurementRepresentationCommonTextParams,
  59. borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 })
  60. };
  61. type TextParams = typeof TextParams
  62. const AngleVisuals = {
  63. 'vectors': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, VectorsParams>) => ShapeRepresentation(getVectorsShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
  64. 'arc': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, ArcParams>) => ShapeRepresentation(getArcShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
  65. 'sector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, SectorParams>) => ShapeRepresentation(getSectorShape, Mesh.Utils, { modifyProps: p => ({ ...p, alpha: p.sectorOpacity }), modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) }),
  66. 'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, markerActions: MarkerAction.None }) }),
  67. };
  68. export const AngleParams = {
  69. ...VectorsParams,
  70. ...ArcParams,
  71. ...SectorParams,
  72. ...TextParams,
  73. visuals: PD.MultiSelect(['vectors', 'sector', 'text'], PD.objectToOptions(AngleVisuals)),
  74. };
  75. export type AngleParams = typeof AngleParams
  76. export type AngleProps = PD.Values<AngleParams>
  77. //
  78. function getAngleState() {
  79. return {
  80. sphereA: Sphere3D(),
  81. sphereB: Sphere3D(),
  82. sphereC: Sphere3D(),
  83. arcDirA: Vec3(),
  84. arcDirC: Vec3(),
  85. arcNormal: Vec3(),
  86. radius: 0,
  87. angle: 0,
  88. };
  89. }
  90. type AngleState = ReturnType<typeof getAngleState>
  91. const tmpVec = Vec3();
  92. const tmpMat = Mat4();
  93. function setAngleState(triple: Loci.Bundle<3>, state: AngleState, arcScale: number) {
  94. const { sphereA, sphereB, sphereC } = state;
  95. const { arcDirA, arcDirC, arcNormal } = state;
  96. const [lociA, lociB, lociC] = triple.loci;
  97. Loci.getBoundingSphere(lociA, sphereA);
  98. Loci.getBoundingSphere(lociB, sphereB);
  99. Loci.getBoundingSphere(lociC, sphereC);
  100. Vec3.sub(arcDirA, sphereA.center, sphereB.center);
  101. Vec3.sub(arcDirC, sphereC.center, sphereB.center);
  102. Vec3.cross(arcNormal, arcDirA, arcDirC);
  103. const len = Math.min(Vec3.magnitude(arcDirA), Vec3.magnitude(arcDirC));
  104. const radius = len * arcScale;
  105. state.radius = radius;
  106. state.angle = Vec3.angle(arcDirA, arcDirC);
  107. return state;
  108. }
  109. function getCircle(state: AngleState, segmentLength?: number) {
  110. const { radius, angle } = state;
  111. const segments = segmentLength ? arcLength(angle, radius) / segmentLength : 32;
  112. Mat4.targetTo(tmpMat, state.sphereB.center, state.sphereA.center, state.arcNormal);
  113. Mat4.setTranslation(tmpMat, state.sphereB.center);
  114. Mat4.mul(tmpMat, tmpMat, Mat4.rotY180);
  115. const circle = Circle({ radius, thetaLength: angle, segments });
  116. return transformPrimitive(circle, tmpMat);
  117. }
  118. const tmpState = getAngleState();
  119. function getAngleName(data: AngleData) {
  120. return data.triples.length === 1 ? `Angle ${angleLabel(data.triples[0], { measureOnly: true })}` : `${data.triples.length} Angles`;
  121. }
  122. //
  123. function buildVectorsLines(data: AngleData, props: AngleProps, lines?: Lines): Lines {
  124. const builder = LinesBuilder.create(128, 64, lines);
  125. for (let i = 0, il = data.triples.length; i < il; ++i) {
  126. setAngleState(data.triples[i], tmpState, props.arcScale);
  127. builder.addFixedLengthDashes(tmpState.sphereB.center, tmpState.sphereA.center, props.dashLength, i);
  128. builder.addFixedLengthDashes(tmpState.sphereB.center, tmpState.sphereC.center, props.dashLength, i);
  129. }
  130. return builder.getLines();
  131. }
  132. function getVectorsShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Lines>) {
  133. const lines = buildVectorsLines(data, props, shape && shape.geometry);
  134. const name = getAngleName(data);
  135. return Shape.create(name, data, lines, () => props.color, () => props.linesSize, () => '');
  136. }
  137. //
  138. function buildArcLines(data: AngleData, props: AngleProps, lines?: Lines): Lines {
  139. const builder = LinesBuilder.create(128, 64, lines);
  140. for (let i = 0, il = data.triples.length; i < il; ++i) {
  141. setAngleState(data.triples[i], tmpState, props.arcScale);
  142. const circle = getCircle(tmpState, props.dashLength);
  143. const { indices, vertices } = circle;
  144. for (let j = 0, jl = indices.length; j < jl; j += 3) {
  145. if (j % 2 === 1) continue; // draw every other segment to get dashes
  146. const start = indices[j] * 3;
  147. const end = indices[j + 1] * 3;
  148. const startX = vertices[start];
  149. const startY = vertices[start + 1];
  150. const startZ = vertices[start + 2];
  151. const endX = vertices[end];
  152. const endY = vertices[end + 1];
  153. const endZ = vertices[end + 2];
  154. builder.add(startX, startY, startZ, endX, endY, endZ, i);
  155. }
  156. }
  157. return builder.getLines();
  158. }
  159. function getArcShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Lines>) {
  160. const lines = buildArcLines(data, props, shape && shape.geometry);
  161. const name = getAngleName(data);
  162. return Shape.create(name, data, lines, () => props.color, () => props.linesSize, () => '');
  163. }
  164. //
  165. function buildSectorMesh(data: AngleData, props: AngleProps, mesh?: Mesh): Mesh {
  166. const state = MeshBuilder.createState(128, 64, mesh);
  167. for (let i = 0, il = data.triples.length; i < il; ++i) {
  168. setAngleState(data.triples[i], tmpState, props.arcScale);
  169. const circle = getCircle(tmpState);
  170. state.currentGroup = i;
  171. MeshBuilder.addPrimitive(state, Mat4.id, circle);
  172. MeshBuilder.addPrimitiveFlipped(state, Mat4.id, circle);
  173. }
  174. return MeshBuilder.getMesh(state);
  175. }
  176. function getSectorShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Mesh>) {
  177. const mesh = buildSectorMesh(data, props, shape && shape.geometry);
  178. const name = getAngleName(data);
  179. const getLabel = (groupId: number ) => angleLabel(data.triples[groupId]);
  180. return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel);
  181. }
  182. //
  183. function buildText(data: AngleData, props: AngleProps, text?: Text): Text {
  184. const builder = TextBuilder.create(props, 128, 64, text);
  185. for (let i = 0, il = data.triples.length; i < il; ++i) {
  186. setAngleState(data.triples[i], tmpState, props.arcScale);
  187. Vec3.add(tmpVec, tmpState.arcDirA, tmpState.arcDirC);
  188. Vec3.setMagnitude(tmpVec, tmpVec, tmpState.radius);
  189. Vec3.add(tmpVec, tmpState.sphereB.center, tmpVec);
  190. const angle = radToDeg(tmpState.angle).toFixed(2);
  191. const label = props.customText || `${angle}\u00B0`;
  192. const radius = Math.max(2, tmpState.sphereA.radius, tmpState.sphereB.radius, tmpState.sphereC.radius);
  193. const scale = radius / 2;
  194. builder.add(label, tmpVec[0], tmpVec[1], tmpVec[2], 0.1, scale, i);
  195. }
  196. return builder.getText();
  197. }
  198. function getTextShape(ctx: RuntimeContext, data: AngleData, props: AngleProps, shape?: Shape<Text>) {
  199. const text = buildText(data, props, shape && shape.geometry);
  200. const name = getAngleName(data);
  201. const getLabel = (groupId: number ) => angleLabel(data.triples[groupId]);
  202. return Shape.create(name, data, text, () => props.textColor, () => props.textSize, getLabel);
  203. }
  204. //
  205. export type AngleRepresentation = Representation<AngleData, AngleParams>
  206. export function AngleRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, AngleParams>): AngleRepresentation {
  207. return Representation.createMulti('Angle', ctx, getParams, Representation.StateBuilder, AngleVisuals as unknown as Representation.Def<AngleData, AngleParams>);
  208. }