@lightningjs/renderer 3.0.2 → 3.0.4

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 (119) hide show
  1. package/README.md +56 -196
  2. package/dist/src/core/CoreNode.d.ts +2 -1
  3. package/dist/src/core/CoreNode.js +31 -7
  4. package/dist/src/core/CoreNode.js.map +1 -1
  5. package/dist/src/core/CoreTextNode.d.ts +26 -6
  6. package/dist/src/core/CoreTextNode.js +163 -60
  7. package/dist/src/core/CoreTextNode.js.map +1 -1
  8. package/dist/src/core/CoreTextureManager.d.ts +8 -0
  9. package/dist/src/core/CoreTextureManager.js +13 -1
  10. package/dist/src/core/CoreTextureManager.js.map +1 -1
  11. package/dist/src/core/Stage.d.ts +8 -0
  12. package/dist/src/core/Stage.js +23 -0
  13. package/dist/src/core/Stage.js.map +1 -1
  14. package/dist/src/core/TextureMemoryManager.d.ts +8 -13
  15. package/dist/src/core/TextureMemoryManager.js +22 -27
  16. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  17. package/dist/src/core/lib/ImageWorker.d.ts +2 -2
  18. package/dist/src/core/lib/ImageWorker.js +31 -12
  19. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  20. package/dist/src/core/lib/WebGlContextWrapper.d.ts +105 -56
  21. package/dist/src/core/lib/WebGlContextWrapper.js +164 -158
  22. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  23. package/dist/src/core/lib/fps.d.ts +15 -0
  24. package/dist/src/core/lib/fps.js +62 -0
  25. package/dist/src/core/lib/fps.js.map +1 -0
  26. package/dist/src/core/lib/textureCompression.js +19 -10
  27. package/dist/src/core/lib/textureCompression.js.map +1 -1
  28. package/dist/src/core/lib/validateImageBitmap.d.ts +2 -1
  29. package/dist/src/core/lib/validateImageBitmap.js +4 -4
  30. package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
  31. package/dist/src/core/platform.js +2 -2
  32. package/dist/src/core/platform.js.map +1 -1
  33. package/dist/src/core/platforms/Platform.d.ts +4 -0
  34. package/dist/src/core/platforms/Platform.js.map +1 -1
  35. package/dist/src/core/platforms/web/WebPlatform.d.ts +2 -0
  36. package/dist/src/core/platforms/web/WebPlatform.js +13 -0
  37. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  38. package/dist/src/core/renderers/CoreRenderer.d.ts +6 -0
  39. package/dist/src/core/renderers/CoreRenderer.js +8 -0
  40. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  41. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +1 -0
  42. package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -0
  43. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  44. package/dist/src/core/renderers/webgl/WebGlRenderOp.d.ts +45 -0
  45. package/dist/src/core/renderers/webgl/WebGlRenderOp.js +127 -0
  46. package/dist/src/core/renderers/webgl/WebGlRenderOp.js.map +1 -0
  47. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +4 -2
  48. package/dist/src/core/renderers/webgl/WebGlRenderer.js +30 -22
  49. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  50. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +2 -3
  51. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  52. package/dist/src/core/text-rendering/CanvasFont.d.ts +14 -0
  53. package/dist/src/core/text-rendering/CanvasFont.js +120 -0
  54. package/dist/src/core/text-rendering/CanvasFont.js.map +1 -0
  55. package/dist/src/core/text-rendering/CanvasFontHandler.d.ts +1 -1
  56. package/dist/src/core/text-rendering/CanvasFontHandler.js +1 -1
  57. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  58. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +3 -5
  59. package/dist/src/core/text-rendering/CanvasTextRenderer.js +16 -22
  60. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  61. package/dist/src/core/text-rendering/CoreFont.d.ts +33 -0
  62. package/dist/src/core/text-rendering/CoreFont.js +48 -0
  63. package/dist/src/core/text-rendering/CoreFont.js.map +1 -0
  64. package/dist/src/core/text-rendering/FontManager.d.ts +11 -0
  65. package/dist/src/core/text-rendering/FontManager.js +41 -0
  66. package/dist/src/core/text-rendering/FontManager.js.map +1 -0
  67. package/dist/src/core/text-rendering/SdfFont.d.ts +29 -0
  68. package/dist/src/core/text-rendering/SdfFont.js +142 -0
  69. package/dist/src/core/text-rendering/SdfFont.js.map +1 -0
  70. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +4 -6
  71. package/dist/src/core/text-rendering/SdfTextRenderer.js +87 -168
  72. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  73. package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
  74. package/dist/src/core/text-rendering/TextGenerator.js +36 -0
  75. package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
  76. package/dist/src/core/text-rendering/TextLayoutEngine.js +43 -12
  77. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  78. package/dist/src/core/text-rendering/TextRenderer.d.ts +41 -27
  79. package/dist/src/core/text-rendering/Utils.d.ts +2 -0
  80. package/dist/src/core/text-rendering/Utils.js +3 -0
  81. package/dist/src/core/text-rendering/Utils.js.map +1 -1
  82. package/dist/src/main-api/Inspector.d.ts +1 -1
  83. package/dist/src/main-api/Inspector.js +25 -20
  84. package/dist/src/main-api/Inspector.js.map +1 -1
  85. package/dist/src/main-api/Renderer.d.ts +14 -0
  86. package/dist/src/main-api/Renderer.js +29 -3
  87. package/dist/src/main-api/Renderer.js.map +1 -1
  88. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  89. package/package.json +2 -1
  90. package/src/core/CoreNode.test.ts +1 -1
  91. package/src/core/CoreNode.ts +37 -8
  92. package/src/core/CoreTextNode.test.ts +350 -0
  93. package/src/core/CoreTextNode.ts +201 -74
  94. package/src/core/CoreTextureManager.ts +14 -2
  95. package/src/core/Stage.ts +29 -0
  96. package/src/core/TextureMemoryManager.test.ts +134 -0
  97. package/src/core/TextureMemoryManager.ts +23 -30
  98. package/src/core/platforms/Platform.ts +5 -0
  99. package/src/core/platforms/web/WebPlatform.ts +13 -0
  100. package/src/core/renderers/CoreRenderer.ts +10 -0
  101. package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
  102. package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
  103. package/src/core/renderers/webgl/WebGlRenderer.ts +40 -31
  104. package/src/core/renderers/webgl/WebGlShaderProgram.test.ts +274 -0
  105. package/src/core/renderers/webgl/WebGlShaderProgram.ts +7 -7
  106. package/src/core/text-rendering/CanvasFontHandler.ts +2 -2
  107. package/src/core/text-rendering/CanvasTextRenderer.ts +24 -45
  108. package/src/core/text-rendering/SdfTextRenderer.ts +106 -215
  109. package/src/core/text-rendering/TextLayoutEngine.ts +61 -28
  110. package/src/core/text-rendering/TextRenderer.ts +42 -33
  111. package/src/core/text-rendering/Utils.ts +5 -1
  112. package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +20 -0
  113. package/src/main-api/Inspector.ts +25 -25
  114. package/src/main-api/Renderer.test.ts +153 -0
  115. package/src/main-api/Renderer.ts +33 -3
  116. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
  117. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
  118. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
  119. package/src/core/renderers/webgl/SdfRenderOp.ts +0 -106
