@lightningjs/renderer 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +56 -196
  2. package/dist/src/core/CoreNode.js +25 -4
  3. package/dist/src/core/CoreNode.js.map +1 -1
  4. package/dist/src/core/CoreTextNode.d.ts +9 -2
  5. package/dist/src/core/CoreTextNode.js +32 -11
  6. package/dist/src/core/CoreTextNode.js.map +1 -1
  7. package/dist/src/core/CoreTextureManager.d.ts +8 -0
  8. package/dist/src/core/CoreTextureManager.js +13 -1
  9. package/dist/src/core/CoreTextureManager.js.map +1 -1
  10. package/dist/src/core/Stage.d.ts +8 -0
  11. package/dist/src/core/Stage.js +23 -0
  12. package/dist/src/core/Stage.js.map +1 -1
  13. package/dist/src/core/TextureMemoryManager.d.ts +8 -13
  14. package/dist/src/core/TextureMemoryManager.js +22 -27
  15. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  16. package/dist/src/core/lib/ImageWorker.d.ts +2 -2
  17. package/dist/src/core/lib/ImageWorker.js +31 -12
  18. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  19. package/dist/src/core/lib/WebGlContextWrapper.d.ts +105 -56
  20. package/dist/src/core/lib/WebGlContextWrapper.js +164 -158
  21. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  22. package/dist/src/core/lib/textureCompression.js +19 -10
  23. package/dist/src/core/lib/textureCompression.js.map +1 -1
  24. package/dist/src/core/lib/validateImageBitmap.d.ts +2 -1
  25. package/dist/src/core/lib/validateImageBitmap.js +4 -4
  26. package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
  27. package/dist/src/core/platform.js +2 -2
  28. package/dist/src/core/platform.js.map +1 -1
  29. package/dist/src/core/platforms/Platform.d.ts +4 -0
  30. package/dist/src/core/platforms/Platform.js.map +1 -1
  31. package/dist/src/core/platforms/web/WebPlatform.d.ts +2 -0
  32. package/dist/src/core/platforms/web/WebPlatform.js +13 -0
  33. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  34. package/dist/src/core/renderers/CoreRenderer.d.ts +6 -0
  35. package/dist/src/core/renderers/CoreRenderer.js +8 -0
  36. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  37. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +1 -0
  38. package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -0
  39. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  40. package/dist/src/core/renderers/webgl/WebGlRenderOp.d.ts +45 -0
  41. package/dist/src/core/renderers/webgl/WebGlRenderOp.js +127 -0
  42. package/dist/src/core/renderers/webgl/WebGlRenderOp.js.map +1 -0
  43. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +2 -0
  44. package/dist/src/core/renderers/webgl/WebGlRenderer.js +30 -22
  45. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  46. package/dist/src/core/text-rendering/CanvasFont.d.ts +14 -0
  47. package/dist/src/core/text-rendering/CanvasFont.js +120 -0
  48. package/dist/src/core/text-rendering/CanvasFont.js.map +1 -0
  49. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +1 -2
  50. package/dist/src/core/text-rendering/CanvasTextRenderer.js +11 -19
  51. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  52. package/dist/src/core/text-rendering/CoreFont.d.ts +33 -0
  53. package/dist/src/core/text-rendering/CoreFont.js +48 -0
  54. package/dist/src/core/text-rendering/CoreFont.js.map +1 -0
  55. package/dist/src/core/text-rendering/FontManager.d.ts +11 -0
  56. package/dist/src/core/text-rendering/FontManager.js +41 -0
  57. package/dist/src/core/text-rendering/FontManager.js.map +1 -0
  58. package/dist/src/core/text-rendering/SdfFont.d.ts +29 -0
  59. package/dist/src/core/text-rendering/SdfFont.js +142 -0
  60. package/dist/src/core/text-rendering/SdfFont.js.map +1 -0
  61. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +2 -2
  62. package/dist/src/core/text-rendering/SdfTextRenderer.js +141 -132
  63. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  64. package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
  65. package/dist/src/core/text-rendering/TextGenerator.js +36 -0
  66. package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
  67. package/dist/src/core/text-rendering/TextRenderer.d.ts +26 -20
  68. package/dist/src/core/text-rendering/Utils.d.ts +2 -0
  69. package/dist/src/core/text-rendering/Utils.js +3 -0
  70. package/dist/src/core/text-rendering/Utils.js.map +1 -1
  71. package/dist/src/main-api/Renderer.d.ts +14 -0
  72. package/dist/src/main-api/Renderer.js +29 -3
  73. package/dist/src/main-api/Renderer.js.map +1 -1
  74. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  75. package/package.json +2 -1
  76. package/src/core/CoreNode.ts +29 -4
  77. package/src/core/CoreTextNode.test.ts +237 -0
  78. package/src/core/CoreTextNode.ts +53 -33
  79. package/src/core/CoreTextureManager.ts +14 -2
  80. package/src/core/Stage.ts +29 -0
  81. package/src/core/TextureMemoryManager.test.ts +134 -0
  82. package/src/core/TextureMemoryManager.ts +23 -30
  83. package/src/core/platforms/Platform.ts +5 -0
  84. package/src/core/platforms/web/WebPlatform.ts +13 -0
  85. package/src/core/renderers/CoreRenderer.ts +10 -0
  86. package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
  87. package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
  88. package/src/core/renderers/webgl/WebGlRenderer.ts +38 -28
  89. package/src/core/text-rendering/CanvasTextRenderer.ts +13 -41
  90. package/src/core/text-rendering/SdfTextRenderer.ts +166 -163
  91. package/src/core/text-rendering/TextRenderer.ts +23 -21
  92. package/src/core/text-rendering/Utils.ts +5 -1
  93. package/src/main-api/Renderer.test.ts +153 -0
  94. package/src/main-api/Renderer.ts +33 -3
  95. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
  96. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
  97. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
