@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.
- package/README.md +56 -196
- package/dist/src/core/CoreNode.d.ts +2 -1
- package/dist/src/core/CoreNode.js +31 -7
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.d.ts +26 -6
- package/dist/src/core/CoreTextNode.js +163 -60
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.d.ts +8 -0
- package/dist/src/core/CoreTextureManager.js +13 -1
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.d.ts +8 -0
- package/dist/src/core/Stage.js +23 -0
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.d.ts +8 -13
- package/dist/src/core/TextureMemoryManager.js +22 -27
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/lib/ImageWorker.d.ts +2 -2
- package/dist/src/core/lib/ImageWorker.js +31 -12
- package/dist/src/core/lib/ImageWorker.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +105 -56
- package/dist/src/core/lib/WebGlContextWrapper.js +164 -158
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/fps.d.ts +15 -0
- package/dist/src/core/lib/fps.js +62 -0
- package/dist/src/core/lib/fps.js.map +1 -0
- package/dist/src/core/lib/textureCompression.js +19 -10
- package/dist/src/core/lib/textureCompression.js.map +1 -1
- package/dist/src/core/lib/validateImageBitmap.d.ts +2 -1
- package/dist/src/core/lib/validateImageBitmap.js +4 -4
- package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
- package/dist/src/core/platform.js +2 -2
- package/dist/src/core/platform.js.map +1 -1
- package/dist/src/core/platforms/Platform.d.ts +4 -0
- package/dist/src/core/platforms/Platform.js.map +1 -1
- package/dist/src/core/platforms/web/WebPlatform.d.ts +2 -0
- package/dist/src/core/platforms/web/WebPlatform.js +13 -0
- package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
- package/dist/src/core/renderers/CoreRenderer.d.ts +6 -0
- package/dist/src/core/renderers/CoreRenderer.js +8 -0
- package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +1 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderOp.d.ts +45 -0
- package/dist/src/core/renderers/webgl/WebGlRenderOp.js +127 -0
- package/dist/src/core/renderers/webgl/WebGlRenderOp.js.map +1 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +4 -2
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +30 -22
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +2 -3
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasFont.d.ts +14 -0
- package/dist/src/core/text-rendering/CanvasFont.js +120 -0
- package/dist/src/core/text-rendering/CanvasFont.js.map +1 -0
- package/dist/src/core/text-rendering/CanvasFontHandler.d.ts +1 -1
- package/dist/src/core/text-rendering/CanvasFontHandler.js +1 -1
- package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +3 -5
- package/dist/src/core/text-rendering/CanvasTextRenderer.js +16 -22
- package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/CoreFont.d.ts +33 -0
- package/dist/src/core/text-rendering/CoreFont.js +48 -0
- package/dist/src/core/text-rendering/CoreFont.js.map +1 -0
- package/dist/src/core/text-rendering/FontManager.d.ts +11 -0
- package/dist/src/core/text-rendering/FontManager.js +41 -0
- package/dist/src/core/text-rendering/FontManager.js.map +1 -0
- package/dist/src/core/text-rendering/SdfFont.d.ts +29 -0
- package/dist/src/core/text-rendering/SdfFont.js +142 -0
- package/dist/src/core/text-rendering/SdfFont.js.map +1 -0
- package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +4 -6
- package/dist/src/core/text-rendering/SdfTextRenderer.js +87 -168
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
- package/dist/src/core/text-rendering/TextGenerator.js +36 -0
- package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
- package/dist/src/core/text-rendering/TextLayoutEngine.js +43 -12
- package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
- package/dist/src/core/text-rendering/TextRenderer.d.ts +41 -27
- package/dist/src/core/text-rendering/Utils.d.ts +2 -0
- package/dist/src/core/text-rendering/Utils.js +3 -0
- package/dist/src/core/text-rendering/Utils.js.map +1 -1
- package/dist/src/main-api/Inspector.d.ts +1 -1
- package/dist/src/main-api/Inspector.js +25 -20
- package/dist/src/main-api/Inspector.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +14 -0
- package/dist/src/main-api/Renderer.js +29 -3
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/core/CoreNode.test.ts +1 -1
- package/src/core/CoreNode.ts +37 -8
- package/src/core/CoreTextNode.test.ts +350 -0
- package/src/core/CoreTextNode.ts +201 -74
- package/src/core/CoreTextureManager.ts +14 -2
- package/src/core/Stage.ts +29 -0
- package/src/core/TextureMemoryManager.test.ts +134 -0
- package/src/core/TextureMemoryManager.ts +23 -30
- package/src/core/platforms/Platform.ts +5 -0
- package/src/core/platforms/web/WebPlatform.ts +13 -0
- package/src/core/renderers/CoreRenderer.ts +10 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
- package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +40 -31
- package/src/core/renderers/webgl/WebGlShaderProgram.test.ts +274 -0
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +7 -7
- package/src/core/text-rendering/CanvasFontHandler.ts +2 -2
- package/src/core/text-rendering/CanvasTextRenderer.ts +24 -45
- package/src/core/text-rendering/SdfTextRenderer.ts +106 -215
- package/src/core/text-rendering/TextLayoutEngine.ts +61 -28
- package/src/core/text-rendering/TextRenderer.ts +42 -33
- package/src/core/text-rendering/Utils.ts +5 -1
- package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +20 -0
- package/src/main-api/Inspector.ts +25 -25
- package/src/main-api/Renderer.test.ts +153 -0
- package/src/main-api/Renderer.ts +33 -3
- package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
- package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
- package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
- 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 {
|
|
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 {
|
|
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
|
-
|
|
70
|
-
if (props.text.length === 0) {
|
|
71
|
-
return {
|
|
72
|
-
width: 0,
|
|
73
|
-
height: 0,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
68
|
+
const cacheKey = getLayoutCacheKey(props);
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
287
|
+
* vertices for rendering quads in WebGL
|
|
288
|
+
*/
|
|
289
|
+
vertexBuffer: Float32Array;
|
|
290
|
+
/**
|
|
291
|
+
* glyph count in layout
|
|
304
292
|
*/
|
|
305
|
-
|
|
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
|
|
388
|
+
export interface RenderInfo {
|
|
385
389
|
width: number;
|
|
386
390
|
height: number;
|
|
387
|
-
hasRemainingText
|
|
388
|
-
remainingLines
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|