From dded62b9f92691c0f203a794ab1d08e0fe07f164 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Tue, 29 Sep 2026 15:19:42 +0200 Subject: [PATCH] Let the example-game smokes read texts in the game again Since 89aa92d (#169) a text in the game is drawn as a picture of its glyphs on a sprite mesh, not as a Pixi `Text`. The test hook `__gm.locate` only looked for `Text` nodes, so every game text was invisible to it. The smokes have been red in CI since then (tasks 385 to 395): 5 checks in jumprun, 1 in tetris. The e2e part stayed green, because it reads editor labels only. - `textPicture.ts` remembers, per mesh, the text a `TextPicture` shows, and hands it out through `pictureText(node)`. - `textProbe.eachText` reads both kinds and passes the text as a string instead of the Pixi node. `locate`, `locateAll` and `overlayTexts` use that string. Tests: - ./check.sh: 195 files, 2815 tests, CHECK EXIT 0 - smoke-jumprun: ALL PASSED, 24 checks - smoke-tetris: ALL PASSED, 19 checks - full e2e run (local): ALL PASSED, 1580 checks, the sum is unchanged although `locate` now also sees texts in the scene - counter-check: CI task 395 with the old code, same checks, 6 red Co-Authored-By: Claude Opus 5.5 --- src/editor/debug/uiHooks.ts | 4 ++-- src/editor/textProbe.ts | 18 ++++++++++++------ src/viewport/textPicture.ts | 12 ++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/editor/debug/uiHooks.ts b/src/editor/debug/uiHooks.ts index 65953314..339defff 100644 --- a/src/editor/debug/uiHooks.ts +++ b/src/editor/debug/uiHooks.ts @@ -64,8 +64,8 @@ export function uiHooks({ app, state, editor }: HookContext) { */ const overlayTexts = (): string[] => { const out: string[] = []; - eachText(ui().overlay, (node) => { - if (node.text) out.push(node.text); + eachText(ui().overlay, (text) => { + if (text) out.push(text); }); return out; }; diff --git a/src/editor/textProbe.ts b/src/editor/textProbe.ts index 6ec1c87f..ad518b19 100644 --- a/src/editor/textProbe.ts +++ b/src/editor/textProbe.ts @@ -1,5 +1,6 @@ import { type Application, type Container, Graphics, Rectangle, Text } from 'pixi.js'; import { Button, Widget } from '../ui'; +import { pictureText } from '../viewport/textPicture'; /** The global box of a widget: where it starts, and how large it is. */ export function rectOf(widget: { getGlobalPosition(): { x: number; y: number }; w: number; h: number }): { @@ -36,7 +37,7 @@ export interface IconHit { * it the sign-in screen needs, since it exists before there is an editor to ask. */ export function textProbe(app: Application): { - eachText: (root: Container, visit: (node: Text, point: { x: number; y: number }) => void) => void; + eachText: (root: Container, visit: (text: string, point: { x: number; y: number }) => void) => void; locateAll: (text: string) => { x: number; y: number }[]; locate: (text: string, last?: boolean) => { x: number; y: number } | null; locateIcons: (name: string) => IconHit[]; @@ -76,14 +77,19 @@ export function textProbe(app: Application): { }; walk(root, null, null); }; - const eachText = (root: Container, visit: (node: Text, point: { x: number; y: number }) => void): void => { + const eachText = ( + root: Container, + visit: (text: string, point: { x: number; y: number }) => void, + ): void => { eachShown(root, (node, clip) => { - if (!(node instanceof Text)) return false; + // A text object in the game is a picture of its glyphs (#169), not a Pixi `Text`. + const text = node instanceof Text ? node.text : pictureText(node); + if (text === undefined) return false; // Global bounds, not `width`/`height`: those are unscaled, and inside the graph canvas // (which is scaled by the zoom) that put the point right of the text it was meant to hit. const box = node.getBounds(); const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 }; - if (!clip || clip.contains(point.x, point.y)) visit(node, point); + if (!clip || clip.contains(point.x, point.y)) visit(text, point); return true; }); }; @@ -105,8 +111,8 @@ export function textProbe(app: Application): { /** Centres of every visible text equal to `text`, in drawing order (the topmost last). */ const locateAll = (text: string): { x: number; y: number }[] => { const hits: { x: number; y: number }[] = []; - eachText(app.stage, (node, point) => { - if (node.text === text) hits.push(point); + eachText(app.stage, (shown, point) => { + if (shown === text) hits.push(point); }); return hits; }; diff --git a/src/viewport/textPicture.ts b/src/viewport/textPicture.ts index 1155464e..606e62fd 100644 --- a/src/viewport/textPicture.ts +++ b/src/viewport/textPicture.ts @@ -70,6 +70,17 @@ export function sharpCoverage( return { data, width: w, height: h }; } +/** + * The text each picture shows, by its mesh. The mesh is a sprite quad and carries no text, so the test + * hook reads a game text through here – otherwise `locate` finds only the editor's own labels. + */ +const shownTexts = new WeakMap(); + +/** The text a node on screen draws as a `TextPicture`, or undefined for any other node. */ +export function pictureText(node: object): string | undefined { + return shownTexts.get(node as Mesh); +} + export class TextPicture { readonly mesh: Mesh = newSpriteMesh(); private readonly style = new TextStyle({ fontFamily: Theme.font, fontSize: 32, fill: 0xffffff }); @@ -88,6 +99,7 @@ export class TextPicture { const color = Number(item.props.color ?? 0xffffff); const resolution = textResolution(scale, item.fontSmooth); const key = JSON.stringify([text, family, size, color, resolution]); + shownTexts.set(this.mesh, text); const stale = key !== this.rasterKey; const previous = stale ? this.texture : null; if (stale) {