@@ -474,28 +474,27 @@ export class WebGlRenderer extends CoreRenderer {
474
474
  private insertRTTNodeInOrder(node: CoreNode) {
475
475
  let insertIndex = this.rttNodes.length; // Default to the end of the array
476
476
 
477
+ // Build a one-shot index map so all lookups below are O(1) instead of O(n).
478
+ const rttNodes = this.rttNodes;
479
+ const indexMap = new Map<number, number>();
480
+ for (let i = 0; i < rttNodes.length; i++) {
481
+ indexMap.set(rttNodes[i]!.id, i);
482
+ }
483
+
477
484
  // 1. Traverse upwards to ensure the node is placed before its RTT parent (if any).
478
485
  let currentNode: CoreNode = node;
479
- while (currentNode) {
480
- if (!currentNode.parent) {
481
- break;
482
- }
483
-
484
- const parentIndex = this.rttNodes.indexOf(currentNode.parent);
485
- if (parentIndex !== -1) {
486
- // Found an RTT parent in the list; set insertIndex to place node before the parent
486
+ while (currentNode.parent !== null) {
487
+ const parentIndex = indexMap.get(currentNode.parent.id);
488
+ if (parentIndex !== undefined) {
487
489
  insertIndex = parentIndex;
488
490
  break;
489
491
  }
490
-
491
492
  currentNode = currentNode.parent;
492
493
  }
493
494
 
494
495
  // 2. Traverse downwards to ensure the node is placed after any RTT children.
495
- // Look through each child recursively to see if any are already in rttNodes.
496
- const maxChildIndex = this.findMaxChildRTTIndex(node);
496
+ const maxChildIndex = this.findMaxChildRTTIndex(node, indexMap);
497
497
  if (maxChildIndex !== -1) {
498
- // Adjust insertIndex to be after the last child RTT node
499
498
  insertIndex = Math.max(insertIndex, maxChildIndex + 1);
500
499
  }
501
500
 
@@ -503,25 +502,25 @@ export class WebGlRenderer extends CoreRenderer {
503
502
  this.rttNodes.splice(insertIndex, 0, node);
504
503
  }
505
504
 
506
- // Helper function to find the highest index of any RTT children of a node within rttNodes
507
- private findMaxChildRTTIndex(node: CoreNode): number {
505
+ // Iterative DFS to find the highest rttNodes index among all RTT descendants of node.
506
+ private findMaxChildRTTIndex(
507
+ node: CoreNode,
508
+ indexMap: Map<number, number>,
509
+ ): number {
508
510
  let maxIndex = -1;
509
-
510
- const traverseChildren = (currentNode: CoreNode) => {
511
- const currentIndex = this.rttNodes.indexOf(currentNode);
512
- if (currentIndex !== -1) {
513
- maxIndex = Math.max(maxIndex, currentIndex);
511
+ // Explicit stack avoids recursive arrow function allocation and call-stack growth.
512
+ const stack: CoreNode[] = [node];
513
+ while (stack.length !== 0) {
514
+ const current = stack.pop()!;
515
+ const idx = indexMap.get(current.id);
516
+ if (idx !== undefined && idx > maxIndex) {
517
+ maxIndex = idx;
514
518
  }
515
-
516
- // Recursively check all children of the current node
517
- for (const child of currentNode.children) {
518
- traverseChildren(child);
519
+ const children = current.children;
520
+ for (let i = 0; i < children.length; i++) {
521
+ stack.push(children[i]!);
519
522
  }
520
- };
521
-
522
- // Start traversal directly with the provided node
523
- traverseChildren(node);
524
-
523
+ }
525
524
  return maxIndex;
526
525
  }
527
526
 
@@ -724,4 +723,15 @@ export class WebGlRenderer extends CoreRenderer {
724
723
  normalized: normalizedColor,
725
724
  };
726
725
  }
726
+
727
+ override destroy(): void {
728
+ const loseCtx = this.glw.getExtension(
729
+ 'WEBGL_lose_context',
730
+ ) as WEBGL_lose_context | null;
731
+ loseCtx?.loseContext();
732
+ }
733
+
734
+ override deleteBuffer(buffer: WebGLBuffer): void {
735
+ this.glw.deleteBuffer(buffer);
736
+ }
727
737
  }
@@ -22,7 +22,7 @@ import type { Stage } from '../Stage.js';
22
22
  import type { TextLineStruct, TextRenderInfo } from './TextRenderer.js';
23
23
  import * as CanvasFontHandler from './CanvasFontHandler.js';
24
24
  import type { CoreTextNodeProps } from '../CoreTextNode.js';
25
- import { hasZeroWidthSpace } from './Utils.js';
25
+ import { getLayoutCacheKey, hasZeroWidthSpace } from './Utils.js';
26
26
  import { mapTextLayout } from './TextLayoutEngine.js';
27
27
 
28
28
  const MAX_TEXTURE_DIMENSION = 4096;
@@ -43,16 +43,7 @@ let measureContext:
43
43
  | null = null;
44
44
 
45
45
  // Cache for text layout calculations
46
- const layoutCache = new Map<
47
- string,
48
- {
49
- lines: string[];
50
- lineWidths: number[];
51
- maxLineWidth: number;
52
- remainingText: string;
53
- moreTextLines: boolean;
54
- }
55
- >();
46
+ const layoutCache = new Map<string, TextRenderInfo>();
56
47
 
57
48
  // Initialize the Text Renderer
58
49
  const init = (stage: Stage): void => {
@@ -96,6 +87,12 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
96
87
  assertTruthy(canvas, 'Canvas is not initialized');
97
88
  assertTruthy(context, 'Canvas context is not available');
98
89
  assertTruthy(measureContext, 'Canvas measureContext is not available');
90
+ const cacheKey = getLayoutCacheKey(props);
91
+
92
+ let layout = layoutCache.get(cacheKey);
93
+ if (layout !== undefined) {
94
+ return layout;
95
+ }
99
96
  // Extract already normalized properties
100
97
  const {
101
98
  text,
@@ -189,48 +186,24 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
189
186
  if (canvas.width > 0 && canvas.height > 0) {
190
187
  imageData = context.getImageData(0, 0, canvasW, canvasH);
191
188
  }
192
- return {
189
+ const renderInfo = {
193
190
  imageData,
194
191
  width: effectiveWidth,
195
192
  height: effectiveHeight,
196
193
  remainingLines,
197
194
  hasRemainingText,
198
195
  };
196
+ layoutCache.set(cacheKey, renderInfo);
197
+ return renderInfo;
199
198
  };
200
199
 
201
- /**
202
- * Generate a cache key for text layout calculations
203
- */
204
- function generateLayoutCacheKey(
205
- text: string,
206
- fontFamily: string,
207
- fontSize: number,
208
- fontStyle: string,
209
- wordWrap: boolean,
210
- wordWrapWidth: number,
211
- letterSpacing: number,
212
- maxLines: number,
213
- overflowSuffix: string,
214
- ): string {
215
- return `${text}-${fontFamily}-${fontSize}-${fontStyle}-${wordWrap}-${wordWrapWidth}-${letterSpacing}-${maxLines}-${overflowSuffix}`;
216
- }
217
-
218
200
  /**
219
201
  * Clear layout cache for memory management
220
202
  */
221
- const clearLayoutCache = (): void => {
203
+ const clearCache = (): void => {
222
204
  layoutCache.clear();
223
205
  };
224
206
 
225
- /**
226
- * Add quads for rendering (Canvas doesn't use quads)
227
- */
228
- const addQuads = (): Float32Array | null => {
229
- // Canvas renderer doesn't use quad-based rendering
230
- // Return null for interface compatibility
231
- return null;
232
- };
233
-
234
207
  /**
235
208
  * Render quads for Canvas renderer (Canvas doesn't use quad-based rendering)
236
209
  */
@@ -246,10 +219,9 @@ const CanvasTextRenderer = {
246
219
  type,
247
220
  font: CanvasFontHandler,
248
221
  renderText,
249
- addQuads,
250
222
  renderQuads,
251
223
  init,
252
- clearLayoutCache,
224
+ clearCache,
253
225
  };
254
226
 
255
227
  export default CanvasTextRenderer;
@@ -25,7 +25,7 @@ import type {
25
25
  TextRenderProps,
26
26
  } from './TextRenderer.js';
27
27
  import type { CoreTextNodeProps } from '../CoreTextNode.js';
28
- import { hasZeroWidthSpace } from './Utils.js';
28
+ import { getLayoutCacheKey, hasZeroWidthSpace } from './Utils.js';
29
29
  import * as SdfFontHandler from './SdfFontHandler.js';
30
30
  import type { CoreRenderer } from '../renderers/CoreRenderer.js';
31
31
  import { WebGlRenderer } from '../renderers/webgl/WebGlRenderer.js';
@@ -35,7 +35,7 @@ import { BufferCollection } from '../renderers/webgl/internal/BufferCollection.j
35
35
  import type { WebGlCtxTexture } from '../renderers/webgl/WebGlCtxTexture.js';
36
36
  import type { WebGlShaderNode } from '../renderers/webgl/WebGlShaderNode.js';
37
37
  import { mergeColorAlpha } from '../../utils.js';
38
- import type { TextLayout, GlyphLayout } from './TextRenderer.js';
38
+ import type { TextLayout } from './TextRenderer.js';
39
39
  import { mapTextLayout } from './TextLayoutEngine.js';
40
40
 
41
41
  // Each glyph requires 6 vertices (2 triangles) with 4 floats each (x, y, u, v)
@@ -57,6 +57,7 @@ const init = (stage: Stage): void => {
57
57
  };
58
58
 
59
59
  const font: FontHandler = SdfFontHandler;
60
+ const layoutCache = new Map<string, TextLayout>();
60
61
 
61
62
  /**
62
63
  * SDF text renderer using MSDF/SDF fonts with WebGL
@@ -74,6 +75,19 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
74
75
  };
75
76
  }
76
77
 
78
+ const cacheKey = getLayoutCacheKey(props);
79
+
80
+ let layout = layoutCache.get(cacheKey);
81
+ if (layout !== undefined) {
82
+ return {
83
+ width: layout.width,
84
+ height: layout.height,
85
+ remainingLines: layout.remainingLines,
86
+ hasRemainingText: layout.hasRemainingText,
87
+ layout, // Cache layout for addQuads
88
+ };
89
+ }
90
+
77
91
  // Get font cache for this font family
78
92
  const fontData = SdfFontHandler.getFontData(props.fontFamily);
79
93
  if (fontData === undefined) {
@@ -85,99 +99,19 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
85
99
  }
86
100
 
87
101
  // Calculate text layout and generate glyph data for caching
88
- const layout = generateTextLayout(props, fontData);
102
+ layout = generateTextLayout(props, fontData);
103
+ layoutCache.set(cacheKey, layout);
89
104
 
90
105
  // For SDF renderer, ImageData is null since we render via WebGL
91
106
  return {
92
- remainingLines: 0,
93
- hasRemainingText: false,
94
107
  width: layout.width,
95
108
  height: layout.height,
109
+ remainingLines: layout.remainingLines,
110
+ hasRemainingText: layout.hasRemainingText,
96
111
  layout, // Cache layout for addQuads
97
112
  };
98
113
  };
99
114
 
100
- /**
101
- * Add quads for rendering using cached layout data
102
- */
103
- const addQuads = (layout?: TextLayout): Float32Array | null => {
104
- if (layout === undefined) {
105
- return null; // No layout data available
106
- }
107
-
108
- const glyphs = layout.glyphs;
109
- const glyphsLength = glyphs.length;
110
-
111
- if (glyphsLength === 0) {
112
- return null;
113
- }
114
-
115
- const vertexBuffer = new Float32Array(
116
- glyphsLength * VERTICES_PER_GLYPH * FLOATS_PER_VERTEX,
117
- );
118
-
119
- let bufferIndex = 0;
120
- let glyphIndex = 0;
121
-
122
- while (glyphIndex < glyphsLength) {
123
- const glyph = glyphs[glyphIndex];
124
- glyphIndex++;
125
- if (glyph === undefined) {
126
- continue;
127
- }
128
-
129
- const x1 = glyph.x;
130
- const y1 = glyph.y;
131
- const x2 = x1 + glyph.width;
132
- const y2 = y1 + glyph.height;
133
-
134
- const u1 = glyph.atlasX;
135
- const v1 = glyph.atlasY;
136
- const u2 = u1 + glyph.atlasWidth;
137
- const v2 = v1 + glyph.atlasHeight;
138
-
139
- // Triangle 1: Top-left, top-right, bottom-left
140
- // Vertex 1: Top-left
141
- vertexBuffer[bufferIndex++] = x1;
142
- vertexBuffer[bufferIndex++] = y1;
143
- vertexBuffer[bufferIndex++] = u1;
144
- vertexBuffer[bufferIndex++] = v1;
145
-
146
- // Vertex 2: Top-right
147
- vertexBuffer[bufferIndex++] = x2;
148
- vertexBuffer[bufferIndex++] = y1;
149
- vertexBuffer[bufferIndex++] = u2;
150
- vertexBuffer[bufferIndex++] = v1;
151
-
152
- // Vertex 3: Bottom-left
153
- vertexBuffer[bufferIndex++] = x1;
154
- vertexBuffer[bufferIndex++] = y2;
155
- vertexBuffer[bufferIndex++] = u1;
156
- vertexBuffer[bufferIndex++] = v2;
157
-
158
- // Triangle 2: Top-right, bottom-right, bottom-left
159
- // Vertex 4: Top-right (duplicate)
160
- vertexBuffer[bufferIndex++] = x2;
161
- vertexBuffer[bufferIndex++] = y1;
162
- vertexBuffer[bufferIndex++] = u2;
163
- vertexBuffer[bufferIndex++] = v1;
164
-
165
- // Vertex 5: Bottom-right
166
- vertexBuffer[bufferIndex++] = x2;
167
- vertexBuffer[bufferIndex++] = y2;
168
- vertexBuffer[bufferIndex++] = u2;
169
- vertexBuffer[bufferIndex++] = v2;
170
-
171
- // Vertex 6: Bottom-left (duplicate)
172
- vertexBuffer[bufferIndex++] = x1;
173
- vertexBuffer[bufferIndex++] = y2;
174
- vertexBuffer[bufferIndex++] = u1;
175
- vertexBuffer[bufferIndex++] = v2;
176
- }
177
-
178
- return vertexBuffer;
179
- };
180
-
181
115
  /**
182
116
  * Create and submit WebGL render operations for SDF text
183
117
  * This is called from CoreTextNode during rendering to add SDF text to the render pipeline
@@ -185,13 +119,13 @@ const addQuads = (layout?: TextLayout): Float32Array | null => {
185
119
  const renderQuads = (
186
120
  renderer: CoreRenderer,
187
121
  layout: TextLayout,
188
- vertexBuffer: Float32Array,
189
122
  renderProps: TextRenderProps,
190
123
  ): void => {
191
124
  const fontFamily = renderProps.fontFamily;
192
125
  const color = renderProps.color;
193
126
  const worldAlpha = renderProps.worldAlpha;
194
127
  const globalTransform = renderProps.globalTransform;
128
+ const vertexBuffer = layout.vertexBuffer;
195
129
 
196
130
  const atlasTexture = SdfFontHandler.getAtlas(fontFamily);
197
131
  if (atlasTexture === null) {
@@ -202,68 +136,82 @@ const renderQuads = (
202
136
  // We can safely assume this is a WebGL renderer else this wouldn't be called
203
137
  const glw = (renderer as WebGlRenderer).glw;
204
138
  const stride = 4 * Float32Array.BYTES_PER_ELEMENT;
205
- const webGlBuffer = glw.createBuffer();
206
-
207
- if (!webGlBuffer) {
208
- console.warn('Failed to create WebGL buffer for SDF text');
209
- return;
210
- }
211
139
 
212
- const webGlBuffers = new BufferCollection([
213
- {
214
- buffer: webGlBuffer,
215
- attributes: {
216
- a_position: {
217
- name: 'a_position',
218
- size: 2,
219
- type: glw.FLOAT as number,
220
- normalized: false,
221
- stride,
222
- offset: 0,
223
- },
224
- a_textureCoords: {
225
- name: 'a_textureCoords',
226
- size: 2,
227
- type: glw.FLOAT as number,
228
- normalized: false,
229
- stride,
230
- offset: 2 * Float32Array.BYTES_PER_ELEMENT,
140
+ /**
141
+ * Wraps a WebGLBuffer in a BufferCollection with the standard SDF vertex
142
+ * layout, creates and submits an SdfRenderOp. Called by both the cache-miss
143
+ * and cache-hit paths so attribute/op setup only exists in one place.
144
+ */
145
+ const buildAndSubmitRenderOp = (gpuBuffer: WebGLBuffer): void => {
146
+ const webGlBuffers = new BufferCollection([
147
+ {
148
+ buffer: gpuBuffer,
149
+ attributes: {
150
+ a_position: {
151
+ name: 'a_position',
152
+ size: 2,
153
+ type: glw.FLOAT as number,
154
+ normalized: false,
155
+ stride,
156
+ offset: 0,
157
+ },
158
+ a_textureCoords: {
159
+ name: 'a_textureCoords',
160
+ size: 2,
161
+ type: glw.FLOAT as number,
162
+ normalized: false,
163
+ stride,
164
+ offset: 2 * Float32Array.BYTES_PER_ELEMENT,
165
+ },
231
166
  },
232
167
  },
233
- },
234
- ]);
168
+ ]);
169
+
170
+ const renderOp = new SdfRenderOp(
171
+ renderer as WebGlRenderer,
172
+ sdfShader!,
173
+ {
174
+ transform: globalTransform,
175
+ color: mergeColorAlpha(color, worldAlpha),
176
+ size: layout.fontScale,
177
+ distanceRange: layout.distanceRange,
178
+ } satisfies SdfShaderProps,
179
+ webGlBuffers,
180
+ worldAlpha,
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ renderProps.clippingRect as any,
183
+ layout.width,
184
+ layout.height,
185
+ false,
186
+ renderProps.parentHasRenderTexture,
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ renderProps.framebufferDimensions as any,
189
+ );
190
+
191
+ renderOp.addTexture(atlasTexture.ctxTexture as WebGlCtxTexture);
192
+ renderOp.numQuads = layout.glyphCount;
193
+ (renderer as WebGlRenderer).addRenderOp(renderOp);
194
+ };
235
195
 
236
- const buffer = webGlBuffers.getBuffer('a_position');
237
- if (buffer !== undefined) {
238
- glw.arrayBufferData(buffer, vertexBuffer, glw.STATIC_DRAW as number);
239
- }
196
+ // Reuse the cached WebGLBuffer if one was provided — avoids a createBuffer
197
+ // call every frame on nodes whose text has not changed.
198
+ const glBufferRefBox = renderProps.glBufferRef;
240
199
 
241
- const renderOp = new SdfRenderOp(
242
- renderer as WebGlRenderer,
243
- sdfShader!, // Ensure sdfShader is not null
244
- {
245
- transform: globalTransform,
246
- color: mergeColorAlpha(color, worldAlpha),
247
- size: layout.fontScale, // Use proper font scaling in shader
248
- distanceRange: layout.distanceRange,
249
- } satisfies SdfShaderProps,
250
- webGlBuffers,
251
- worldAlpha,
252
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
- renderProps.clippingRect as any,
254
- layout.width,
255
- layout.height,
256
- false,
257
- renderProps.parentHasRenderTexture,
258
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
- renderProps.framebufferDimensions as any,
260
- );
261
-
262
- // Add atlas texture and set quad count
263
- renderOp.addTexture(atlasTexture.ctxTexture as WebGlCtxTexture);
264
- renderOp.numQuads = layout.glyphs.length;
200
+ if (glBufferRefBox.current === null) {
201
+ // Cache miss: allocate a new buffer, upload vertex data, then cache it.
202
+ const newBuffer = glw.createBuffer() as WebGLBuffer | null;
203
+ if (newBuffer === null) {
204
+ console.warn('Failed to create WebGL buffer for SDF text');
205
+ return;
206
+ }
207
+ // Upload vertex data directly into the new buffer.
208
+ glw.arrayBufferData(newBuffer, vertexBuffer, glw.STATIC_DRAW as number);
209
+ // Write back into the ref box so the owning node can reuse it next frame.
210
+ glBufferRefBox.current = newBuffer;
211
+ }
265
212
 
266
- (renderer as WebGlRenderer).addRenderOp(renderOp);
213
+ // Cache hit (or freshly allocated): build the render op and submit.
214
+ buildAndSubmitRenderOp(glBufferRefBox.current);
267
215
  };
268
216
 
269
217
  /**
@@ -315,8 +263,27 @@ const generateTextLayout = (
315
263
  );
316
264
 
317
265
  const lineAmount = lines.length;
266
+ let bufferIndex = 0;
267
+ let glyphCount = 0;
268
+ // Count total glyphs (excluding spaces) for buffer allocation
269
+ for (let i = 0; i < lineAmount; i++) {
270
+ const textLine = (lines[i] as TextLineStruct)[0];
271
+ for (const char of textLine) {
272
+ if (hasZeroWidthSpace(char) === true) {
273
+ continue;
274
+ }
275
+ const codepoint = char.codePointAt(0);
276
+ if (codepoint === undefined) {
277
+ continue;
278
+ }
279
+ glyphCount++;
280
+ }
281
+ }
282
+
283
+ const vertexBuffer = new Float32Array(
284
+ glyphCount * VERTICES_PER_GLYPH * FLOATS_PER_VERTEX,
285
+ );
318
286
 
319
- const glyphs: GlyphLayout[] = [];
320
287
  let currentX = 0;
321
288
  let currentY = 0;
322
289
  for (let i = 0; i < lineAmount; i++) {
@@ -351,23 +318,52 @@ const generateTextLayout = (
351
318
  // Apply pair kerning before placing this glyph.
352
319
  currentX += kerning;
353
320
 
354
- // Calculate glyph position and atlas coordinates (in design units)
355
- const glyphLayout: GlyphLayout = {
356
- codepoint,
357
- glyphId: glyph.id,
358
- x: currentX + glyph.xoffset,
359
- y: currentY + glyph.yoffset,
360
- width: glyph.width,
361
- height: glyph.height,
362
- xOffset: glyph.xoffset,
363
- yOffset: glyph.yoffset,
364
- atlasX: glyph.x / atlasWidth,
365
- atlasY: glyph.y / atlasHeight,
366
- atlasWidth: glyph.width / atlasWidth,
367
- atlasHeight: glyph.height / atlasHeight,
368
- };
369
-
370
- glyphs.push(glyphLayout);
321
+ const x1 = currentX + glyph.xoffset;
322
+ const y1 = currentY + glyph.yoffset;
323
+ const x2 = x1 + glyph.width;
324
+ const y2 = y1 + glyph.height;
325
+ const u1 = glyph.x / atlasWidth;
326
+ const v1 = glyph.y / atlasHeight;
327
+ const u2 = u1 + glyph.width / atlasWidth;
328
+ const v2 = v1 + glyph.height / atlasHeight;
329
+
330
+ // Triangle 1: Top-left, top-right, bottom-left
331
+ // Vertex 1: Top-left
332
+ vertexBuffer[bufferIndex++] = x1;
333
+ vertexBuffer[bufferIndex++] = y1;
334
+ vertexBuffer[bufferIndex++] = u1;
335
+ vertexBuffer[bufferIndex++] = v1;
336
+
337
+ // Vertex 2: Top-right
338
+ vertexBuffer[bufferIndex++] = x2;
339
+ vertexBuffer[bufferIndex++] = y1;
340
+ vertexBuffer[bufferIndex++] = u2;
341
+ vertexBuffer[bufferIndex++] = v1;
342
+
343
+ // Vertex 3: Bottom-left
344
+ vertexBuffer[bufferIndex++] = x1;
345
+ vertexBuffer[bufferIndex++] = y2;
346
+ vertexBuffer[bufferIndex++] = u1;
347
+ vertexBuffer[bufferIndex++] = v2;
348
+
349
+ // Triangle 2: Top-right, bottom-right, bottom-left
350
+ // Vertex 4: Top-right (duplicate)
351
+ vertexBuffer[bufferIndex++] = x2;
352
+ vertexBuffer[bufferIndex++] = y1;
353
+ vertexBuffer[bufferIndex++] = u2;
354
+ vertexBuffer[bufferIndex++] = v1;
355
+
356
+ // Vertex 5: Bottom-right
357
+ vertexBuffer[bufferIndex++] = x2;
358
+ vertexBuffer[bufferIndex++] = y2;
359
+ vertexBuffer[bufferIndex++] = u2;
360
+ vertexBuffer[bufferIndex++] = v2;
361
+
362
+ // Vertex 6: Bottom-left (duplicate)
363
+ vertexBuffer[bufferIndex++] = x1;
364
+ vertexBuffer[bufferIndex++] = y2;
365
+ vertexBuffer[bufferIndex++] = u1;
366
+ vertexBuffer[bufferIndex++] = v2;
371
367
 
372
368
  // Advance position with letter spacing (in design units)
373
369
  currentX += glyph.xadvance + letterSpacing;
@@ -378,16 +374,23 @@ const generateTextLayout = (
378
374
 
379
375
  // Convert final dimensions to pixel space for the layout
380
376
  return {
381
- glyphs,
377
+ vertexBuffer,
378
+ glyphCount,
382
379
  distanceRange: fontScale * fontData.distanceField.distanceRange,
383
380
  width: effectiveWidth * fontScale,
384
381
  height: effectiveHeight,
385
382
  fontScale: fontScale,
386
383
  lineHeight: lineHeightPx,
387
384
  fontFamily,
385
+ remainingLines,
386
+ hasRemainingText,
388
387
  };
389
388
  };
390
389
 
390
+ const clearCache = (): void => {
391
+ layoutCache.clear();
392
+ };
393
+
391
394
  /**
392
395
  * SDF Text Renderer - implements TextRenderer interface
393
396
  */
@@ -395,9 +398,9 @@ const SdfTextRenderer = {
395
398
  type,
396
399
  font,
397
400
  renderText,
398
- addQuads,
399
401
  renderQuads,
400
402
  init,
403
+ clearCache,
401
404
  };
402
405
 
403
406
  export default SdfTextRenderer;