text.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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 { ParamDefinition as PD } from '../../../mol-util/param-definition';
  7. import { ValueCell } from '../../../mol-util';
  8. import { GeometryUtils } from '../geometry';
  9. import { LocationIterator, PositionLocation } from '../../../mol-geo/util/location-iterator';
  10. import { TransformData } from '../transform-data';
  11. import { Theme } from '../../../mol-theme/theme';
  12. import { createColors } from '../color-data';
  13. import { createSizes, getMaxSize } from '../size-data';
  14. import { createMarkers } from '../marker-data';
  15. import { ColorNames } from '../../../mol-util/color/names';
  16. import { Sphere3D } from '../../../mol-math/geometry';
  17. import { TextureImage, createTextureImage, calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
  18. import { TextValues } from '../../../mol-gl/renderable/text';
  19. import { Color } from '../../../mol-util/color';
  20. import { Vec3, Vec4 } from '../../../mol-math/linear-algebra';
  21. import { FontAtlasParams } from './font-atlas';
  22. import { RenderableState } from '../../../mol-gl/renderable';
  23. import { clamp } from '../../../mol-math/interpolate';
  24. import { createRenderObject as _createRenderObject } from '../../../mol-gl/render-object';
  25. import { BaseGeometry } from '../base';
  26. import { createEmptyOverpaint } from '../overpaint-data';
  27. import { createEmptyTransparency } from '../transparency-data';
  28. import { hashFnv32a } from '../../../mol-data/util';
  29. import { GroupMapping, createGroupMapping } from '../../util';
  30. import { createEmptyClipping } from '../clipping-data';
  31. type TextAttachment = (
  32. 'bottom-left' | 'bottom-center' | 'bottom-right' |
  33. 'middle-left' | 'middle-center' | 'middle-right' |
  34. 'top-left' | 'top-center' | 'top-right'
  35. )
  36. /** Text */
  37. export interface Text {
  38. readonly kind: 'text',
  39. /** Number of characters in the text */
  40. charCount: number,
  41. /** Font Atlas */
  42. readonly fontTexture: ValueCell<TextureImage<Uint8Array>>,
  43. /** Center buffer as array of xyz values wrapped in a value cell */
  44. readonly centerBuffer: ValueCell<Float32Array>,
  45. /** Mapping buffer as array of xy values wrapped in a value cell */
  46. readonly mappingBuffer: ValueCell<Float32Array>,
  47. /** Depth buffer as array of z values wrapped in a value cell */
  48. readonly depthBuffer: ValueCell<Float32Array>,
  49. /** Index buffer as array of center index triplets wrapped in a value cell */
  50. readonly indexBuffer: ValueCell<Uint32Array>,
  51. /** Group buffer as array of group ids for each vertex wrapped in a value cell */
  52. readonly groupBuffer: ValueCell<Float32Array>,
  53. /** Texture coordinates buffer as array of uv values wrapped in a value cell */
  54. readonly tcoordBuffer: ValueCell<Float32Array>,
  55. /** Bounding sphere of the text */
  56. readonly boundingSphere: Sphere3D
  57. /** Maps group ids to text indices */
  58. readonly groupMapping: GroupMapping
  59. setBoundingSphere(boundingSphere: Sphere3D): void
  60. }
  61. export namespace Text {
  62. export function create(fontTexture: TextureImage<Uint8Array>, centers: Float32Array, mappings: Float32Array, depths: Float32Array, indices: Uint32Array, groups: Float32Array, tcoords: Float32Array, charCount: number, text?: Text): Text {
  63. return text ?
  64. update(fontTexture, centers, mappings, depths, indices, groups, tcoords, charCount, text) :
  65. fromData(fontTexture, centers, mappings, depths, indices, groups, tcoords, charCount);
  66. }
  67. export function createEmpty(text?: Text): Text {
  68. const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1, Uint8Array);
  69. const cb = text ? text.centerBuffer.ref.value : new Float32Array(0);
  70. const mb = text ? text.mappingBuffer.ref.value : new Float32Array(0);
  71. const db = text ? text.depthBuffer.ref.value : new Float32Array(0);
  72. const ib = text ? text.indexBuffer.ref.value : new Uint32Array(0);
  73. const gb = text ? text.groupBuffer.ref.value : new Float32Array(0);
  74. const tb = text ? text.tcoordBuffer.ref.value : new Float32Array(0);
  75. return create(ft, cb, mb, db, ib, gb, tb, 0, text);
  76. }
  77. function hashCode(text: Text) {
  78. return hashFnv32a([
  79. text.charCount, text.fontTexture.ref.version,
  80. text.centerBuffer.ref.version, text.mappingBuffer.ref.version,
  81. text.depthBuffer.ref.version, text.indexBuffer.ref.version,
  82. text.groupBuffer.ref.version, text.tcoordBuffer.ref.version
  83. ]);
  84. }
  85. function fromData(fontTexture: TextureImage<Uint8Array>, centers: Float32Array, mappings: Float32Array, depths: Float32Array, indices: Uint32Array, groups: Float32Array, tcoords: Float32Array, charCount: number): Text {
  86. const boundingSphere = Sphere3D();
  87. let groupMapping: GroupMapping;
  88. let currentHash = -1;
  89. let currentGroup = -1;
  90. const text = {
  91. kind: 'text' as const,
  92. charCount,
  93. fontTexture: ValueCell.create(fontTexture),
  94. centerBuffer: ValueCell.create(centers),
  95. mappingBuffer: ValueCell.create(mappings),
  96. depthBuffer: ValueCell.create(depths),
  97. indexBuffer: ValueCell.create(indices),
  98. groupBuffer: ValueCell.create(groups),
  99. tcoordBuffer: ValueCell.create(tcoords),
  100. get boundingSphere() {
  101. const newHash = hashCode(text);
  102. if (newHash !== currentHash) {
  103. const b = calculateInvariantBoundingSphere(text.centerBuffer.ref.value, text.charCount * 4, 4);
  104. Sphere3D.copy(boundingSphere, b);
  105. currentHash = newHash;
  106. }
  107. return boundingSphere;
  108. },
  109. get groupMapping() {
  110. if (text.groupBuffer.ref.version !== currentGroup) {
  111. groupMapping = createGroupMapping(text.groupBuffer.ref.value, text.charCount, 4);
  112. currentGroup = text.groupBuffer.ref.version;
  113. }
  114. return groupMapping;
  115. },
  116. setBoundingSphere(sphere: Sphere3D) {
  117. Sphere3D.copy(boundingSphere, sphere);
  118. currentHash = hashCode(text);
  119. }
  120. };
  121. return text;
  122. }
  123. function update(fontTexture: TextureImage<Uint8Array>, centers: Float32Array, mappings: Float32Array, depths: Float32Array, indices: Uint32Array, groups: Float32Array, tcoords: Float32Array, charCount: number, text: Text) {
  124. text.charCount = charCount;
  125. ValueCell.update(text.fontTexture, fontTexture);
  126. ValueCell.update(text.centerBuffer, centers);
  127. ValueCell.update(text.mappingBuffer, mappings);
  128. ValueCell.update(text.depthBuffer, depths);
  129. ValueCell.update(text.indexBuffer, indices);
  130. ValueCell.update(text.groupBuffer, groups);
  131. ValueCell.update(text.tcoordBuffer, tcoords);
  132. return text;
  133. }
  134. export const Params = {
  135. ...BaseGeometry.Params,
  136. ...FontAtlasParams,
  137. sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
  138. borderWidth: PD.Numeric(0, { min: 0, max: 0.5, step: 0.01 }),
  139. borderColor: PD.Color(ColorNames.grey),
  140. offsetX: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
  141. offsetY: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
  142. offsetZ: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
  143. background: PD.Boolean(false),
  144. backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
  145. backgroundColor: PD.Color(ColorNames.grey),
  146. backgroundOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
  147. tether: PD.Boolean(false),
  148. tetherLength: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }),
  149. tetherBaseWidth: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 }),
  150. attachment: PD.Select('middle-center', [
  151. ['bottom-left', 'bottom-left'], ['bottom-center', 'bottom-center'], ['bottom-right', 'bottom-right'],
  152. ['middle-left', 'middle-left'], ['middle-center', 'middle-center'], ['middle-right', 'middle-right'],
  153. ['top-left', 'top-left'], ['top-center', 'top-center'], ['top-right', 'top-right'],
  154. ] as [TextAttachment, string][]),
  155. };
  156. export type Params = typeof Params
  157. export const Utils: GeometryUtils<Text, Params> = {
  158. Params,
  159. createEmpty,
  160. createValues,
  161. createValuesSimple,
  162. updateValues,
  163. updateBoundingSphere,
  164. createRenderableState,
  165. updateRenderableState,
  166. createPositionIterator
  167. };
  168. function createPositionIterator(text: Text, transform: TransformData): LocationIterator {
  169. const groupCount = text.charCount * 4;
  170. const instanceCount = transform.instanceCount.ref.value;
  171. const location = PositionLocation();
  172. const p = location.position;
  173. const v = text.centerBuffer.ref.value;
  174. const m = transform.aTransform.ref.value;
  175. const getLocation = (groupIndex: number, instanceIndex: number) => {
  176. if (instanceIndex < 0) {
  177. Vec3.fromArray(p, v, groupIndex * 3);
  178. } else {
  179. Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
  180. }
  181. return location;
  182. };
  183. return LocationIterator(groupCount, instanceCount, 4, getLocation);
  184. }
  185. function createValues(text: Text, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextValues {
  186. const { instanceCount, groupCount } = locationIt;
  187. const positionIt = createPositionIterator(text, transform);
  188. const color = createColors(locationIt, positionIt, theme.color);
  189. const size = createSizes(locationIt, theme.size);
  190. const marker = createMarkers(instanceCount * groupCount);
  191. const overpaint = createEmptyOverpaint();
  192. const transparency = createEmptyTransparency();
  193. const clipping = createEmptyClipping();
  194. const counts = { drawCount: text.charCount * 2 * 3, vertexCount: text.charCount * 4, groupCount, instanceCount };
  195. const padding = getPadding(text.mappingBuffer.ref.value, text.depthBuffer.ref.value, text.charCount, getMaxSize(size));
  196. const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), text.boundingSphere, padding);
  197. const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
  198. return {
  199. aPosition: text.centerBuffer,
  200. aMapping: text.mappingBuffer,
  201. aDepth: text.depthBuffer,
  202. aGroup: text.groupBuffer,
  203. elements: text.indexBuffer,
  204. boundingSphere: ValueCell.create(boundingSphere),
  205. invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
  206. uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
  207. ...color,
  208. ...size,
  209. ...marker,
  210. ...overpaint,
  211. ...transparency,
  212. ...clipping,
  213. ...transform,
  214. aTexCoord: text.tcoordBuffer,
  215. tFont: text.fontTexture,
  216. padding: ValueCell.create(padding),
  217. ...BaseGeometry.createValues(props, counts),
  218. uSizeFactor: ValueCell.create(props.sizeFactor),
  219. uBorderWidth: ValueCell.create(clamp(props.borderWidth, 0, 0.5)),
  220. uBorderColor: ValueCell.create(Color.toArrayNormalized(props.borderColor, Vec3.zero(), 0)),
  221. uOffsetX: ValueCell.create(props.offsetX),
  222. uOffsetY: ValueCell.create(props.offsetY),
  223. uOffsetZ: ValueCell.create(props.offsetZ),
  224. uBackgroundColor: ValueCell.create(Color.toArrayNormalized(props.backgroundColor, Vec3.zero(), 0)),
  225. uBackgroundOpacity: ValueCell.create(props.backgroundOpacity),
  226. };
  227. }
  228. function createValuesSimple(text: Text, props: Partial<PD.Values<Params>>, colorValue: Color, sizeValue: number, transform?: TransformData) {
  229. const s = BaseGeometry.createSimple(colorValue, sizeValue, transform);
  230. const p = { ...PD.getDefaultValues(Params), ...props };
  231. return createValues(text, s.transform, s.locationIterator, s.theme, p);
  232. }
  233. function updateValues(values: TextValues, props: PD.Values<Params>) {
  234. BaseGeometry.updateValues(values, props);
  235. ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
  236. ValueCell.updateIfChanged(values.uBorderWidth, props.borderWidth);
  237. if (Color.fromNormalizedArray(values.uBorderColor.ref.value, 0) !== props.borderColor) {
  238. Color.toArrayNormalized(props.borderColor, values.uBorderColor.ref.value, 0);
  239. ValueCell.update(values.uBorderColor, values.uBorderColor.ref.value);
  240. }
  241. ValueCell.updateIfChanged(values.uOffsetX, props.offsetX);
  242. ValueCell.updateIfChanged(values.uOffsetY, props.offsetY);
  243. ValueCell.updateIfChanged(values.uOffsetZ, props.offsetZ);
  244. if (Color.fromNormalizedArray(values.uBackgroundColor.ref.value, 0) !== props.backgroundColor) {
  245. Color.toArrayNormalized(props.backgroundColor, values.uBackgroundColor.ref.value, 0);
  246. ValueCell.update(values.uBackgroundColor, values.uBackgroundColor.ref.value);
  247. }
  248. ValueCell.updateIfChanged(values.uBackgroundOpacity, props.backgroundOpacity);
  249. }
  250. function updateBoundingSphere(values: TextValues, text: Text) {
  251. const padding = getPadding(values.aMapping.ref.value, values.aDepth.ref.value, text.charCount, getMaxSize(values));
  252. const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), text.boundingSphere, padding);
  253. const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value);
  254. if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
  255. ValueCell.update(values.boundingSphere, boundingSphere);
  256. }
  257. if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
  258. ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
  259. ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
  260. }
  261. ValueCell.update(values.padding, padding);
  262. }
  263. function createRenderableState(props: PD.Values<Params>): RenderableState {
  264. const state = BaseGeometry.createRenderableState(props);
  265. updateRenderableState(state, props);
  266. return state;
  267. }
  268. function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
  269. BaseGeometry.updateRenderableState(state, props);
  270. state.pickable = false;
  271. state.opaque = false;
  272. state.writeDepth = true;
  273. }
  274. }
  275. function getPadding(mappings: Float32Array, depths: Float32Array, charCount: number, maxSize: number) {
  276. let maxOffset = 0;
  277. let maxDepth = 0;
  278. for (let i = 0, il = charCount * 4; i < il; ++i) {
  279. const i2 = 2 * i;
  280. const ox = Math.abs(mappings[i2]);
  281. if (ox > maxOffset) maxOffset = ox;
  282. const oy = Math.abs(mappings[i2 + 1]);
  283. if (oy > maxOffset) maxOffset = oy;
  284. const d = Math.abs(depths[i]);
  285. if (d > maxDepth) maxDepth = d;
  286. }
  287. // console.log(maxDepth + maxSize, maxDepth, maxSize, maxSize + maxSize * maxOffset, depths)
  288. return Math.max(maxDepth, maxSize + maxSize * maxOffset);
  289. // return maxSize + maxSize * maxOffset + maxDepth
  290. }