@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
@@ -18,7 +18,6 @@
18
18
  */
19
19
 
20
20
  import { CoreRenderer, type BufferInfo } from '../CoreRenderer.js';
21
- import type { SdfRenderOp } from './SdfRenderOp.js';
22
21
  import type { CoreContextTexture } from '../CoreContextTexture.js';
23
22
  import {
24
23
  createIndexBuffer,
@@ -47,15 +46,15 @@ import type { WebGlShaderType } from './WebGlShaderNode.js';
47
46
  import { WebGlShaderNode } from './WebGlShaderNode.js';
48
47
  import type { Dimensions } from '../../../common/CommonTypes.js';
49
48
  import type { GlContextWrapper } from '../../platforms/GlContextWrapper.js';
50
- import type { Platform } from '../../platforms/Platform.js';
51
49
  import type { Stage } from '../../Stage.js';
50
+ import type { CoreTextNode } from '../../CoreTextNode.js';
52
51
 
53
52
  interface CoreWebGlSystem {
54
53
  parameters: CoreWebGlParameters;
55
54
  extensions: CoreWebGlExtensions;
56
55
  }
57
56
 
58
- export type WebGlRenderOp = CoreNode | SdfRenderOp;
57
+ export type WebGlRenderOp = CoreNode | CoreTextNode;
59
58
 
60
59
  export class WebGlRenderer extends CoreRenderer {
61
60
  //// WebGL Native Context and Data
@@ -474,28 +473,27 @@ export class WebGlRenderer extends CoreRenderer {
474
473
  private insertRTTNodeInOrder(node: CoreNode) {
475
474
  let insertIndex = this.rttNodes.length; // Default to the end of the array
476
475
 
476
+ // Build a one-shot index map so all lookups below are O(1) instead of O(n).
477
+ const rttNodes = this.rttNodes;
478
+ const indexMap = new Map<number, number>();
479
+ for (let i = 0; i < rttNodes.length; i++) {
480
+ indexMap.set(rttNodes[i]!.id, i);
481
+ }
482
+
477
483
  // 1. Traverse upwards to ensure the node is placed before its RTT parent (if any).
478
484
  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
485
+ while (currentNode.parent !== null) {
486
+ const parentIndex = indexMap.get(currentNode.parent.id);
487
+ if (parentIndex !== undefined) {
487
488
  insertIndex = parentIndex;
488
489
  break;
489
490
  }
490
-
491
491
  currentNode = currentNode.parent;
492
492
  }
493
493
 
494
494
  // 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);
495
+ const maxChildIndex = this.findMaxChildRTTIndex(node, indexMap);
497
496
  if (maxChildIndex !== -1) {
498
- // Adjust insertIndex to be after the last child RTT node
499
497
  insertIndex = Math.max(insertIndex, maxChildIndex + 1);
500
498
  }
501
499
 
@@ -503,25 +501,25 @@ export class WebGlRenderer extends CoreRenderer {
503
501
  this.rttNodes.splice(insertIndex, 0, node);
504
502
  }
505
503
 
506
- // Helper function to find the highest index of any RTT children of a node within rttNodes
507
- private findMaxChildRTTIndex(node: CoreNode): number {
504
+ // Iterative DFS to find the highest rttNodes index among all RTT descendants of node.
505
+ private findMaxChildRTTIndex(
506
+ node: CoreNode,
507
+ indexMap: Map<number, number>,
508
+ ): number {
508
509
  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);
510
+ // Explicit stack avoids recursive arrow function allocation and call-stack growth.
511
+ const stack: CoreNode[] = [node];
512
+ while (stack.length !== 0) {
513
+ const current = stack.pop()!;
514
+ const idx = indexMap.get(current.id);
515
+ if (idx !== undefined && idx > maxIndex) {
516
+ maxIndex = idx;
514
517
  }
515
-
516
- // Recursively check all children of the current node
517
- for (const child of currentNode.children) {
518
- traverseChildren(child);
518
+ const children = current.children;
519
+ for (let i = 0; i < children.length; i++) {
520
+ stack.push(children[i]!);
519
521
  }
520
- };
521
-
522
- // Start traversal directly with the provided node
523
- traverseChildren(node);
524
-
522
+ }
525
523
  return maxIndex;
526
524
  }
527
525
 
@@ -724,4 +722,15 @@ export class WebGlRenderer extends CoreRenderer {
724
722
  normalized: normalizedColor,
725
723
  };
726
724
  }
725
+
726
+ override destroy(): void {
727
+ const loseCtx = this.glw.getExtension(
728
+ 'WEBGL_lose_context',
729
+ ) as WEBGL_lose_context | null;
730
+ loseCtx?.loseContext();
731
+ }
732
+
733
+ override deleteBuffer(buffer: WebGLBuffer): void {
734
+ this.glw.deleteBuffer(buffer);
735
+ }
727
736
  }
@@ -0,0 +1,274 @@
1
+ /*
2
+ * If not stated otherwise in this file or this component's LICENSE file the
3
+ * following copyright and licenses apply:
4
+ *
5
+ * Copyright 2026 Comcast Cable Communications Management, LLC.
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the License);
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ import { describe, expect, it, vi } from 'vitest';
21
+ import { mock } from 'vitest-mock-extended';
22
+ import { CoreTextNode, type CoreTextNodeProps } from '../../CoreTextNode.js';
23
+ import type { Stage } from '../../Stage.js';
24
+ import type { TextRenderer } from '../../text-rendering/TextRenderer.js';
25
+ import { createBound } from '../../lib/utils.js';
26
+ import type { CoreRenderer } from '../CoreRenderer.js';
27
+ import { WebGlRenderer } from './WebGlRenderer.js';
28
+ import { WebGlShaderProgram } from './WebGlShaderProgram.js';
29
+
30
+ const makeStage = (): Stage =>
31
+ mock<Stage>({
32
+ strictBound: createBound(0, 0, 1920, 1080),
33
+ preloadBound: createBound(0, 0, 1920, 1080),
34
+ defaultTexture: {
35
+ state: 'loaded',
36
+ },
37
+ pixelRatio: 2,
38
+ renderer: mock<CoreRenderer>() as CoreRenderer,
39
+ });
40
+
41
+ const makeTextProps = (): CoreTextNodeProps => ({
42
+ alpha: 1,
43
+ autosize: false,
44
+ boundsMargin: null,
45
+ clipping: false,
46
+ color: 0xffffffff,
47
+ colorBl: 0xffffffff,
48
+ colorBottom: 0xffffffff,
49
+ colorBr: 0xffffffff,
50
+ colorLeft: 0xffffffff,
51
+ colorRight: 0xffffffff,
52
+ colorTl: 0xffffffff,
53
+ colorTop: 0xffffffff,
54
+ colorTr: 0xffffffff,
55
+ h: 20,
56
+ mount: 0,
57
+ mountX: 0,
58
+ mountY: 0,
59
+ parent: null,
60
+ pivot: 0,
61
+ pivotX: 0,
62
+ pivotY: 0,
63
+ rotation: 0,
64
+ rtt: false,
65
+ scale: 1,
66
+ scaleX: 1,
67
+ scaleY: 1,
68
+ shader: null,
69
+ src: '',
70
+ texture: null,
71
+ textureOptions: {},
72
+ w: 100,
73
+ x: 0,
74
+ y: 0,
75
+ zIndex: 0,
76
+ text: 'Test',
77
+ textAlign: 'left',
78
+ contain: 'none',
79
+ fontFamily: 'Arial',
80
+ fontStyle: 'normal',
81
+ fontSize: 16,
82
+ letterSpacing: 0,
83
+ lineHeight: 1,
84
+ maxHeight: 0,
85
+ maxLines: 0,
86
+ maxWidth: 0,
87
+ offsetY: 0,
88
+ overflowSuffix: '...',
89
+ verticalAlign: 'top',
90
+ wordBreak: 'break-word',
91
+ textRendererOverride: null,
92
+ forceLoad: false,
93
+ });
94
+
95
+ const makeSdfTextRenderer = (): TextRenderer =>
96
+ ({
97
+ clearCache: vi.fn(),
98
+ type: 'sdf',
99
+ font: {
100
+ isFontLoaded: vi.fn().mockReturnValue(true),
101
+ loadFont: vi.fn(),
102
+ waitingForFont: vi.fn(),
103
+ stopWaitingForFont: vi.fn(),
104
+ },
105
+ init: vi.fn(),
106
+ renderText: vi.fn().mockReturnValue({
107
+ width: 100,
108
+ height: 20,
109
+ layout: { glyphs: [], width: 100, height: 20 },
110
+ }),
111
+ addQuads: vi.fn().mockReturnValue(new Float32Array(0)),
112
+ renderQuads: vi.fn(),
113
+ } as unknown as TextRenderer);
114
+
115
+ const makeSdfTextNode = () => {
116
+ const node = new CoreTextNode(
117
+ makeStage(),
118
+ makeTextProps(),
119
+ makeSdfTextRenderer(),
120
+ );
121
+ node.parentHasRenderTexture = true;
122
+ node.framebufferDimensions = { w: 320, h: 180 };
123
+ node.rttParent = { framebufferDimensions: { w: 640, h: 360 } } as any;
124
+ return node;
125
+ };
126
+
127
+ describe('WebGlShaderProgram.bindRenderOp', () => {
128
+ function createProgram() {
129
+ const program = Object.create(
130
+ WebGlShaderProgram.prototype,
131
+ ) as WebGlShaderProgram;
132
+ const bindTextures = vi.fn();
133
+ const bindBufferCollection = vi.fn();
134
+ const uniform1f = vi.fn();
135
+ const uniform2f = vi.fn();
136
+ const glw = {
137
+ canvas: { width: 1920, height: 1080 },
138
+ uniform1f,
139
+ uniform2f,
140
+ };
141
+
142
+ (program as any).bindTextures = bindTextures;
143
+ (program as any).bindBufferCollection = bindBufferCollection;
144
+ (program as any).glw = glw;
145
+ (program as any).useTimeValue = false;
146
+ (program as any).useSystemAlpha = false;
147
+ (program as any).useSystemDimensions = false;
148
+
149
+ return {
150
+ program,
151
+ bindTextures,
152
+ bindBufferCollection,
153
+ uniform1f,
154
+ uniform2f,
155
+ };
156
+ }
157
+
158
+ it('binds SDF shader props while using the main framebuffer resolution', () => {
159
+ const {
160
+ program,
161
+ bindTextures,
162
+ bindBufferCollection,
163
+ uniform1f,
164
+ uniform2f,
165
+ } = createProgram();
166
+ const onSdfBind = vi.fn();
167
+ const renderOp = {
168
+ isCoreNode: false,
169
+ isSdfRenderOp: true,
170
+ shader: { shaderType: { onSdfBind } },
171
+ sdfShaderProps: { size: 16, distanceRange: 4 },
172
+ renderOpTextures: [],
173
+ quadBufferCollection: {},
174
+ parentHasRenderTexture: false,
175
+ framebufferDimensions: null,
176
+ rtt: false,
177
+ stage: { pixelRatio: 1.5 },
178
+ time: 0,
179
+ worldAlpha: 1,
180
+ w: 100,
181
+ h: 20,
182
+ };
183
+
184
+ program.bindRenderOp(renderOp as any);
185
+
186
+ expect(bindTextures).toHaveBeenCalledWith(renderOp.renderOpTextures);
187
+ expect(bindBufferCollection).toHaveBeenCalledWith(
188
+ renderOp.quadBufferCollection,
189
+ );
190
+ expect(uniform1f).toHaveBeenCalledWith('u_pixelRatio', 1.5);
191
+ expect(uniform2f).toHaveBeenCalledWith('u_resolution', 1920, 1080);
192
+ expect(onSdfBind).toHaveBeenCalledWith(renderOp.sdfShaderProps);
193
+ });
194
+
195
+ it('keeps SDF binding active when rendering into a parent framebuffer', () => {
196
+ const { program, uniform1f, uniform2f } = createProgram();
197
+ const onSdfBind = vi.fn();
198
+ const renderOp = {
199
+ isCoreNode: false,
200
+ isSdfRenderOp: true,
201
+ shader: { shaderType: { onSdfBind } },
202
+ sdfShaderProps: { size: 18, distanceRange: 6 },
203
+ renderOpTextures: [],
204
+ quadBufferCollection: {},
205
+ parentHasRenderTexture: true,
206
+ framebufferDimensions: { w: 320, h: 180 },
207
+ rtt: false,
208
+ stage: { pixelRatio: 2 },
209
+ time: 0,
210
+ worldAlpha: 1,
211
+ w: 100,
212
+ h: 20,
213
+ };
214
+
215
+ program.bindRenderOp(renderOp as any);
216
+
217
+ expect(uniform1f).toHaveBeenCalledWith('u_pixelRatio', 1);
218
+ expect(uniform2f).toHaveBeenCalledWith('u_resolution', 320, 180);
219
+ expect(onSdfBind).toHaveBeenCalledWith(renderOp.sdfShaderProps);
220
+ });
221
+
222
+ it('uses parent RTT dimensions for SDF text render ops', () => {
223
+ const { program, uniform1f, uniform2f } = createProgram();
224
+ const onSdfBind = vi.fn();
225
+ const sdfShaderProps = {
226
+ color: 0xffffffff,
227
+ distanceRange: 1,
228
+ size: 16,
229
+ transform: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]),
230
+ };
231
+ const renderOp = {
232
+ framebufferDimensions: { w: 320, h: 180 },
233
+ isCoreNode: true,
234
+ isSdfRenderOp: true,
235
+ parentFramebufferDimensions: { w: 640, h: 360 },
236
+ parentHasRenderTexture: true,
237
+ quadBufferCollection: {},
238
+ renderOpTextures: [],
239
+ rtt: false,
240
+ sdfShaderProps,
241
+ shader: {
242
+ shaderType: {
243
+ onSdfBind,
244
+ },
245
+ },
246
+ stage: { pixelRatio: 2 },
247
+ time: 0,
248
+ worldAlpha: 1,
249
+ w: 100,
250
+ h: 20,
251
+ } as any;
252
+
253
+ program.bindRenderOp(renderOp);
254
+
255
+ expect(uniform1f).toHaveBeenCalledWith('u_pixelRatio', 1.0);
256
+ expect(uniform2f).toHaveBeenCalledWith('u_resolution', 640, 360);
257
+ expect(onSdfBind).toHaveBeenCalledWith(sdfShaderProps);
258
+ });
259
+ });
260
+
261
+ describe('WebGlRenderer.canReuseRenderOp', () => {
262
+ it('reuses SDF text render ops with matching parent RTT dimensions', () => {
263
+ const renderer = Object.create(WebGlRenderer.prototype) as WebGlRenderer;
264
+ const node = makeSdfTextNode();
265
+
266
+ node.props.shader = {
267
+ shaderKey: 'default',
268
+ } as any;
269
+
270
+ renderer.curRenderOp = node;
271
+
272
+ expect(renderer.reuseRenderOp(node)).toBe(true);
273
+ });
274
+ });
@@ -33,6 +33,8 @@ import {
33
33
  type UniformSet4Params,
34
34
  } from './internal/ShaderUtils.js';
35
35
  import { CoreNode } from '../../CoreNode.js';
36
+ import type { CoreTextNode } from '../../CoreTextNode.js';
37
+ import type { SdfShaderProps } from '../../shaders/webgl/SdfShader.js';
36
38
 
37
39
  export class WebGlShaderProgram implements CoreShaderProgram {
38
40
  protected program: WebGLProgram | null;
@@ -200,13 +202,11 @@ export class WebGlShaderProgram implements CoreShaderProgram {
200
202
  }
201
203
 
202
204
  bindRenderOp(renderOp: WebGlRenderOp) {
203
- const isCoreNode = renderOp.isCoreNode;
204
-
205
205
  this.bindTextures(renderOp.renderOpTextures);
206
206
  this.bindBufferCollection(renderOp.quadBufferCollection);
207
207
 
208
208
  const parentHasRenderTexture = renderOp.parentHasRenderTexture;
209
- const framebufferDimensions = isCoreNode
209
+ const framebufferDimensions = renderOp.isCoreNode
210
210
  ? renderOp.parentFramebufferDimensions
211
211
  : renderOp.framebufferDimensions;
212
212
 
@@ -248,11 +248,11 @@ export class WebGlShaderProgram implements CoreShaderProgram {
248
248
  }
249
249
 
250
250
  /**temporary fix to make sdf texts work */
251
- if (isCoreNode === false && renderOp.sdfShaderProps !== undefined) {
252
- const opShader = renderOp.shader; // SdfRenderOp has .shader
253
- (opShader.shaderType as WebGlShaderType).onSdfBind?.call(
251
+ if (renderOp.isSdfRenderOp === true) {
252
+ const opShader = renderOp.shader!; // SdfRenderOp has .shader
253
+ (opShader.shaderType as WebGlShaderType<SdfShaderProps>).onSdfBind?.call(
254
254
  this.glw,
255
- renderOp.sdfShaderProps,
255
+ (renderOp as CoreTextNode).sdfShaderProps,
256
256
  );
257
257
  return;
258
258
  }
@@ -136,7 +136,7 @@ export const getFontFamilies = (): FontFamilyMap => {
136
136
  */
137
137
  export const init = (
138
138
  c: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
139
- mc: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
139
+ mc?: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
140
140
  ): void => {
141
141
  if (initialized === true) {
142
142
  return;
@@ -149,7 +149,7 @@ export const init = (
149
149
  }
150
150
 
151
151
  context = c;
152
- measureContext = mc;
152
+ measureContext = mc || c;
153
153
 
154
154
  // Register the default 'sans-serif' font face
155
155
  const defaultMetrics: FontMetrics = {
@@ -19,15 +19,21 @@
19
19
 
20
20
  import { assertTruthy } from '../../utils.js';
21
21
  import type { Stage } from '../Stage.js';
22
- import type { TextLineStruct, TextRenderInfo } from './TextRenderer.js';
22
+ import type {
23
+ CanvasRenderInfo,
24
+ FontHandler,
25
+ TextLineStruct,
26
+ TextRenderInfo,
27
+ } from './TextRenderer.js';
23
28
  import * as CanvasFontHandler from './CanvasFontHandler.js';
24
29
  import type { CoreTextNodeProps } from '../CoreTextNode.js';
25
- import { hasZeroWidthSpace } from './Utils.js';
30
+ import { getLayoutCacheKey, hasZeroWidthSpace } from './Utils.js';
26
31
  import { mapTextLayout } from './TextLayoutEngine.js';
27
32
 
28
33
  const MAX_TEXTURE_DIMENSION = 4096;
29
34
 
30
35
  const type = 'canvas' as const;
36
+ const font: FontHandler = CanvasFontHandler;
31
37
 
32
38
  let canvas: HTMLCanvasElement | OffscreenCanvas | null = null;
33
39
  let context:
@@ -43,16 +49,7 @@ let measureContext:
43
49
  | null = null;
44
50
 
45
51
  // 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
- >();
52
+ const renderInfoCache = new Map<string, CanvasRenderInfo>();
56
53
 
57
54
  // Initialize the Text Renderer
58
55
  const init = (stage: Stage): void => {
@@ -96,6 +93,12 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
96
93
  assertTruthy(canvas, 'Canvas is not initialized');
97
94
  assertTruthy(context, 'Canvas context is not available');
98
95
  assertTruthy(measureContext, 'Canvas measureContext is not available');
96
+ const cacheKey = getLayoutCacheKey(props);
97
+
98
+ let layout = renderInfoCache.get(cacheKey);
99
+ if (layout !== undefined) {
100
+ return layout;
101
+ }
99
102
  // Extract already normalized properties
100
103
  const {
101
104
  text,
@@ -189,46 +192,23 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
189
192
  if (canvas.width > 0 && canvas.height > 0) {
190
193
  imageData = context.getImageData(0, 0, canvasW, canvasH);
191
194
  }
192
- return {
195
+ const renderInfo = {
196
+ type,
193
197
  imageData,
194
198
  width: effectiveWidth,
195
199
  height: effectiveHeight,
196
200
  remainingLines,
197
201
  hasRemainingText,
198
- };
202
+ } as CanvasRenderInfo;
203
+ renderInfoCache.set(cacheKey, renderInfo);
204
+ return renderInfo;
199
205
  };
200
206
 
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
207
  /**
219
208
  * Clear layout cache for memory management
220
209
  */
221
- const clearLayoutCache = (): void => {
222
- layoutCache.clear();
223
- };
224
-
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;
210
+ const clearCache = (): void => {
211
+ renderInfoCache.clear();
232
212
  };
233
213
 
234
214
  /**
@@ -244,12 +224,11 @@ const renderQuads = (): void => {
244
224
  */
245
225
  const CanvasTextRenderer = {
246
226
  type,
247
- font: CanvasFontHandler,
227
+ font,
248
228
  renderText,
249
- addQuads,
250
229
  renderQuads,
251
230
  init,
252
- clearLayoutCache,
231
+ clearCache,
253
232
  };
254
233
 
255
234
  export default CanvasTextRenderer;