@@ -20,23 +20,19 @@
20
20
  import type { Stage } from '../Stage.js';
21
21
  import type {
22
22
  FontHandler,
23
+ SdfRenderInfo,
23
24
  TextLineStruct,
24
25
  TextRenderInfo,
25
- TextRenderProps,
26
26
  } from './TextRenderer.js';
27
- import type { CoreTextNodeProps } from '../CoreTextNode.js';
28
- import { hasZeroWidthSpace } from './Utils.js';
27
+ import type { CoreTextNode, CoreTextNodeProps } from '../CoreTextNode.js';
28
+ import { getLayoutCacheKey, hasZeroWidthSpace } from './Utils.js';
29
29
  import * as SdfFontHandler from './SdfFontHandler.js';
30
- import type { CoreRenderer } from '../renderers/CoreRenderer.js';
31
30
  import { WebGlRenderer } from '../renderers/webgl/WebGlRenderer.js';
32
- import { SdfRenderOp } from '../renderers/webgl/SdfRenderOp.js';
33
- import { Sdf, type SdfShaderProps } from '../shaders/webgl/SdfShader.js';
34
- import { BufferCollection } from '../renderers/webgl/internal/BufferCollection.js';
35
- import type { WebGlCtxTexture } from '../renderers/webgl/WebGlCtxTexture.js';
31
+ import { Sdf } from '../shaders/webgl/SdfShader.js';
36
32
  import type { WebGlShaderNode } from '../renderers/webgl/WebGlShaderNode.js';
