camera-helper.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. /**
  2. * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import produce from 'immer';
  7. import { Interval } from '../../mol-data/int/interval';
  8. import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
  9. import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
  10. import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
  11. import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
  12. import { PickingId } from '../../mol-geo/geometry/picking';
  13. import { GraphicsRenderObject } from '../../mol-gl/render-object';
  14. import { Scene } from '../../mol-gl/scene';
  15. import { WebGLContext } from '../../mol-gl/webgl/context';
  16. import { GraphicsRenderVariantsBlended } from '../../mol-gl/webgl/render-item';
  17. import { Sphere3D } from '../../mol-math/geometry';
  18. import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
  19. import { DataLoci, EmptyLoci, Loci } from '../../mol-model/loci';
  20. import { Shape } from '../../mol-model/shape';
  21. import { Visual } from '../../mol-repr/visual';
  22. import { ColorNames } from '../../mol-util/color/names';
  23. import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
  24. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  25. import { Camera, ICamera } from '../camera';
  26. import { Viewport } from '../camera/util';
  27. // TODO add scale line/grid
  28. const AxesParams = {
  29. ...Mesh.Params,
  30. alpha: { ...Mesh.Params.alpha, defaultValue: 0.51 },
  31. ignoreLight: { ...Mesh.Params.ignoreLight, defaultValue: true },
  32. colorX: PD.Color(ColorNames.red, { isEssential: true }),
  33. colorY: PD.Color(ColorNames.green, { isEssential: true }),
  34. colorZ: PD.Color(ColorNames.blue, { isEssential: true }),
  35. scale: PD.Numeric(0.33, { min: 0.1, max: 2, step: 0.1 }, { isEssential: true }),
  36. };
  37. type AxesParams = typeof AxesParams
  38. type AxesProps = PD.Values<AxesParams>
  39. export const CameraHelperParams = {
  40. axes: PD.MappedStatic('on', {
  41. on: PD.Group(AxesParams),
  42. off: PD.Group({})
  43. }, { cycle: true, description: 'Show camera orientation axes' }),
  44. };
  45. export type CameraHelperParams = typeof CameraHelperParams
  46. export type CameraHelperProps = PD.Values<CameraHelperParams>
  47. export class CameraHelper {
  48. scene: Scene;
  49. camera: Camera;
  50. props: CameraHelperProps = {
  51. axes: { name: 'off', params: {} }
  52. };
  53. private renderObject: GraphicsRenderObject | undefined;
  54. constructor(private webgl: WebGLContext, props: Partial<CameraHelperProps> = {}) {
  55. this.scene = Scene.create(webgl, GraphicsRenderVariantsBlended);
  56. this.camera = new Camera();
  57. Vec3.set(this.camera.up, 0, 1, 0);
  58. Vec3.set(this.camera.target, 0, 0, 0);
  59. this.setProps(props);
  60. }
  61. setProps(props: Partial<CameraHelperProps>) {
  62. this.props = produce(this.props, p => {
  63. if (props.axes !== undefined) {
  64. p.axes.name = props.axes.name;
  65. if (props.axes.name === 'on') {
  66. this.scene.clear();
  67. const params = { ...props.axes.params, scale: props.axes.params.scale * this.webgl.pixelRatio };
  68. this.renderObject = createAxesRenderObject(params);
  69. this.scene.add(this.renderObject);
  70. this.scene.commit();
  71. Vec3.set(this.camera.position, 0, 0, params.scale * 200);
  72. Mat4.lookAt(this.camera.view, this.camera.position, this.camera.target, this.camera.up);
  73. p.axes.params = { ...props.axes.params };
  74. }
  75. }
  76. });
  77. }
  78. get isEnabled() {
  79. return this.props.axes.name === 'on';
  80. }
  81. getLoci(pickingId: PickingId) {
  82. const { objectId, groupId, instanceId } = pickingId;
  83. if (!this.renderObject || objectId !== this.renderObject.id || groupId === CameraHelperAxis.None) return EmptyLoci;
  84. return CameraAxesLoci(this, groupId, instanceId);
  85. }
  86. private eachGroup = (loci: Loci, apply: (interval: Interval) => boolean): boolean => {
  87. if (!this.renderObject) return false;
  88. if (!isCameraAxesLoci(loci)) return false;
  89. let changed = false;
  90. const groupCount = this.renderObject.values.uGroupCount.ref.value;
  91. const { elements } = loci;
  92. for (const { groupId, instanceId } of elements) {
  93. const idx = instanceId * groupCount + groupId;
  94. if (apply(Interval.ofSingleton(idx))) changed = true;
  95. }
  96. return changed;
  97. };
  98. mark(loci: Loci, action: MarkerAction) {
  99. if (!MarkerActions.is(MarkerActions.Highlighting, action)) return false;
  100. if (!isCameraAxesLoci(loci)) return false;
  101. if (loci.data !== this) return false;
  102. return Visual.mark(this.renderObject, loci, action, this.eachGroup);
  103. }
  104. update(camera: ICamera) {
  105. if (!this.renderObject) return;
  106. updateCamera(this.camera, camera.viewport, camera.viewOffset);
  107. Mat4.extractRotation(this.scene.view, camera.view);
  108. const r = this.renderObject.values.boundingSphere.ref.value.radius;
  109. Mat4.setTranslation(this.scene.view, Vec3.create(
  110. -camera.viewport.width / 2 + r,
  111. -camera.viewport.height / 2 + r,
  112. 0
  113. ));
  114. }
  115. }
  116. export const enum CameraHelperAxis {
  117. None = 0,
  118. X,
  119. Y,
  120. Z,
  121. XY,
  122. XZ,
  123. YZ
  124. }
  125. function getAxisLabel(axis: number) {
  126. switch (axis) {
  127. case CameraHelperAxis.X: return 'X Axis';
  128. case CameraHelperAxis.Y: return 'Y Axis';
  129. case CameraHelperAxis.Z: return 'Z Axis';
  130. case CameraHelperAxis.XY: return 'XY Plane';
  131. case CameraHelperAxis.XZ: return 'XZ Plane';
  132. case CameraHelperAxis.YZ: return 'YZ Plane';
  133. default: return 'Axes';
  134. }
  135. }
  136. function CameraAxesLoci(cameraHelper: CameraHelper, groupId: number, instanceId: number) {
  137. return DataLoci('camera-axes', cameraHelper, [{ groupId, instanceId }],
  138. void 0 /** bounding sphere */,
  139. () => getAxisLabel(groupId));
  140. }
  141. export type CameraAxesLoci = ReturnType<typeof CameraAxesLoci>
  142. export function isCameraAxesLoci(x: Loci): x is CameraAxesLoci {
  143. return x.kind === 'data-loci' && x.tag === 'camera-axes';
  144. }
  145. function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.ViewOffset) {
  146. const { near, far } = camera;
  147. const fullLeft = -viewport.width / 2;
  148. const fullRight = viewport.width / 2;
  149. const fullTop = viewport.height / 2;
  150. const fullBottom = -viewport.height / 2;
  151. const dx = (fullRight - fullLeft) / 2;
  152. const dy = (fullTop - fullBottom) / 2;
  153. const cx = (fullRight + fullLeft) / 2;
  154. const cy = (fullTop + fullBottom) / 2;
  155. let left = cx - dx;
  156. let right = cx + dx;
  157. let top = cy + dy;
  158. let bottom = cy - dy;
  159. if (viewOffset.enabled) {
  160. const scaleW = (fullRight - fullLeft) / viewOffset.width;
  161. const scaleH = (fullTop - fullBottom) / viewOffset.height;
  162. left += scaleW * viewOffset.offsetX;
  163. right = left + scaleW * viewOffset.width;
  164. top -= scaleH * viewOffset.offsetY;
  165. bottom = top - scaleH * viewOffset.height;
  166. }
  167. Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
  168. }
  169. function createAxesMesh(scale: number, mesh?: Mesh) {
  170. const state = MeshBuilder.createState(512, 256, mesh);
  171. const radius = 0.075 * scale;
  172. const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
  173. const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
  174. const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
  175. const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
  176. state.currentGroup = CameraHelperAxis.None;
  177. addSphere(state, Vec3.origin, radius, 2);
  178. state.currentGroup = CameraHelperAxis.X;
  179. addSphere(state, x, radius, 2);
  180. addCylinder(state, Vec3.origin, x, 1, cylinderProps);
  181. state.currentGroup = CameraHelperAxis.Y;
  182. addSphere(state, y, radius, 2);
  183. addCylinder(state, Vec3.origin, y, 1, cylinderProps);
  184. state.currentGroup = CameraHelperAxis.Z;
  185. addSphere(state, z, radius, 2);
  186. addCylinder(state, Vec3.origin, z, 1, cylinderProps);
  187. Vec3.scale(x, x, 0.5);
  188. Vec3.scale(y, y, 0.5);
  189. Vec3.scale(z, z, 0.5);
  190. state.currentGroup = CameraHelperAxis.XY;
  191. MeshBuilder.addTriangle(state, Vec3.origin, x, y);
  192. MeshBuilder.addTriangle(state, Vec3.origin, y, x);
  193. const xy = Vec3.add(Vec3(), x, y);
  194. MeshBuilder.addTriangle(state, xy, x, y);
  195. MeshBuilder.addTriangle(state, xy, y, x);
  196. state.currentGroup = CameraHelperAxis.XZ;
  197. MeshBuilder.addTriangle(state, Vec3.origin, x, z);
  198. MeshBuilder.addTriangle(state, Vec3.origin, z, x);
  199. const xz = Vec3.add(Vec3(), x, z);
  200. MeshBuilder.addTriangle(state, xz, x, z);
  201. MeshBuilder.addTriangle(state, xz, z, x);
  202. state.currentGroup = CameraHelperAxis.YZ;
  203. MeshBuilder.addTriangle(state, Vec3.origin, y, z);
  204. MeshBuilder.addTriangle(state, Vec3.origin, z, y);
  205. const yz = Vec3.add(Vec3(), y, z);
  206. MeshBuilder.addTriangle(state, yz, y, z);
  207. MeshBuilder.addTriangle(state, yz, z, y);
  208. return MeshBuilder.getMesh(state);
  209. }
  210. function getAxesShape(props: AxesProps, shape?: Shape<Mesh>) {
  211. const scale = 100 * props.scale;
  212. const mesh = createAxesMesh(scale, shape?.geometry);
  213. mesh.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale + scale / 4));
  214. const getColor = (groupId: number) => {
  215. switch (groupId) {
  216. case 1: return props.colorX;
  217. case 2: return props.colorY;
  218. case 3: return props.colorZ;
  219. default: return ColorNames.grey;
  220. }
  221. };
  222. return Shape.create('axes', {}, mesh, getColor, () => 1, () => '');
  223. }
  224. function createAxesRenderObject(props: AxesProps) {
  225. const shape = getAxesShape(props);
  226. return Shape.createRenderObject(shape, props);
  227. }