@lightningjs/renderer 3.0.1 → 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 (100) 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/SdfFontHandler.js +7 -5
  62. package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
  63. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +2 -2
  64. package/dist/src/core/text-rendering/SdfTextRenderer.js +141 -132
  65. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  66. package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
  67. package/dist/src/core/text-rendering/TextGenerator.js +36 -0
  68. package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
  69. package/dist/src/core/text-rendering/TextRenderer.d.ts +26 -20
  70. package/dist/src/core/text-rendering/Utils.d.ts +2 -0
  71. package/dist/src/core/text-rendering/Utils.js +3 -0
  72. package/dist/src/core/text-rendering/Utils.js.map +1 -1
  73. package/dist/src/main-api/Renderer.d.ts +14 -0
  74. package/dist/src/main-api/Renderer.js +29 -3
  75. package/dist/src/main-api/Renderer.js.map +1 -1
  76. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  77. package/package.json +2 -1
  78. package/src/core/CoreNode.ts +29 -4
  79. package/src/core/CoreTextNode.test.ts +237 -0
  80. package/src/core/CoreTextNode.ts +53 -33
  81. package/src/core/CoreTextureManager.ts +14 -2
  82. package/src/core/Stage.ts +29 -0
  83. package/src/core/TextureMemoryManager.test.ts +134 -0
  84. package/src/core/TextureMemoryManager.ts +23 -30
  85. package/src/core/platforms/Platform.ts +5 -0
  86. package/src/core/platforms/web/WebPlatform.ts +13 -0
  87. package/src/core/renderers/CoreRenderer.ts +10 -0
  88. package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
  89. package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
  90. package/src/core/renderers/webgl/WebGlRenderer.ts +38 -28
  91. package/src/core/text-rendering/CanvasTextRenderer.ts +13 -41
  92. package/src/core/text-rendering/SdfFontHandler.ts +8 -5
  93. package/src/core/text-rendering/SdfTextRenderer.ts +166 -163
  94. package/src/core/text-rendering/TextRenderer.ts +23 -21
  95. package/src/core/text-rendering/Utils.ts +5 -1
  96. package/src/main-api/Renderer.test.ts +153 -0
  97. package/src/main-api/Renderer.ts +33 -3
  98. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
  99. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
  100. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
@@ -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;
@@ -302,11 +302,14 @@ export const loadFont = async (
302
302
  const nwff: CoreTextNode[] = (nodesWaitingForFont[fontFamily] = []);
303
303
  // Create loading promise
304
304
  const loadPromise = (async (): Promise<void> => {
305
- // Load font JSON data via the platform's fetch abstraction so behaviour
306
- // can be overridden per platform (XHR, Fetch API, custom loaders, etc.).
307
- const blob = (await stage.platform.fetch(atlasDataUrl)) as Blob;
308
- const fontData = JSON.parse(await blob.text()) as SdfFontData;
309
- if (fontData === null || fontData.chars === undefined) {
305
+ // Load font JSON data
306
+ const response = await fetch(atlasDataUrl);
307
+ if (!response.ok) {
308
+ throw new Error(`Failed to load font data: ${response.statusText}`);
309
+ }
310
+
311
+ const fontData = (await response.json()) as SdfFontData;
312
+ if (!fontData || !fontData.chars) {
310
313
  throw new Error('Invalid SDF font data format');
311
314
  }
312
315
 
@@ -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;
@@ -254,14 +254,6 @@ export interface TrProps extends TrFontProps {
254
254
  * Glyph layout information for WebGL rendering
255
255
  */
256
256
  export interface GlyphLayout {
257
- /**
258
- * Unicode codepoint
259
- */
260
- codepoint: number;
261
- /**
262
- * Glyph ID in the font atlas
263
- */
264
- glyphId: number;
265
257
  /**
266
258
  * X position relative to text origin
267
259
  */
@@ -278,14 +270,6 @@ export interface GlyphLayout {
278
270
  * Height of glyph in font units
279
271
  */
280
272
  height: number;
281
- /**
282
- * X offset for glyph positioning
283
- */
284
- xOffset: number;
285
- /**
286
- * Y offset for glyph positioning
287
- */
288
- yOffset: number;
289
273
  /**
290
274
  * Atlas texture coordinates (normalized 0-1)
291
275
  */
@@ -300,9 +284,13 @@ export interface GlyphLayout {
300
284
  */
301
285
  export interface TextLayout {
302
286
  /**
303
- * Individual glyph layouts
287
+ * vertices for rendering quads in WebGL
304
288
  */
305
- glyphs: GlyphLayout[];
289
+ vertexBuffer: Float32Array;
290
+ /**
291
+ * glyph count in layout
292
+ */
293
+ glyphCount: number;
306
294
  /**
307
295
  * Total text width
308
296
  */
@@ -327,6 +315,14 @@ export interface TextLayout {
327
315
  * distanceRange used
328
316
  */
329
317
  distanceRange: number;
318
+ /**
319
+ * number of lines that exceeded maxHeight and were truncated
320
+ */
321
+ remainingLines: number;
322
+ /**
323
+ * Whether there is remaining text that exceeded maxHeight and was truncated
324
+ */
325
+ hasRemainingText: boolean;
330
326
  }
331
327
 
332
328
  export interface FontLoadOptions {
@@ -379,6 +375,14 @@ export interface TextRenderProps {
379
375
  parentHasRenderTexture: boolean;
380
376
  framebufferDimensions: unknown;
381
377
  stage: Stage;
378
+ /**
379
+ * Mutable wrapper ref used by the SDF renderer to cache the underlying
380
+ * WebGLBuffer across frames. The SDF renderer reads and writes the
381
+ * `.current` property so the node's ref box is updated in-place.
382
+ * CoreTextNode owns the ref and is responsible for calling
383
+ * deleteBuffer when the buffer is no longer needed.
384
+ */
385
+ glBufferRef: { current: WebGLBuffer | null };
382
386
  }
383
387
 
384
388
  export interface TextRenderInfo {
@@ -394,15 +398,13 @@ export interface TextRenderer {
394
398
  type: 'canvas' | 'sdf';
395
399
  font: FontHandler;
396
400
  renderText: (props: CoreTextNodeProps) => TextRenderInfo;
397
- // Updated to accept layout data and return vertex buffer for performance
398
- addQuads: (layout?: TextLayout) => Float32Array | null;
399
401
  renderQuads: (
400
402
  renderer: CoreRenderer,
401
403
  layout: TextLayout,
402
- vertexBuffer: Float32Array,
403
404
  renderProps: TextRenderProps,
404
405
  ) => void;
405
406
  init: (stage: Stage) => void;
407
+ clearCache: () => void;
406
408
  }
407
409
 
408
410
  /**
@@ -17,7 +17,7 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
- import type { NormalizedFontMetrics } from './TextRenderer.js';
20
+ import type { CoreTextNodeProps } from '../CoreTextNode.js';
21
21
 
22
22
  const invisibleChars = /[\u200B\u200C\u200D\uFEFF\u00AD\u2060]/g;
23
23
 
@@ -97,3 +97,7 @@ export function tokenizeString(tokenRegex: RegExp, text: string): string[] {
97
97
  final.pop();
98
98
  return final.filter((word) => word != '');
99
99
  }
100
+
101
+ export function getLayoutCacheKey(props: CoreTextNodeProps): string {
102
+ return `${props.text}-${props.fontFamily}-${props.fontSize}-${props.letterSpacing}-${props.lineHeight}-${props.maxHeight}-${props.maxWidth}-${props.textAlign}-${props.wordBreak}-${props.maxLines}-${props.overflowSuffix}`;
103
+ }