37
- import { mergeColorAlpha } from '../../utils.js';
38
- import type { TextLayout, GlyphLayout } from './TextRenderer.js';
33
+ import type { TextLayout } from './TextRenderer.js';
39
34
  import { mapTextLayout } from './TextLayoutEngine.js';
35
+ import type { WebGlCtxTexture } from '../renderers/webgl/WebGlCtxTexture.js';
40
36
 
41
37
  // Each glyph requires 6 vertices (2 triangles) with 4 floats each (x, y, u, v)
42
38
  const FLOATS_PER_VERTEX = 4;
@@ -46,6 +42,7 @@ const VERTICES_PER_GLYPH = 6;
46
42
  const type = 'sdf' as const;
47
43
 
48
44
  let sdfShader: WebGlShaderNode | null = null;
45
+ let renderer: WebGlRenderer | null = null;
49
46
 
50
47
  // Initialize the SDF text renderer
51
48
  const init = (stage: Stage): void => {
@@ -54,9 +51,11 @@ const init = (stage: Stage): void => {
54
51
  // Register SDF shader with the shader manager
55
52
  stage.shManager.registerShaderType('Sdf', Sdf);
56
53
  sdfShader = stage.shManager.createShader('Sdf') as WebGlShaderNode;
54
+ renderer = stage.renderer as WebGlRenderer;
57
55
  };
58
56
 
59
57
  const font: FontHandler = SdfFontHandler;
58
+ const renderInfoCache = new Map<string, SdfRenderInfo>();
60
59
 
61
60
  /**
62
61
  * SDF text renderer using MSDF/SDF fonts with WebGL
@@ -66,204 +65,41 @@ const font: FontHandler = SdfFontHandler;
66
65
  * @returns Object containing ImageData and dimensions
67
66
  */
68
67
  const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
69
- // Early return if no text
70
- if (props.text.length === 0) {
71
- return {
72
- width: 0,
73
- height: 0,
74
- };
75
- }
68
+ const cacheKey = getLayoutCacheKey(props);
76
69
 
77
- // Get font cache for this font family
78
- const fontData = SdfFontHandler.getFontData(props.fontFamily);
79
- if (fontData === undefined) {
80
- // Font not loaded, return empty result
81
- return {
82
- width: 0,
83
- height: 0,
84
- };
70
+ let renderInfo = renderInfoCache.get(cacheKey);
71
+ if (renderInfo !== undefined) {
72
+ return renderInfo;
85
73
  }
86
74
 
87
75
  // Calculate text layout and generate glyph data for caching
88
- const layout = generateTextLayout(props, fontData);
89
-
90
- // For SDF renderer, ImageData is null since we render via WebGL
91
- return {
92
- remainingLines: 0,
93
- hasRemainingText: false,
76
+ const layout = generateTextLayout(
77
+ props,
78
+ SdfFontHandler.getFontData(props.fontFamily)!,
79
+ );
80
+ renderInfo = {
81
+ type,
82
+ layout,
94
83
  width: layout.width,
95
84
  height: layout.height,
96
- layout, // Cache layout for addQuads
97
- };
98
- };
99
-
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;
85
+ remainingLines: layout.remainingLines,
86
+ hasRemainingText: layout.hasRemainingText,
87
+ atlasTexture: SdfFontHandler.getAtlas(props.fontFamily)!
88
+ .ctxTexture as WebGlCtxTexture,
89
+ } as SdfRenderInfo;
90
+ renderInfoCache.set(cacheKey, renderInfo);
110
91
 
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;
92
+ // For SDF renderer, ImageData is null since we render via WebGL
93
+ return renderInfo;
179
94
  };
180
95
 
181
96
  /**
182
97
  * Create and submit WebGL render operations for SDF text
183
98
  * This is called from CoreTextNode during rendering to add SDF text to the render pipeline
184
99
  */
