Files
feuerwerk/src/editor/textProbe.ts
T

89 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type Application, type Container, Rectangle, Text } from 'pixi.js';
import { Widget } from '../ui';
/** 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 }): {
x: number;
y: number;
w: number;
h: number;
} {
const p = widget.getGlobalPosition();
return { x: p.x, y: p.y, w: widget.w, h: widget.h };
}
/** The overlap of two global rectangles; an empty one when they do not meet. */
function intersect(a: Rectangle, b: Rectangle): Rectangle {
const x = Math.max(a.x, b.x);
const y = Math.max(a.y, b.y);
return new Rectangle(
x,
y,
Math.max(0, Math.min(a.right, b.right) - x),
Math.max(0, Math.min(a.bottom, b.bottom) - y),
);
}
/**
* Finding widgets by the text they show – the ground floor of the test hook, and the only part of
* 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;
locateAll: (text: string) => { x: number; y: number }[];
locate: (text: string, last?: boolean) => { x: number; y: number } | null;
} {
/**
* Every text under `root` that is really on screen, in drawing order (the topmost last), handed
* to `visit` with the centre a click on it would use.
*
* A row scrolled out of its list is still `visible` – only masked away – and reporting it would
* hand a check a point outside the panel, where the click lands on whatever is underneath. That
* is how a step that passes on a short list fails on a long one (#39 in CI), so a text whose
* centre falls outside the clipping it sits in counts as not being there at all.
*
* Every hook that reads text goes through here. A `locate` that refuses a scrolled-away row
* while `overlayTexts` still lists it is worse than either rule alone: a check picks a row off
* the one hook and then cannot find it with the other.
*
* The clip is the box of the *widget* that owns the mask, not the mask itself: Pixi hands a
* graphics that serves as a mask to the mask effect, and `getBounds()` on it then reports an
* empty rectangle at the origin – which would throw away every masked text in the editor.
*/
const eachText = (root: Container, visit: (node: Text, point: { x: number; y: number }) => void): void => {
const walk = (node: Container, clip: Rectangle | null, owner: Widget | null): void => {
if (!node.visible) return;
const holder = node instanceof Widget ? node : owner;
let inner = clip;
if (node.mask && holder) {
const r = rectOf(holder);
const box = new Rectangle(r.x, r.y, r.w, r.h);
inner = clip ? intersect(clip, box) : box;
}
if (node instanceof Text) {
// 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 (!inner || inner.contains(point.x, point.y)) visit(node, point);
return;
}
for (const child of node.children) walk(child, inner, holder);
};
walk(root, null, null);
};
/** 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);
});
return hits;
};
const locate = (text: string, last = false): { x: number; y: number } | null => {
const hits = locateAll(text);
return (last ? hits.at(-1) : hits[0]) ?? null;
};
return { eachText, locateAll, locate };
}