Skip to content

Commit 534bfa1

Browse files
chiefcllclaude
andauthored
perf(text): fast path for single-space separators in wrapLine + tests for #128 (#130)
PR #128 made wrapLine measure the real separator string so multi-space runs are counted at their true width, but that added a measureText call per word gap even for the dominant single ' ' case — on the Canvas backend that's a ctx.measureText per gap during layout. Reuse the precomputed spaceWidth when the separator is exactly one space and only measure multi-char runs. Also adds the unit tests #128 landed without: multi-space separators wrap at their real width and are preserved when the line fits, ZWSP runs stay zero-width, and the single-space path uses the precomputed width. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f1d0009 commit 534bfa1

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

src/core/text-rendering/TextLayoutEngine.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,9 +461,14 @@ export const wrapLine = (
461461
continue;
462462
}
463463
const space = spaces[spaceIdx++] || '';
464-
// Measure the real separator \u2014 multi-space runs collapse into one match.
464+
// Single ' ' is the dominant separator \u2014 reuse the precomputed width.
465+
// Multi-space/ZWSP runs collapse into one match; measure those for real.
465466
const effectiveSpaceWidth =
466-
space.length > 0 ? measureText(space, fontFamily, letterSpacing) : 0;
467+
space === ' '
468+
? spaceWidth
469+
: space.length > 0
470+
? measureText(space, fontFamily, letterSpacing)
471+
: 0;
467472
const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
468473

469474
if (totalWidth < maxWidth) {

src/core/text-rendering/tests/TextLayoutEngine.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,83 @@ describe('SDF Text Utils', () => {
270270
expect(lines).toHaveLength(1);
271271
expect(lines[0]?.[0]).toBe(' hello world');
272272
});
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+
});
273350
});
274351

275352
describe('wrapText', () => {

0 commit comments

Comments
 (0)