185
- const renderQuads = (
186
- renderer: CoreRenderer,
187
- layout: TextLayout,
188
- vertexBuffer: Float32Array,
189
- renderProps: TextRenderProps,
190
- ): void => {
191
- const fontFamily = renderProps.fontFamily;
192
- const color = renderProps.color;
193
- const worldAlpha = renderProps.worldAlpha;
194
- const globalTransform = renderProps.globalTransform;
195
-
196
- const atlasTexture = SdfFontHandler.getAtlas(fontFamily);
197
- if (atlasTexture === null) {
198
- console.warn(`SDF atlas texture not found for font: ${fontFamily}`);
199
- return;
200
- }
201
-
202
- // We can safely assume this is a WebGL renderer else this wouldn't be called
203
- const glw = (renderer as WebGlRenderer).glw;
204
- 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
-
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,
231
- },
232
- },
233
- },
234
- ]);
235
-
236
- const buffer = webGlBuffers.getBuffer('a_position');
237
- if (buffer !== undefined) {
238
- glw.arrayBufferData(buffer, vertexBuffer, glw.STATIC_DRAW as number);
239
- }
240
-
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;
265
-
266
- (renderer as WebGlRenderer).addRenderOp(renderOp);
100
+ const renderQuads = (textNode: CoreTextNode): void => {
101
+ textNode.props.shader = sdfShader;
102
+ renderer!.addRenderOp(textNode);
267
103
  };
268
104
 
269
105
  /**
@@ -315,8 +151,27 @@ const generateTextLayout = (
315
151
  );
316
152
 
317
153
  const lineAmount = lines.length;
154
+ let bufferIndex = 0;
155
+ let glyphCount = 0;
156
+ // Count total glyphs (excluding spaces) for buffer allocation
157
+ for (let i = 0; i < lineAmount; i++) {
158
+ const textLine = (lines[i] as TextLineStruct)[0];
159
+ for (const char of textLine) {
160
+ if (hasZeroWidthSpace(char) === true) {
161
+ continue;
162
+ }
163
+ const codepoint = char.codePointAt(0);
164
+ if (codepoint === undefined) {
165
+ continue;
166
+ }
167
+ glyphCount++;
168
+ }
169
+ }
170
+
171
+ const vertexBuffer = new Float32Array(
172
+ glyphCount * VERTICES_PER_GLYPH * FLOATS_PER_VERTEX,
173
+ );
318
174
 
319
- const glyphs: GlyphLayout[] = [];
320
175
  let currentX = 0;
321
176
  let currentY = 0;
322
177
  for (let i = 0; i < lineAmount; i++) {
@@ -351,23 +206,52 @@ const generateTextLayout = (
351
206
  // Apply pair kerning before placing this glyph.
352
207
  currentX += kerning;
353
208
 
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);
209
+ const x1 = currentX + glyph.xoffset;
210
+ const y1 = currentY + glyph.yoffset;
211
+ const x2 = x1 + glyph.width;
212
+ const y2 = y1 + glyph.height;
213
+ const u1 = glyph.x / atlasWidth;
214
+ const v1 = glyph.y / atlasHeight;
215
+ const u2 = u1 + glyph.width / atlasWidth;
216
+ const v2 = v1 + glyph.height / atlasHeight;
217
+
218
+ // Triangle 1: Top-left, top-right, bottom-left
219
+ // Vertex 1: Top-left
220
+ vertexBuffer[bufferIndex++] = x1;
221
+ vertexBuffer[bufferIndex++] = y1;
222
+ vertexBuffer[bufferIndex++] = u1;
223
+ vertexBuffer[bufferIndex++] = v1;
224
+
225
+ // Vertex 2: Top-right
226
+ vertexBuffer[bufferIndex++] = x2;
227
+ vertexBuffer[bufferIndex++] = y1;
228
+ vertexBuffer[bufferIndex++] = u2;
229
+ vertexBuffer[bufferIndex++] = v1;
230
+
231
+ // Vertex 3: Bottom-left
232
+ vertexBuffer[bufferIndex++] = x1;
233
+ vertexBuffer[bufferIndex++] = y2;
234
+ vertexBuffer[bufferIndex++] = u1;
235
+ vertexBuffer[bufferIndex++] = v2;
236
+
237
+ // Triangle 2: Top-right, bottom-right, bottom-left
238
+ // Vertex 4: Top-right (duplicate)
239
+ vertexBuffer[bufferIndex++] = x2;
240
+ vertexBuffer[bufferIndex++] = y1;
241
+ vertexBuffer[bufferIndex++] = u2;
242
+ vertexBuffer[bufferIndex++] = v1;
243
+
244
+ // Vertex 5: Bottom-right
245
+ vertexBuffer[bufferIndex++] = x2;
246
+ vertexBuffer[bufferIndex++] = y2;
247
+ vertexBuffer[bufferIndex++] = u2;
248
+ vertexBuffer[bufferIndex++] = v2;
249
+
250
+ // Vertex 6: Bottom-left (duplicate)
251
+ vertexBuffer[bufferIndex++] = x1;
252
+ vertexBuffer[bufferIndex++] = y2;
253
+ vertexBuffer[bufferIndex++] = u1;
254
+ vertexBuffer[bufferIndex++] = v2;
371
255
 
372
256
  // Advance position with letter spacing (in design units)
373
257
  currentX += glyph.xadvance + letterSpacing;
@@ -378,16 +262,23 @@ const generateTextLayout = (
378
262
 
379
263
  // Convert final dimensions to pixel space for the layout
380
264
  return {
381
- glyphs,
265
+ vertexBuffer,
266
+ glyphCount,
382
267
  distanceRange: fontScale * fontData.distanceField.distanceRange,
383
268
  width: effectiveWidth * fontScale,
384
269
  height: effectiveHeight,
385
270
  fontScale: fontScale,
386
271
  lineHeight: lineHeightPx,
387
272
  fontFamily,
273
+ remainingLines,
274
+ hasRemainingText,
388
275
  };
389
276
  };
390
277
 
278
+ const clearCache = (): void => {
279
+ renderInfoCache.clear();
280
+ };
281
+
391
282
  /**
392
283
  * SDF Text Renderer - implements TextRenderer interface
393
284
  */
