babylon.text2d.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. module BABYLON {
  2. export class Text2DRenderCache extends ModelRenderCache {
  3. effectsReady: boolean = false;
  4. vb: WebGLBuffer = null;
  5. ib: WebGLBuffer = null;
  6. instancingAttributes: InstancingAttributeInfo[] = null;
  7. fontTexture: FontTexture = null;
  8. effect: Effect = null;
  9. effectInstanced: Effect = null;
  10. render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
  11. // Do nothing if the shader is still loading/preparing
  12. if (!this.effectsReady) {
  13. if ((!this.effect.isReady() || (this.effectInstanced && !this.effectInstanced.isReady()))) {
  14. return false;
  15. }
  16. this.effectsReady = true;
  17. }
  18. var engine = instanceInfo.owner.owner.engine;
  19. this.fontTexture.update();
  20. let effect = context.useInstancing ? this.effectInstanced : this.effect;
  21. engine.enableEffect(effect);
  22. effect.setTexture("diffuseSampler", this.fontTexture);
  23. engine.bindBuffers(this.vb, this.ib, [1], 4, effect);
  24. var curAlphaMode = engine.getAlphaMode();
  25. engine.setAlphaMode(Engine.ALPHA_COMBINE, true);
  26. let pid = context.groupInfoPartData[0];
  27. if (context.useInstancing) {
  28. if (!this.instancingAttributes) {
  29. this.instancingAttributes = this.loadInstancingAttributes(Text2D.TEXT2D_MAINPARTID, effect);
  30. }
  31. engine.updateAndBindInstancesBuffer(pid._partBuffer, null, this.instancingAttributes);
  32. engine.draw(true, 0, 6, pid._partData.usedElementCount);
  33. engine.unBindInstancesBuffer(pid._partBuffer, this.instancingAttributes);
  34. } else {
  35. for (let i = context.partDataStartIndex; i < context.partDataEndIndex; i++) {
  36. this.setupUniforms(effect, 0, pid._partData, i);
  37. engine.draw(true, 0, 6);
  38. }
  39. }
  40. engine.setAlphaMode(curAlphaMode);
  41. return true;
  42. }
  43. public dispose(): boolean {
  44. if (!super.dispose()) {
  45. return false;
  46. }
  47. if (this.vb) {
  48. this._engine._releaseBuffer(this.vb);
  49. this.vb = null;
  50. }
  51. if (this.ib) {
  52. this._engine._releaseBuffer(this.ib);
  53. this.ib = null;
  54. }
  55. if (this.fontTexture) {
  56. this.fontTexture.dispose();
  57. this.fontTexture = null;
  58. }
  59. if (this.effect) {
  60. this._engine._releaseEffect(this.effect);
  61. this.effect = null;
  62. }
  63. if (this.effectInstanced) {
  64. this._engine._releaseEffect(this.effectInstanced);
  65. this.effectInstanced = null;
  66. }
  67. return true;
  68. }
  69. }
  70. export class Text2DInstanceData extends InstanceDataBase {
  71. constructor(partId: number, dataElementCount: number) {
  72. super(partId, dataElementCount);
  73. }
  74. @instanceData()
  75. get topLeftUV(): Vector2 {
  76. return null;
  77. }
  78. @instanceData()
  79. get sizeUV(): Vector2 {
  80. return null;
  81. }
  82. @instanceData()
  83. get textureSize(): Vector2 {
  84. return null;
  85. }
  86. @instanceData()
  87. get color(): Color4 {
  88. return null;
  89. }
  90. }
  91. @className("Text2D")
  92. export class Text2D extends RenderablePrim2D {
  93. static TEXT2D_MAINPARTID = 1;
  94. public static fontProperty: Prim2DPropInfo;
  95. public static defaultFontColorProperty: Prim2DPropInfo;
  96. public static textProperty: Prim2DPropInfo;
  97. public static areaSizeProperty: Prim2DPropInfo;
  98. @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, pi => Text2D.fontProperty = pi, false, true)
  99. public get fontName(): string {
  100. return this._fontName;
  101. }
  102. public set fontName(value: string) {
  103. if (this._fontName) {
  104. throw new Error("Font Name change is not supported right now.");
  105. }
  106. this._fontName = value;
  107. }
  108. @dynamicLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Text2D.defaultFontColorProperty = pi)
  109. public get defaultFontColor(): Color4 {
  110. return this._defaultFontColor;
  111. }
  112. public set defaultFontColor(value: Color4) {
  113. this._defaultFontColor = value;
  114. }
  115. @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 3, pi => Text2D.textProperty = pi, false, true)
  116. public get text(): string {
  117. return this._text;
  118. }
  119. public set text(value: string) {
  120. this._text = value;
  121. this._actualSize = null; // A change of text will reset the Actual Area Size which will be recomputed next time it's used
  122. this._updateCharCount();
  123. }
  124. @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 4, pi => Text2D.areaSizeProperty = pi)
  125. public get areaSize(): Size {
  126. return this._areaSize;
  127. }
  128. public set areaSize(value: Size) {
  129. this._areaSize = value;
  130. }
  131. public get actualSize(): Size {
  132. if (this.areaSize) {
  133. return this.areaSize;
  134. }
  135. if (this._actualSize) {
  136. return this._actualSize;
  137. }
  138. this._actualSize = this.fontTexture.measureText(this._text, this._tabulationSize);
  139. return this._actualSize;
  140. }
  141. protected get fontTexture(): FontTexture {
  142. if (this._fontTexture) {
  143. return this._fontTexture;
  144. }
  145. this._fontTexture = FontTexture.GetCachedFontTexture(this.owner.scene, this.fontName);
  146. return this._fontTexture;
  147. }
  148. public dispose(): boolean {
  149. if (!super.dispose()) {
  150. return false;
  151. }
  152. if (this._fontTexture) {
  153. FontTexture.ReleaseCachedFontTexture(this.owner.scene, this.fontName);
  154. this._fontTexture = null;
  155. }
  156. return true;
  157. }
  158. protected updateLevelBoundingInfo() {
  159. BoundingInfo2D.CreateFromSizeToRef(this.actualSize, this._levelBoundingInfo, this.origin);
  160. }
  161. protected setupText2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, origin: Vector2, fontName: string, text: string, areaSize: Size, defaultFontColor: Color4, tabulationSize: number, isVisible: boolean, marginTop: number, marginLeft: number, marginRight: number, marginBottom: number, vAlignment: number, hAlignment: number) {
  162. this.setupRenderablePrim2D(owner, parent, id, position, origin, isVisible, marginTop, marginLeft, marginRight, marginBottom, hAlignment, vAlignment);
  163. this.fontName = fontName;
  164. this.defaultFontColor = defaultFontColor;
  165. this.text = text;
  166. this.areaSize = areaSize;
  167. this._tabulationSize = tabulationSize;
  168. this.isAlphaTest = true;
  169. }
  170. /**
  171. * Create a Text primitive
  172. * @param parent the parent primitive, must be a valid primitive (or the Canvas)
  173. * @param text the text to display
  174. * Options:
  175. * - id a text identifier, for information purpose
  176. * - position: the X & Y positions relative to its parent. Alternatively the x and y properties can be set. Default is [0;0]
  177. * - origin: define the normalized origin point location, default [0.5;0.5]
  178. * - fontName: the name/size/style of the font to use, following the CSS notation. Default is "12pt Arial".
  179. * - defaultColor: the color by default to apply on each letter of the text to display, default is plain white.
  180. * - areaSize: the size of the area in which to display the text, default is auto-fit from text content.
  181. * - tabulationSize: number of space character to insert when a tabulation is encountered, default is 4
  182. * - isVisible: true if the text must be visible, false for hidden. Default is true.
  183. * - marginTop/Left/Right/Bottom: define the margin for the corresponding edge, if all of them are null, margin is not used in layout computing. Default Value is null for each.
  184. * - hAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
  185. * - vAlighment: define horizontal alignment of the Canvas, alignment is optional, default value null: no alignment.
  186. */
  187. public static Create(parent: Prim2DBase, text: string, options?: { id?: string, position?: Vector2, x?: number, y?: number, origin?: Vector2, fontName?: string, defaultFontColor?: Color4, areaSize?: Size, tabulationSize?: number, isVisible?: boolean, marginTop?: number, marginLeft?: number, marginRight?: number, marginBottom?: number, hAlignment?: number, vAlignment?: number}): Text2D {
  188. Prim2DBase.CheckParent(parent);
  189. let text2d = new Text2D();
  190. if (!options) {
  191. text2d.setupText2D(parent.owner, parent, null, Vector2.Zero(), null, "12pt Arial", text, null, new Color4(1,1,1,1), 4, true, null, null, null, null, null, null);
  192. } else {
  193. let pos = options.position || new Vector2(options.x || 0, options.y || 0);
  194. text2d.setupText2D(parent.owner, parent, options.id || null, pos, options.origin || null, options.fontName || "12pt Arial", text, options.areaSize, options.defaultFontColor || new Color4(1, 1, 1, 1), options.tabulationSize || 4, options.isVisible || true, options.marginTop || null, options.marginLeft || null, options.marginRight || null, options.marginBottom || null, options.vAlignment || null, options.hAlignment || null);
  195. }
  196. return text2d;
  197. }
  198. protected levelIntersect(intersectInfo: IntersectInfo2D): boolean {
  199. // For now I can't do something better that boundingInfo is a hit, detecting an intersection on a particular letter would be possible, but do we really need it? Not for now...
  200. return true;
  201. }
  202. protected createModelRenderCache(modelKey: string): ModelRenderCache {
  203. let renderCache = new Text2DRenderCache(this.owner.engine, modelKey);
  204. return renderCache;
  205. }
  206. protected setupModelRenderCache(modelRenderCache: ModelRenderCache) {
  207. let renderCache = <Text2DRenderCache>modelRenderCache;
  208. let engine = this.owner.engine;
  209. renderCache.fontTexture = this.fontTexture;
  210. let vb = new Float32Array(4);
  211. for (let i = 0; i < 4; i++) {
  212. vb[i] = i;
  213. }
  214. renderCache.vb = engine.createVertexBuffer(vb);
  215. let ib = new Float32Array(6);
  216. ib[0] = 0;
  217. ib[1] = 2;
  218. ib[2] = 1;
  219. ib[3] = 0;
  220. ib[4] = 3;
  221. ib[5] = 2;
  222. renderCache.ib = engine.createIndexBuffer(ib);
  223. // Get the instanced version of the effect, if the engine does not support it, null is return and we'll only draw on by one
  224. let ei = this.getDataPartEffectInfo(Text2D.TEXT2D_MAINPARTID, ["index"], true);
  225. if (ei) {
  226. renderCache.effectInstanced = engine.createEffect("text2d", ei.attributes, ei.uniforms, ["diffuseSampler"], ei.defines, null);
  227. }
  228. ei = this.getDataPartEffectInfo(Text2D.TEXT2D_MAINPARTID, ["index"], false);
  229. renderCache.effect = engine.createEffect("text2d", ei.attributes, ei.uniforms, ["diffuseSampler"], ei.defines, null);
  230. return renderCache;
  231. }
  232. protected createInstanceDataParts(): InstanceDataBase[] {
  233. return [new Text2DInstanceData(Text2D.TEXT2D_MAINPARTID, this._charCount)];
  234. }
  235. // Looks like a hack!? Yes! Because that's what it is!
  236. // For the InstanceData layer to compute correctly we need to set all the properties involved, which won't be the case if there's no text
  237. // This method is called before the layout construction for us to detect this case, set some text and return the initial one to restore it after (there can be some text without char to display, say "\t\n" for instance)
  238. protected beforeRefreshForLayoutConstruction(part: InstanceDataBase): any {
  239. if (!this._charCount) {
  240. let curText = this._text;
  241. this.text = "A";
  242. return curText;
  243. }
  244. }
  245. // if obj contains something, we restore the _text property
  246. protected afterRefreshForLayoutConstruction(part: InstanceDataBase, obj: any) {
  247. if (obj !== undefined) {
  248. this.text = obj;
  249. }
  250. }
  251. protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
  252. if (!super.refreshInstanceDataPart(part)) {
  253. return false;
  254. }
  255. if (part.id === Text2D.TEXT2D_MAINPARTID) {
  256. let d = <Text2DInstanceData>part;
  257. let texture = this.fontTexture;
  258. let ts = texture.getSize();
  259. let textSize = texture.measureText(this.text, this._tabulationSize);
  260. let offset = Vector2.Zero();
  261. let charxpos = 0;
  262. d.dataElementCount = this._charCount;
  263. d.curElement = 0;
  264. for (let char of this.text) {
  265. // Line feed
  266. if (char === "\n") {
  267. offset.x = 0;
  268. offset.y -= texture.lineHeight;
  269. }
  270. // Tabulation ?
  271. if (char === "\t") {
  272. let nextPos = charxpos + this._tabulationSize;
  273. nextPos = nextPos - (nextPos % this._tabulationSize);
  274. offset.x += (nextPos - charxpos) * texture.spaceWidth;
  275. charxpos = nextPos;
  276. continue;
  277. }
  278. if (char < " ") {
  279. continue;
  280. }
  281. this.updateInstanceDataPart(d, offset, textSize);
  282. let ci = texture.getChar(char);
  283. offset.x += ci.charWidth;
  284. d.topLeftUV = ci.topLeftUV;
  285. let suv = ci.bottomRightUV.subtract(ci.topLeftUV);
  286. d.sizeUV = suv;
  287. d.textureSize = new Vector2(ts.width, ts.height);
  288. d.color = this.defaultFontColor;
  289. ++d.curElement;
  290. }
  291. }
  292. return true;
  293. }
  294. private _updateCharCount() {
  295. let count = 0;
  296. for (let char of this._text) {
  297. if (char === "\r" || char === "\n" || char === "\t" || char < " ") {
  298. continue;
  299. }
  300. ++count;
  301. }
  302. this._charCount = count;
  303. }
  304. private _fontTexture: FontTexture;
  305. private _tabulationSize: number;
  306. private _charCount: number;
  307. private _fontName: string;
  308. private _defaultFontColor: Color4;
  309. private _text: string;
  310. private _areaSize: Size;
  311. private _actualSize: Size;
  312. private _vAlign: number;
  313. private _hAlign: number;
  314. }
  315. }