@solidtv/renderer 1.5.6 → 1.6.1
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/dist/src/core/CoreNode.js +2 -2
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.d.ts +31 -0
- package/dist/src/core/CoreTextureManager.js +87 -6
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.js +11 -2
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +17 -1
- package/dist/src/core/lib/WebGlContextWrapper.js +21 -0
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/colorParser.d.ts +4 -1
- package/dist/src/core/lib/colorParser.js +16 -6
- package/dist/src/core/lib/colorParser.js.map +1 -1
- package/dist/src/core/platforms/web/WebPlatform.js +60 -50
- package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +4 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +28 -13
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js +1 -0
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js.map +1 -1
- package/dist/src/core/renderers/webgl/SdfRenderOp.js +2 -2
- package/dist/src/core/renderers/webgl/SdfRenderOp.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +43 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +109 -10
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +21 -13
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +8 -3
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.js +12 -11
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
- package/dist/src/core/shaders/canvas/Rounded.js +3 -3
- package/dist/src/core/shaders/canvas/Rounded.js.map +1 -1
- package/dist/src/core/shaders/canvas/RoundedWithShadow.js +3 -3
- package/dist/src/core/shaders/canvas/RoundedWithShadow.js.map +1 -1
- package/dist/src/core/shaders/canvas/utils/render.js +3 -3
- package/dist/src/core/shaders/canvas/utils/render.js.map +1 -1
- package/dist/src/core/shaders/webgl/Border.js +12 -11
- package/dist/src/core/shaders/webgl/Border.js.map +1 -1
- package/dist/src/core/shaders/webgl/HolePunch.js +12 -5
- package/dist/src/core/shaders/webgl/HolePunch.js.map +1 -1
- package/dist/src/core/shaders/webgl/LinearGradient.js +3 -19
- package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialGradient.js +3 -20
- package/dist/src/core/shaders/webgl/RadialGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialProgress.js +15 -23
- package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithBorder.js +35 -32
- package/dist/src/core/shaders/webgl/RoundedWithBorder.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js +58 -45
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithShadow.js +15 -13
- package/dist/src/core/shaders/webgl/RoundedWithShadow.js.map +1 -1
- package/dist/src/core/shaders/webgl/Shadow.js +4 -5
- package/dist/src/core/shaders/webgl/Shadow.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +23 -10
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.js +14 -2
- package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +28 -5
- package/dist/src/main-api/Renderer.js +5 -1
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/src/utils.js +1 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.ts +2 -2
- package/src/core/CoreTextureManager.test.ts +147 -0
- package/src/core/CoreTextureManager.ts +97 -8
- package/src/core/Stage.renderListUpdate.test.ts +73 -0
- package/src/core/Stage.ts +13 -1
- package/src/core/lib/WebGlContextWrapper.test.ts +23 -1
- package/src/core/lib/WebGlContextWrapper.ts +23 -1
- package/src/core/lib/colorParser.test.ts +46 -0
- package/src/core/lib/colorParser.ts +17 -6
- package/src/core/platforms/web/WebPlatform.ts +63 -52
- package/src/core/renderers/canvas/CanvasRenderer.test.ts +230 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +29 -15
- package/src/core/renderers/canvas/CanvasShaderNode.test.ts +73 -0
- package/src/core/renderers/canvas/CanvasShaderNode.ts +1 -0
- package/src/core/renderers/webgl/SdfRenderOp.ts +2 -2
- package/src/core/renderers/webgl/WebGlRenderer.reuseRenderOp.test.ts +122 -0
- package/src/core/renderers/webgl/WebGlRenderer.sdfBuffer.test.ts +302 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +151 -11
- package/src/core/renderers/webgl/WebGlShaderProgram.reuseRenderOp.test.ts +150 -0
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +27 -15
- package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +4 -2
- package/src/core/renderers/webgl/internal/ShaderUtils.test.ts +124 -0
- package/src/core/renderers/webgl/internal/ShaderUtils.ts +14 -13
- package/src/core/shaders/canvas/Rounded.ts +3 -3
- package/src/core/shaders/canvas/RoundedWithShadow.ts +3 -3
- package/src/core/shaders/canvas/utils/render.ts +3 -3
- package/src/core/shaders/webgl/Border.ts +12 -11
- package/src/core/shaders/webgl/HolePunch.ts +12 -5
- package/src/core/shaders/webgl/LinearGradient.ts +3 -19
- package/src/core/shaders/webgl/RadialGradient.ts +3 -20
- package/src/core/shaders/webgl/RadialProgress.ts +15 -23
- package/src/core/shaders/webgl/RoundedWithBorder.ts +35 -32
- package/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +58 -45
- package/src/core/shaders/webgl/RoundedWithShadow.ts +15 -13
- package/src/core/shaders/webgl/Shadow.ts +4 -5
- package/src/core/text-rendering/SdfTextRenderer.test.ts +177 -0
- package/src/core/text-rendering/SdfTextRenderer.ts +38 -10
- package/src/core/text-rendering/TextLayoutEngine.ts +15 -2
- package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +99 -0
- package/src/main-api/Renderer.ts +35 -6
- package/src/utils.ts +1 -1
|
@@ -133,6 +133,183 @@ describe('SdfTextRenderer layout cache', () => {
|
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
describe('SdfTextRenderer renderQuads cache dispatch', () => {
|
|
137
|
+
type FakeRenderer = {
|
|
138
|
+
stage: unknown;
|
|
139
|
+
sdfBufferIdx: number;
|
|
140
|
+
fSdfBuffer: Float32Array;
|
|
141
|
+
addSdfQuads: ReturnType<typeof vi.fn>;
|
|
142
|
+
addSdfCachedQuads: ReturnType<typeof vi.fn>;
|
|
143
|
+
addSdfTranslatedQuads: ReturnType<typeof vi.fn>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const GLYPHS = 2;
|
|
147
|
+
|
|
148
|
+
const makeRenderer = (): FakeRenderer => {
|
|
149
|
+
const renderer: FakeRenderer = {
|
|
150
|
+
stage: {
|
|
151
|
+
options: { textLayoutCacheSize: 10 },
|
|
152
|
+
shManager: {
|
|
153
|
+
registerShaderType: vi.fn(),
|
|
154
|
+
createShader: vi.fn(() => ({})),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
sdfBufferIdx: 0,
|
|
158
|
+
fSdfBuffer: new Float32Array(1024),
|
|
159
|
+
// The miss path snapshots [startIdx, endIdx) of fSdfBuffer, so the
|
|
160
|
+
// fake must advance sdfBufferIdx like the real writer does.
|
|
161
|
+
addSdfQuads: vi.fn(() => {
|
|
162
|
+
renderer.sdfBufferIdx += GLYPHS * 24;
|
|
163
|
+
}),
|
|
164
|
+
addSdfCachedQuads: vi.fn(),
|
|
165
|
+
addSdfTranslatedQuads: vi.fn(),
|
|
166
|
+
};
|
|
167
|
+
return renderer;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const makeLayout = () => ({
|
|
171
|
+
glyphs: new Float32Array(GLYPHS * 8),
|
|
172
|
+
glyphCount: GLYPHS,
|
|
173
|
+
fontScale: 1,
|
|
174
|
+
distanceRange: 4,
|
|
175
|
+
width: 100,
|
|
176
|
+
height: 50,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const makeCache = () => ({
|
|
180
|
+
vertices: null as Float32Array | null,
|
|
181
|
+
glyphCount: 0,
|
|
182
|
+
color: 0,
|
|
183
|
+
alpha: -1,
|
|
184
|
+
transform: new Float32Array(6),
|
|
185
|
+
layoutRef: null as unknown,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const makeRenderProps = (
|
|
189
|
+
cache: ReturnType<typeof makeCache>,
|
|
190
|
+
transform: number[],
|
|
191
|
+
) => ({
|
|
192
|
+
fontFamily: 'Test',
|
|
193
|
+
fontSize: 42,
|
|
194
|
+
color: 0xffffffff,
|
|
195
|
+
offsetY: 0,
|
|
196
|
+
worldAlpha: 1,
|
|
197
|
+
globalTransform: new Float32Array(transform),
|
|
198
|
+
clippingRect: { x: 0, y: 0, w: 0, h: 0, valid: false },
|
|
199
|
+
width: 100,
|
|
200
|
+
height: 50,
|
|
201
|
+
parentHasRenderTexture: false,
|
|
202
|
+
framebufferDimensions: null,
|
|
203
|
+
sdfCache: cache,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
207
|
+
const TRANSLATED = [1, 0, 0, 0, 1, 0, 120, -40, 1];
|
|
208
|
+
const SCALED = [2, 0, 0, 0, 2, 0, 0, 0, 1];
|
|
209
|
+
|
|
210
|
+
const renderQuads = (
|
|
211
|
+
renderer: FakeRenderer,
|
|
212
|
+
layout: ReturnType<typeof makeLayout>,
|
|
213
|
+
renderProps: ReturnType<typeof makeRenderProps>,
|
|
214
|
+
) =>
|
|
215
|
+
SdfTextRenderer.renderQuads(
|
|
216
|
+
renderer as never,
|
|
217
|
+
layout as never,
|
|
218
|
+
null as never,
|
|
219
|
+
renderProps as never,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const setupWithAtlas = () => {
|
|
223
|
+
initRenderer(10);
|
|
224
|
+
vi.mocked(SdfFontHandler.getAtlas).mockReturnValue({
|
|
225
|
+
ctxTexture: {},
|
|
226
|
+
} as never);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
it('takes the miss path first and snapshots the cache', () => {
|
|
230
|
+
setupWithAtlas();
|
|
231
|
+
const renderer = makeRenderer();
|
|
232
|
+
const layout = makeLayout();
|
|
233
|
+
const cache = makeCache();
|
|
234
|
+
|
|
235
|
+
renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
|
|
236
|
+
|
|
237
|
+
expect(renderer.addSdfQuads.mock.calls.length).toBe(1);
|
|
238
|
+
expect(cache.vertices).not.toBeNull();
|
|
239
|
+
expect(cache.layoutRef).toBe(layout);
|
|
240
|
+
expect(cache.glyphCount).toBe(GLYPHS);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('takes the mem-copy path when nothing changed', () => {
|
|
244
|
+
setupWithAtlas();
|
|
245
|
+
const renderer = makeRenderer();
|
|
246
|
+
const layout = makeLayout();
|
|
247
|
+
const cache = makeCache();
|
|
248
|
+
|
|
249
|
+
renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
|
|
250
|
+
renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
|
|
251
|
+
|
|
252
|
+
expect(renderer.addSdfQuads.mock.calls.length).toBe(1);
|
|
253
|
+
expect(renderer.addSdfCachedQuads.mock.calls.length).toBe(1);
|
|
254
|
+
expect(renderer.addSdfTranslatedQuads.mock.calls.length).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('takes the translation path when only tx/ty changed, keeping the cache base', () => {
|
|
258
|
+
setupWithAtlas();
|
|
259
|
+
const renderer = makeRenderer();
|
|
260
|
+
const layout = makeLayout();
|
|
261
|
+
const cache = makeCache();
|
|
262
|
+
|
|
263
|
+
renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
|
|
264
|
+
const baseVertices = cache.vertices;
|
|
265
|
+
|
|
266
|
+
renderQuads(renderer, layout, makeRenderProps(cache, TRANSLATED));
|
|
267
|
+
|
|
268
|
+
expect(renderer.addSdfQuads.mock.calls.length).toBe(1);
|
|
269
|
+
expect(renderer.addSdfCachedQuads.mock.calls.length).toBe(0);
|
|
270
|
+
const calls = renderer.addSdfTranslatedQuads.mock.calls;
|
|
271
|
+
expect(calls.length).toBe(1);
|
|
272
|
+
// (cachedVertices, glyphCount, dx, dy, ...)
|
|
273
|
+
expect(calls[0]![0]).toBe(baseVertices);
|
|
274
|
+
expect(calls[0]![1]).toBe(GLYPHS);
|
|
275
|
+
expect(calls[0]![2]).toBe(120);
|
|
276
|
+
expect(calls[0]![3]).toBe(-40);
|
|
277
|
+
// The scroll path must not re-snapshot: base transform and vertices stay
|
|
278
|
+
expect(cache.vertices).toBe(baseVertices);
|
|
279
|
+
expect(cache.transform[4]).toBe(0);
|
|
280
|
+
expect(cache.transform[5]).toBe(0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('falls back to the miss path when scale or rotation changed', () => {
|
|
284
|
+
setupWithAtlas();
|
|
285
|
+
const renderer = makeRenderer();
|
|
286
|
+
const layout = makeLayout();
|
|
287
|
+
const cache = makeCache();
|
|
288
|
+
|
|
289
|
+
renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
|
|
290
|
+
renderQuads(renderer, layout, makeRenderProps(cache, SCALED));
|
|
291
|
+
|
|
292
|
+
expect(renderer.addSdfQuads.mock.calls.length).toBe(2);
|
|
293
|
+
expect(renderer.addSdfTranslatedQuads.mock.calls.length).toBe(0);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('falls back to the miss path when color or alpha changed', () => {
|
|
297
|
+
setupWithAtlas();
|
|
298
|
+
const renderer = makeRenderer();
|
|
299
|
+
const layout = makeLayout();
|
|
300
|
+
const cache = makeCache();
|
|
301
|
+
|
|
302
|
+
renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
|
|
303
|
+
|
|
304
|
+
const faded = makeRenderProps(cache, TRANSLATED);
|
|
305
|
+
faded.worldAlpha = 0.5;
|
|
306
|
+
renderQuads(renderer, layout, faded);
|
|
307
|
+
|
|
308
|
+
expect(renderer.addSdfQuads.mock.calls.length).toBe(2);
|
|
309
|
+
expect(renderer.addSdfTranslatedQuads.mock.calls.length).toBe(0);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
136
313
|
describe('SdfTextRenderer lazy shader compile', () => {
|
|
137
314
|
it('registers the SDF shader at init but defers compilation', () => {
|
|
138
315
|
const shManager = {
|
|
@@ -150,11 +150,14 @@ const addQuads = (_layout?: TextLayout): Float32Array | null => {
|
|
|
150
150
|
/**
|
|
151
151
|
* Submit SDF glyphs to the renderer's shared batched buffer.
|
|
152
152
|
*
|
|
153
|
-
*
|
|
154
|
-
* 1. **
|
|
155
|
-
* The cached pre-transformed Float32Array is mem-copied directly
|
|
156
|
-
* shared SDF buffer (no per-glyph matrix math).
|
|
157
|
-
* 2. **
|
|
153
|
+
* Three paths:
|
|
154
|
+
* 1. **Exact cache hit** — layout, transform, color, and alpha haven't
|
|
155
|
+
* changed. The cached pre-transformed Float32Array is mem-copied directly
|
|
156
|
+
* into the shared SDF buffer (no per-glyph matrix math).
|
|
157
|
+
* 2. **Translation hit** — only tx/ty changed (the scroll path). The cached
|
|
158
|
+
* vertices are copied with the position delta applied; the cache keeps its
|
|
159
|
+
* original base so nothing is re-snapshotted.
|
|
160
|
+
* 3. **Cache miss** — re-computes per-glyph world-space vertices via
|
|
158
161
|
* `addSdfQuads`, then snapshots the result into the cache.
|
|
159
162
|
*/
|
|
160
163
|
const renderQuads = (
|
|
@@ -176,7 +179,7 @@ const renderQuads = (
|
|
|
176
179
|
// Compiles on the first real SDF draw; cheap memoized lookup thereafter.
|
|
177
180
|
const shader = getSdfShader(webGlRenderer.stage);
|
|
178
181
|
|
|
179
|
-
// --- Cache-hit fast
|
|
182
|
+
// --- Cache-hit fast paths -----------------------------------------------
|
|
180
183
|
if (cache !== undefined && cache.vertices !== null) {
|
|
181
184
|
const ct = cache.transform;
|
|
182
185
|
const t = renderProps.globalTransform;
|
|
@@ -187,13 +190,38 @@ const renderQuads = (
|
|
|
187
190
|
ct[0] === t[0] &&
|
|
188
191
|
ct[1] === t[1] &&
|
|
189
192
|
ct[2] === t[3] &&
|
|
190
|
-
ct[3] === t[4]
|
|
191
|
-
ct[4] === t[6] &&
|
|
192
|
-
ct[5] === t[7]
|
|
193
|
+
ct[3] === t[4]
|
|
193
194
|
) {
|
|
194
|
-
|
|
195
|
+
const dx = t[6]! - ct[4]!;
|
|
196
|
+
const dy = t[7]! - ct[5]!;
|
|
197
|
+
|
|
198
|
+
if (dx === 0 && dy === 0) {
|
|
199
|
+
// Fully static: mem-copy the cached vertices as-is.
|
|
200
|
+
webGlRenderer.addSdfCachedQuads(
|
|
201
|
+
cache.vertices,
|
|
202
|
+
cache.glyphCount,
|
|
203
|
+
ctxTexture,
|
|
204
|
+
renderProps.clippingRect,
|
|
205
|
+
renderProps.worldAlpha,
|
|
206
|
+
layout.width,
|
|
207
|
+
layout.height,
|
|
208
|
+
renderProps.parentHasRenderTexture,
|
|
209
|
+
renderProps.framebufferDimensions,
|
|
210
|
+
shader,
|
|
211
|
+
);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Pure translation (the scroll path): same glyphs, same scale/rotation,
|
|
216
|
+
// only tx/ty moved. Copy the cached vertices shifted by the delta from
|
|
217
|
+
// the cached base transform. The cache keeps its original base, so
|
|
218
|
+
// every frame recomputes from the same reference — no drift and no
|
|
219
|
+
// per-frame re-snapshot.
|
|
220
|
+
webGlRenderer.addSdfTranslatedQuads(
|
|
195
221
|
cache.vertices,
|
|
196
222
|
cache.glyphCount,
|
|
223
|
+
dx,
|
|
224
|
+
dy,
|
|
197
225
|
ctxTexture,
|
|
198
226
|
renderProps.clippingRect,
|
|
199
227
|
renderProps.worldAlpha,
|
|
@@ -368,6 +368,13 @@ export const wrapLine = (
|
|
|
368
368
|
): WrappedLinesStruct => {
|
|
369
369
|
const words = line.split(spaceRegex);
|
|
370
370
|
const spaces = line.match(spaceRegex) || [];
|
|
371
|
+
// Leading whitespace splits into an empty token; fold it into the next
|
|
372
|
+
// word so it isn't dropped and re-applied after the first word instead.
|
|
373
|
+
if (words[0] === '' && words.length > 1) {
|
|
374
|
+
const leadingSpace = spaces.shift() ?? '';
|
|
375
|
+
words.shift();
|
|
376
|
+
words[0] = leadingSpace + (words[0] ?? '');
|
|
377
|
+
}
|
|
371
378
|
const wrappedLines: TextLineStruct[] = [];
|
|
372
379
|
let currentLine = '';
|
|
373
380
|
let currentLineWidth = 0;
|
|
@@ -454,8 +461,14 @@ export const wrapLine = (
|
|
|
454
461
|
continue;
|
|
455
462
|
}
|
|
456
463
|
const space = spaces[spaceIdx++] || '';
|
|
457
|
-
//
|
|
458
|
-
|
|
464
|
+
// Single ' ' is the dominant separator \u2014 reuse the precomputed width.
|
|
465
|
+
// Multi-space/ZWSP runs collapse into one match; measure those for real.
|
|
466
|
+
const effectiveSpaceWidth =
|
|
467
|
+
space === ' '
|
|
468
|
+
? spaceWidth
|
|
469
|
+
: space.length > 0
|
|
470
|
+
? measureText(space, fontFamily, letterSpacing)
|
|
471
|
+
: 0;
|
|
459
472
|
const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
|
|
460
473
|
|
|
461
474
|
if (totalWidth < maxWidth) {
|
|
@@ -248,6 +248,105 @@ describe('SDF Text Utils', () => {
|
|
|
248
248
|
expect(result[0][0]?.[2]).toBe(true);
|
|
249
249
|
expect(result[1]).toBe(0);
|
|
250
250
|
});
|
|
251
|
+
|
|
252
|
+
it('should keep leading whitespace attached to the first word, not the word after it', () => {
|
|
253
|
+
// Regression test: a line starting with padding spaces (used e.g. to
|
|
254
|
+
// reserve room for an inline badge before the first word) used to have
|
|
255
|
+
// those spaces silently reassigned as the gap AFTER the first word.
|
|
256
|
+
const result = wrapLine(
|
|
257
|
+
testMeasureText,
|
|
258
|
+
' hello world',
|
|
259
|
+
'Arial',
|
|
260
|
+
1000,
|
|
261
|
+
0,
|
|
262
|
+
10, // spaceWidth
|
|
263
|
+
'',
|
|
264
|
+
0,
|
|
265
|
+
'break-word',
|
|
266
|
+
10,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const [lines] = result;
|
|
270
|
+
expect(lines).toHaveLength(1);
|
|
271
|
+
expect(lines[0]?.[0]).toBe(' hello world');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should count the full width of a multi-space separator when wrapping', () => {
|
|
275
|
+
// 'aa bb' really measures 60 (6 chars * 10). Counting the separator
|
|
276
|
+
// as a single spaceWidth (50) used to keep it on one line past maxWidth.
|
|
277
|
+
const result = wrapLine(
|
|
278
|
+
testMeasureText,
|
|
279
|
+
'aa bb',
|
|
280
|
+
'Arial',
|
|
281
|
+
55, // maxWidth — below the real 60, above the buggy 50
|
|
282
|
+
0,
|
|
283
|
+
10, // spaceWidth
|
|
284
|
+
'',
|
|
285
|
+
0,
|
|
286
|
+
'break-word',
|
|
287
|
+
10,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const [lines] = result;
|
|
291
|
+
expect(lines).toHaveLength(2);
|
|
292
|
+
expect(lines[0]).toEqual(['aa', 20, false, 0, 0]);
|
|
293
|
+
expect(lines[1]).toEqual(['bb', 20, false, 0, 0]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should preserve a multi-space separator and its width when the line fits', () => {
|
|
297
|
+
const result = wrapLine(
|
|
298
|
+
testMeasureText,
|
|
299
|
+
'aa bb',
|
|
300
|
+
'Arial',
|
|
301
|
+
1000,
|
|
302
|
+
0,
|
|
303
|
+
10, // spaceWidth
|
|
304
|
+
'',
|
|
305
|
+
0,
|
|
306
|
+
'break-word',
|
|
307
|
+
10,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(result[0][0]).toEqual(['aa bb', 60, false, 0, 0]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should treat a run of multiple ZWSPs as a zero-width separator', () => {
|
|
314
|
+
const result = wrapLine(
|
|
315
|
+
testMeasureText,
|
|
316
|
+
'aa\u200B\u200Bbb',
|
|
317
|
+
'Arial',
|
|
318
|
+
1000,
|
|
319
|
+
0,
|
|
320
|
+
10, // spaceWidth
|
|
321
|
+
'',
|
|
322
|
+
0,
|
|
323
|
+
'break-word',
|
|
324
|
+
10,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Zero-width separator: no visible gap added, width is just the glyphs
|
|
328
|
+
expect(result[0][0]).toEqual(['aabb', 40, false, 0, 0]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should use the precomputed spaceWidth for a single-space separator', () => {
|
|
332
|
+
// spaceWidth (4) deliberately differs from testMeasureText(' ') (10):
|
|
333
|
+
// the single-space fast path must reuse the precomputed value instead
|
|
334
|
+
// of re-measuring the separator.
|
|
335
|
+
const result = wrapLine(
|
|
336
|
+
testMeasureText,
|
|
337
|
+
'aa bb',
|
|
338
|
+
'Arial',
|
|
339
|
+
1000,
|
|
340
|
+
0,
|
|
341
|
+
4, // spaceWidth
|
|
342
|
+
'',
|
|
343
|
+
0,
|
|
344
|
+
'break-word',
|
|
345
|
+
10,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(result[0][0]).toEqual(['aa bb', 44, false, 0, 0]);
|
|
349
|
+
});
|
|
251
350
|
});
|
|
252
351
|
|
|
253
352
|
describe('wrapText', () => {
|
package/src/main-api/Renderer.ts
CHANGED
|
@@ -481,6 +481,26 @@ export type RendererMainSettings = RendererRuntimeSettings & {
|
|
|
481
481
|
*/
|
|
482
482
|
numImageWorkers: number;
|
|
483
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Maximum number of image fetch+decode operations allowed to run at once
|
|
486
|
+
* when image workers are unavailable
|
|
487
|
+
*
|
|
488
|
+
* @remarks
|
|
489
|
+
* Only applies when there is no image worker manager (i.e.
|
|
490
|
+
* `numImageWorkers === 0`, or workers/`createImageBitmap` are unsupported).
|
|
491
|
+
* In that mode every image decode runs on the main thread, so a burst — such
|
|
492
|
+
* as a scroll that makes many image nodes renderable in a single tick — can
|
|
493
|
+
* fire dozens of decodes back-to-back and starve the render loop. This caps
|
|
494
|
+
* how many run concurrently; on-screen (priority) textures bypass the cap so
|
|
495
|
+
* they never wait behind off-screen prefetch.
|
|
496
|
+
*
|
|
497
|
+
* Has no effect when image workers are active — the worker pool already
|
|
498
|
+
* bounds concurrency by its size. Set to `0` to disable the cap (unbounded).
|
|
499
|
+
*
|
|
500
|
+
* @defaultValue `4`
|
|
501
|
+
*/
|
|
502
|
+
imageDecodeConcurrency: number;
|
|
503
|
+
|
|
484
504
|
/**
|
|
485
505
|
* Renderer Engine
|
|
486
506
|
*
|
|
@@ -610,13 +630,17 @@ export type RendererMainSettings = RendererRuntimeSettings & {
|
|
|
610
630
|
* - Full - Supports createImageBitmap(image, sx, sy, sw, sh, options)
|
|
611
631
|
*
|
|
612
632
|
* Note with auto detection, the renderer will attempt to use the most advanced
|
|
613
|
-
* version of the API available. If the API is not available
|
|
614
|
-
*
|
|
633
|
+
* version of the API available. If the API is not available (e.g. Chrome < 50,
|
|
634
|
+
* including the Chrome 38 support floor), the renderer falls back to loading
|
|
635
|
+
* images via `new Image()`.
|
|
615
636
|
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
637
|
+
* `'auto'` runs a small startup probe (a 1x1 PNG) to determine support; this
|
|
638
|
+
* adds a negligible one-time startup cost. Set an explicit value (`'full'`,
|
|
639
|
+
* `'options'`, `'basic'`) to skip the probe ONLY when the target runtime is
|
|
640
|
+
* known to support that level — forcing a level the runtime lacks will fail
|
|
641
|
+
* to load images. When in doubt, use `'auto'`.
|
|
618
642
|
*
|
|
619
|
-
* @defaultValue `
|
|
643
|
+
* @defaultValue `auto`
|
|
620
644
|
*/
|
|
621
645
|
createImageBitmapSupport: 'auto' | 'basic' | 'options' | 'full';
|
|
622
646
|
|
|
@@ -781,6 +805,10 @@ export class RendererMain extends EventEmitter {
|
|
|
781
805
|
textLayoutCacheSize: settings.textLayoutCacheSize ?? 250,
|
|
782
806
|
numImageWorkers:
|
|
783
807
|
settings.numImageWorkers !== undefined ? settings.numImageWorkers : 2,
|
|
808
|
+
imageDecodeConcurrency:
|
|
809
|
+
settings.imageDecodeConcurrency !== undefined
|
|
810
|
+
? settings.imageDecodeConcurrency
|
|
811
|
+
: 4,
|
|
784
812
|
enableContextSpy: settings.enableContextSpy ?? false,
|
|
785
813
|
forceWebGL2: settings.forceWebGL2 ?? false,
|
|
786
814
|
disableVertexArrayObject: settings.disableVertexArrayObject ?? false,
|
|
@@ -792,7 +820,7 @@ export class RendererMain extends EventEmitter {
|
|
|
792
820
|
textBaselineMode: settings.textBaselineMode ?? 'optical',
|
|
793
821
|
textureProcessingTimeLimit: settings.textureProcessingTimeLimit || 10,
|
|
794
822
|
canvas: settings.canvas,
|
|
795
|
-
createImageBitmapSupport: settings.createImageBitmapSupport || '
|
|
823
|
+
createImageBitmapSupport: settings.createImageBitmapSupport || 'auto',
|
|
796
824
|
// undefined -> true (assume honored, no probe); 'auto' -> probe;
|
|
797
825
|
// explicit boolean -> force the value.
|
|
798
826
|
premultiplyAlphaHonored:
|
|
@@ -854,6 +882,7 @@ export class RendererMain extends EventEmitter {
|
|
|
854
882
|
fpsUpdateInterval: settings.fpsUpdateInterval!,
|
|
855
883
|
enableClear: settings.enableClear!,
|
|
856
884
|
numImageWorkers: settings.numImageWorkers!,
|
|
885
|
+
imageDecodeConcurrency: settings.imageDecodeConcurrency!,
|
|
857
886
|
renderEngine: settings.renderEngine!,
|
|
858
887
|
textureMemory: resolvedTxSettings,
|
|
859
888
|
eventBus: this,
|
package/src/utils.ts
CHANGED
|
@@ -9,7 +9,7 @@ export function createWebGLContext(
|
|
|
9
9
|
alpha: true,
|
|
10
10
|
antialias: false,
|
|
11
11
|
depth: false,
|
|
12
|
-
stencil:
|
|
12
|
+
stencil: false,
|
|
13
13
|
desynchronized: false,
|
|
14
14
|
// Disabled because it prevents Visual Regression Tests from working
|
|
15
15
|
// failIfMajorPerformanceCaveat: true,
|