@@ -395,9 +286,9 @@ const SdfTextRenderer = {
395
286
  type,
396
287
  font,
397
288
  renderText,
398
- addQuads,
399
289
  renderQuads,
400
290
  init,
291
+ clearCache,
401
292
  };
402
293
 
403
294
  export default SdfTextRenderer;
@@ -227,7 +227,9 @@ export const wrapText = (
227
227
  : [[['', 0, false, 0, 0]], remainingLines, i < lines.length - 1];
228
228
 
229
229
  remainingLines--;
230
- wrappedLines.push(...wrappedLine);
230
+ for (let j = 0; j < wrappedLine.length; j++) {
231
+ wrappedLines.push(wrappedLine[j]!);
232
+ }
231
233
 
232
234
  if (hasMaxLines === true && remainingLines <= 0) {
233
235
  const lastLine = wrappedLines[wrappedLines.length - 1]!;
@@ -278,44 +280,75 @@ export const wrapLine = (
278
280
  let hasRemainingText = true;
279
281
 
280
282
  const wrapFn = getWrapStrategy(wordBreak);
281
- while (words.length > 0 && remainingLines > 0) {
282
- let word = words.shift()!;
283
- let wordWidth = measureText(word, fontFamily, letterSpacing);
283
+ let wordIdx = 0;
284
+ let spaceIdx = 0;
285
+ let pendingWord = '';
286
+
287
+ while (
288
+ (pendingWord.length > 0 || wordIdx < words.length) &&
289
+ remainingLines > 0
290
+ ) {
291
+ let word: string;
292
+ let wordWidth: number;
284
293
  let remainingWord = '';
285
294
 
295
+ if (pendingWord.length > 0) {
296
+ word = pendingWord;
297
+ pendingWord = '';
298
+ } else {
299
+ word = words[wordIdx++]!;
300
+ }
301
+ wordWidth = measureText(word, fontFamily, letterSpacing);
302
+
286
303
  //handle first word of new line separately to avoid empty line issues
287
304
  if (currentLineWidth === 0) {
288
305
  // Word doesn't fit on current line
289
306
  //if first word doesn't fit on empty line
290
307
  if (wordWidth > maxWidth) {
291
308
  remainingLines--;
309
+ const isLastLine = remainingLines === 0;
310
+ let lineTruncated = isLastLine;
292
311
  //truncate word to fit
293
- [word, remainingWord, wordWidth] =
294
- remainingLines === 0
295
- ? truncateWord(
296
- measureText,
297
- word,
298
- wordWidth,
299
- maxWidth,
300
- fontFamily,
301
- letterSpacing,
302
- overflowSuffix,
303
- overflowWidth,
304
- )
305
- : splitWord(
306
- measureText,
307
- word,
308
- wordWidth,
309
- maxWidth,
310
- fontFamily,
311
- letterSpacing,
312
- );
312
+ [word, remainingWord, wordWidth] = isLastLine
313
+ ? truncateWord(
314
+ measureText,
315
+ word,
316
+ wordWidth,
317
+ maxWidth,
318
+ fontFamily,
319
+ letterSpacing,
320
+ overflowSuffix,
321
+ overflowWidth,
322
+ )
323
+ : splitWord(
324
+ measureText,
325
+ word,
326
+ wordWidth,
327
+ maxWidth,
328
+ fontFamily,
329
+ letterSpacing,
330
+ );
313
331
 
314
332
  if (remainingWord.length > 0) {
315
- words.unshift(remainingWord);
333
+ if (word.length === 0) {
334
+ if (overflowSuffix.length > 0) {
335
+ word = overflowSuffix;
336
+ wordWidth = overflowWidth;
337
+ } else {
338
+ word = remainingWord.charAt(0);
339
+ if (word.length === 0) {
340
+ break;
341
+ }
342
+ wordWidth = measureText(word, fontFamily, letterSpacing);
343
+ }
344
+ remainingWord = '';
345
+ remainingLines = 0;
346
+ lineTruncated = true;
347
+ }
348
+ pendingWord = remainingWord;
316
349
  }
317
350
  // first word doesn't fit on an empty line
318
- wrappedLines.push([word, wordWidth, false, 0, 0]);
351
+ wrappedLines.push([word, wordWidth, lineTruncated, 0, 0]);
319
352
  } else if (wordWidth + spaceWidth >= maxWidth) {
320
353
  remainingLines--;
321
354
  // word with space doesn't fit, but word itself fits - put on new line
@@ -326,7 +359,7 @@ export const wrapLine = (
326
359
  }
327
360
  continue;
328
361
  }
329
- const space = spaces.shift() || '';
362
+ const space = spaces[spaceIdx++] || '';
330
363
  // For width calculation, treat ZWSP as having 0 width but regular space functionality
331
364
  const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
332
365
  const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
@@ -367,7 +400,7 @@ export const wrapLine = (
367
400
  );
368
401
 
369
402
  if (remainingWord.length > 0) {
370
- words.unshift(remainingWord);
403
+ pendingWord = remainingWord;
371
404
  }
372
405
  }
373
406
 
@@ -17,8 +17,8 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
- import type { CoreTextNodeProps } from '../CoreTextNode.js';
21
- import type { CoreRenderer } from '../renderers/CoreRenderer.js';
20
+ import type { CoreTextNode, CoreTextNodeProps } from '../CoreTextNode.js';
21
+ import type { WebGlCtxTexture } from '../renderers/webgl/WebGlCtxTexture.js';
22
22
  import type { Stage } from '../Stage.js';
23
23
 
24
24
  // Text baseline and vertical align types
@@ -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
288
+ */
289
+ vertexBuffer: Float32Array;
290
+ /**
291
+ * glyph count in layout
304
292
  */
305
- glyphs: GlyphLayout[];
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,30 +375,43 @@ 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
- export interface TextRenderInfo {
388
+ export interface RenderInfo {
385
389
  width: number;
386
390
  height: number;
387
- hasRemainingText?: boolean;
388
- remainingLines?: number;
389
- imageData?: ImageData | null; // Image data for Canvas Text Renderer
390
- layout?: TextLayout; // Layout data for SDF renderer caching
391
+ hasRemainingText: boolean;
392
+ remainingLines: number;
391
393
  }
392
394
 
395
+ export type SdfRenderInfo = RenderInfo & {
396
+ type: 'sdf';
397
+ layout: TextLayout;
398
+ atlasTexture: WebGlCtxTexture;
399
+ };
400
+
401
+ export type CanvasRenderInfo = RenderInfo & {
402
+ type: 'canvas';
403
+ imageData: ImageData;
404
+ };
405
+
406
+ export type TextRenderInfo = SdfRenderInfo | CanvasRenderInfo;
407
+
393
408
  export interface TextRenderer {
394
409
  type: 'canvas' | 'sdf';
395
410
  font: FontHandler;
396
411
  renderText: (props: CoreTextNodeProps) => TextRenderInfo;
397
- // Updated to accept layout data and return vertex buffer for performance
398
- addQuads: (layout?: TextLayout) => Float32Array | null;
399
- renderQuads: (
400
- renderer: CoreRenderer,
401
- layout: TextLayout,
402
- vertexBuffer: Float32Array,
403
- renderProps: TextRenderProps,
404
- ) => void;
412
+ renderQuads: (textNode: CoreTextNode) => void;
405
413
  init: (stage: Stage) => void;
414
+ clearCache: () => void;
406
415
  }
407
416
 
408
417
  /**
@@ -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
+ }