Keep descenders of a text whole and frame it at its drawn size
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:
1 parent
9445f39c8d
commit
d4ff652293
12 files changed
+284
-47
No files matched your search
@@ -63,7 +63,7 @@ export abstract class FreeViewport extends WorldViewport<EditorState> {
|
||||
protected frameContent(): void {
|
||||
const { items } = this.collectFrame();
|
||||
const boxes = items.map((item) => {
|
||||
const e = footprintExtent(item.type, item.props, item.m, item.frame);
|
||||
const e = footprintExtent(item.type, item.props, item.m, item.frame, item.font);
|
||||
return { id: item.id, x: e.left, y: e.top, w: e.right - e.left, h: e.bottom - e.top };
|
||||
});
|
||||
const { minX, minY, maxX, maxY } = boundsOf(boxes, { minX: 0, minY: 0, maxX: 0, maxY: 0 })!;
|
||||
|
||||
@@ -20,11 +20,13 @@ import {
|
||||
linearOf,
|
||||
type Mat,
|
||||
} from '../../core/model/matrix';
|
||||
import { fontFileOf } from '../../core/model/font';
|
||||
import { placementRejection } from '../../core/model/placement';
|
||||
import type { Position, Vec2 } from '../../core/model/Properties';
|
||||
import { findAsset, type ObjectHolder } from '../../core/model/Project';
|
||||
import { cellAtPoint } from '../../core/model/tilemap';
|
||||
import { footprintCorners, iconCorners } from '../../core/properties/bounds';
|
||||
import { fontOf } from '../../core/properties/fonts';
|
||||
import { localMatrixOf, parentTransform, transformOf } from '../../core/properties/transform';
|
||||
import { FramePreview, isDragFrame } from '../framePreview';
|
||||
import { panelPolicy } from '../playThrottle';
|
||||
@@ -1138,9 +1140,10 @@ export class SceneViewport extends FreeViewport {
|
||||
if (!preview || !asset) return;
|
||||
const color = preview.rejection ? Theme.colors.danger : Theme.colors.accent;
|
||||
const m = localMatrixOf(preview.at, transformOf(asset.props));
|
||||
const font = fontFileOf(this.state.project, fontOf(asset.props));
|
||||
const centre = worldToScreen(view, preview.at.x, preview.at.y);
|
||||
this.gizmos
|
||||
.poly(this.screenCorners(footprintCorners(asset.type, asset.props, m), view), true)
|
||||
.poly(this.screenCorners(footprintCorners(asset.type, asset.props, m, 0, null, font), view), true)
|
||||
.fill({ color, alpha: 0.12 })
|
||||
.stroke({ width: 2, color });
|
||||
this.gizmos
|
||||
@@ -1216,7 +1219,7 @@ export class SceneViewport extends FreeViewport {
|
||||
return this.screenCorners(
|
||||
spot
|
||||
? iconCorners(item.type, item.props, spot, this.symbolScale(), pad / view.scale)
|
||||
: footprintCorners(item.type, item.props, item.m, pad / view.scale, item.frame),
|
||||
: footprintCorners(item.type, item.props, item.m, pad / view.scale, item.frame, item.font),
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export class SizeGizmoEditor {
|
||||
const frame = contentFrame(item.m, this.contentCorners(item.id));
|
||||
if (frame) return frame;
|
||||
}
|
||||
return footprintCorners(item.type, item.props, item.m, 0, item.frame);
|
||||
return footprintCorners(item.type, item.props, item.m, 0, item.frame, item.font);
|
||||
}
|
||||
|
||||
/** The corners of everything below a container that has a shape of its own, at any depth. */
|
||||
@@ -157,7 +157,7 @@ export class SizeGizmoEditor {
|
||||
};
|
||||
return items
|
||||
.filter((i) => !SHAPELESS.has(i.type) && below(i))
|
||||
.flatMap((i) => footprintCorners(i.type, i.props, i.m, 0, i.frame));
|
||||
.flatMap((i) => footprintCorners(i.type, i.props, i.m, 0, i.frame, i.font));
|
||||
}
|
||||
|
||||
private screenBox(item: DrawItem, view: View): { corners: Vec2[]; base: Vec2; knob: Vec2 } {
|
||||
@@ -184,7 +184,7 @@ export class SizeGizmoEditor {
|
||||
e.stopPropagation();
|
||||
const tree = resolvedHolder(state.project, holder);
|
||||
const parent = parentTransform(state.project, tree, ref.object.parentId);
|
||||
const size = footprintOf(item.type, item.props, item.frame);
|
||||
const size = footprintOf(item.type, item.props, item.frame, item.font);
|
||||
const overrides: Record<string, PropertyValue | undefined> = {};
|
||||
for (const key of ['width', 'height', 'radius', 'rotation', 'scale'])
|
||||
overrides[key] = ref.object.overrides[key];
|
||||
|
||||
Reference in new issue
Block a user