Keep descenders of a text whole and frame it at its drawn size
feuerwerk / gate (push) Successful in 5m21s
feuerwerk / suite (push) Successful in 18m31s

A text was drawn into a texture as tall as Pixi measures the string
"|ÉqÅM". Whatever reached further, like the "g" of Manrope (0.015 em
below the "q"), was cut off at the bottom. The blue frame around a
text was only estimated from the font size and the number of letters,
so it was far too wide for most texts.

- textPicture.ts takes the height of a line from the font itself
  (fontBoundingBoxAscent and fontBoundingBoxDescent, as a browser
  does). Pixi draws with a margin, and the picture is cut to that box
  afterwards. Where Pixi's own box is larger, Pixi's box wins.
- Both raster paths (linear and nearest) use the cut.
- The height no longer depends on the letters typed, so a text does
  not jump while it is edited.
- bounds.ts asks a measurer that textPicture.ts registers
  (setTextMeasure). Selection, picking in a running game, framing the
  camera, the drop preview and culling all get the measured size.
  The footprint functions take the font file for this.
- Unit tests in node have no canvas and keep the estimate
  (estimatedTextSize).
- Texts get taller: Source Sans 3 at 48 px is 68 instead of 55 px,
  Manrope is 1.366 em instead of 1.25 em. The glyphs stay in place,
  the centre moves by less than half a pixel. A shader on a text
  gets a taller UV rectangle.

Tests:
- Gate on melektaus-atx green: 307 files, 4762 tests.
- 5 new unit tests for textBox, textCut and the measurer hook.
- e2e 30: 26 checks, 30f-iii new. The texture is 68 high, the font
  box is 68, the frame equals the drawn picture (237x68).
- Counter test: with Pixi's old box the texture is 55 high and
  30f-iii is red.
- Not checked in the browser: the Manrope caption of the user and
  texts with several lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-13 15:18:59 +02:00
1 parent 9445f39c8d
commit d4ff652293
12 files changed
+284 -47

No files matched your search

+24 -2
View File
@@ -2,11 +2,12 @@ import { describe, expect, it } from 'vitest';
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
import { AddColliderCommand } from '../../src/core/commands/colliderCommands';
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { footprintOf, textSize } from '../../src/core/properties/bounds';
import { footprintContains, footprintOf, setTextMeasure, textSize } from '../../src/core/properties/bounds';
import { IDENTITY } from '../../src/core/model/matrix';
import { makeSpriteScene, place, startWorld } from '../helpers';
describe('text objects', () => {
it('measures a text footprint from its content and font size', () => {
it('estimates a text footprint from its content and font size where nothing measures', () => {
expect(textSize({ text: 'ab', fontSize: 100 }).w).toBeCloseTo(110, 5);
expect(textSize({ text: 'ab', fontSize: 100 }).h).toBeCloseTo(125, 5);
expect(footprintOf('text', { text: 'abcd', fontSize: 100 }).w).toBeCloseTo(220, 5);
@@ -17,6 +18,27 @@ describe('text objects', () => {
expect(textSize({ text: 'abcd', fontSize: 100, letterSpacing: -6 }).w).toBeCloseTo(202, 5);
expect(textSize({ text: 'a', fontSize: 100, letterSpacing: 10 }).w).toBeCloseTo(55, 5);
});
it('takes the measured size, in the text’s own font, once something measures', () => {
const asked: string[] = [];
setTextMeasure((props, font) => {
asked.push(font);
return props.text === 'unmeasured' ? null : { w: 300, h: 80 };
});
try {
expect(footprintOf('text', { text: 'Ag', fontSize: 100 }, null, 'face.ttf')).toEqual({ w: 300, h: 80 });
expect(
footprintContains('text', { text: 'Ag', align: 'center' }, IDENTITY, { x: 140, y: 0 }, 'a.ttf'),
).toBe(true);
expect(footprintContains('text', { text: 'Ag', align: 'center' }, IDENTITY, { x: 0, y: 45 })).toBe(
false,
);
expect(asked).toEqual(['face.ttf', 'a.ttf', '']);
expect(textSize({ text: 'unmeasured', fontSize: 100 }).h).toBeCloseTo(125, 5);
} finally {
setTextMeasure(null);
}
});
});
describe('the mouse', () => {