@solidtv/renderer 1.5.6 → 1.6.0
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/Stage.js +9 -1
- 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/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/Stage.renderListUpdate.test.ts +73 -0
- package/src/core/Stage.ts +11 -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/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/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,
|