Trim needless comments and fix the picker width

Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 15:52:05 +02:00
1 parent f16801aef3
commit 4d84231b9f
115 files changed
+707 -1564

No files matched your search

+33 -32
View File
@@ -1,11 +1,11 @@
import { type Application, type Container, Rectangle, Text } from 'pixi.js';
import { ui, Widget } from '../ui';
import { Button, ui, Widget } from '../ui';
import type { CentreTab, Editor } from './Editor';
import type { EditorState } from './state';
/**
* Installs `window.__gm`, which the check scripts in tools/ use to find widgets by their
* visible text and to read editor state. Read-only access; harmless in production.
* visible text and to read editor state.
*/
export function installDebugHook(app: Application, state: EditorState, editor: Editor): void {
const { viewport, gameView, graphEditor, textureEditor, hierarchy } = editor;
@@ -25,8 +25,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
for (const child of node.children) walk(child);
};
walk(app.stage);
// TypeScript does not track the assignment inside `walk`, so it believes `found` is still
// `null` here – hence both the disable and the cast below.
// TypeScript does not track the assignment inside `walk`, hence the disable and the cast.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!found) return null;
const node = found as Text;
@@ -51,13 +50,9 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
return out;
};
/**
* Visible texts drawn wider than the widget that owns them – the shape of bug where a label
* runs past its own control instead of being shortened to fit.
*
* The owner's box is its declared `w`/`h` at its global position, NOT `getBounds()`: a Pixi
* container has no size of its own, so its bounds are the union of its children – including
* the very text that overflows, which makes every overflow measure as zero. Texts inside a
* scroll container are skipped, where sticking out is the point and the mask hides it.
* Visible texts drawn wider than the widget that owns them. The owner's box is its declared
* `w`/`h`, not `getBounds()`: a Pixi container's bounds are the union of its children –
* including the overflowing text – so every overflow would measure as zero.
*/
const textOverflows = (all = false): { text: string; owner: string; over: number }[] => {
const out: { text: string; owner: string; over: number }[] = [];
@@ -65,11 +60,9 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
if (!node.visible) return;
const isWidget = node instanceof Widget;
const box = isWidget ? node : owner;
// A mask further up – a panel clipping its contents – does not excuse a label spilling
// out of its own control, so the question restarts at every widget. Only a mask at or
// below the owner (a text field's scroll window, a scroll container) makes it intended.
// `Boolean`, not `!== null`: Pixi's mask getter returns `undefined` when nothing masks,
// so comparing against null marks every container as clipped and hides every finding.
// A mask further up does not excuse a label spilling out of its own control, so the
// question restarts at every widget. `Boolean`, not `!== null`: Pixi's mask getter
// returns `undefined` when nothing masks.
const masked = (isWidget ? false : clipped) || Boolean(node.mask);
if (node instanceof Text) {
if (!box || !node.text || (masked && !all)) return;
@@ -86,6 +79,22 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
walk(app.stage, null, false);
return out;
};
/**
* The counterpart of `textOverflows`: buttons whose label was shortened away to nothing, which
* leaves a control that says nothing at all. An empty `Text` is invisible to the overflow walk.
*/
const labelsDropped = (): { text: string; owner: string; w: number }[] => {
const out: { text: string; owner: string; w: number }[] = [];
const walk = (node: Container): void => {
if (!node.visible) return;
if (node instanceof Button && node.text && !node.shownText) {
out.push({ text: node.text, owner: node.constructor.name, w: node.w });
}
for (const child of node.children) walk(child);
};
walk(app.stage);
return out;
};
/** Screen rectangle of the open popup (the backdrop is not a widget), or null when none is. */
const overlayRect = (): { x: number; y: number; w: number; h: number } | null => {
const popups = ui().overlay.children.filter((node): node is Widget => node instanceof Widget);
@@ -100,33 +109,30 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
overlayTexts,
overlayRect,
textOverflows,
labelsDropped,
viewportCenter: () => centreOf(viewport),
viewportInfo: () => viewport.debugInfo(),
gameInfo: () => gameView.debugInfo(),
layerOf: (id: string) => viewport.layerOf(id),
drawOrder: () => viewport.drawOrder(),
/** Screen position of an object in the Scene tab (its drawn position through the view). */
objectScreen: (id: string) => viewport.objectScreen(id),
/** The rectangle it covers on screen – for sampling colours inside it with `pixelAt`. */
objectRect: (id: string) => viewport.objectRect(id),
/** The same for the Game tab (through the camera), so tests can click what the player sees. */
/** The same for the Game tab – through the camera, so what the player sees. */
gameScreen: (id: string) => gameView.objectScreen(id),
graph: {
current: () => graphEditor.currentAssetId,
portAt: (nodeId: string, port: string, isOutput: boolean) =>
graphEditor.canvas.portGlobal(nodeId, port, isOutput),
nodeAt: (nodeId: string) => graphEditor.canvas.nodeGlobal(nodeId),
// The canvas, not the whole tab: a point measured through the header would sit above
// the drawing area a right-click test aims at.
// The canvas, not the whole tab: a centre measured through the header sits above the
// drawing area.
center: () => centreOf(graphEditor.canvas),
pickerWidth: () => graphEditor.pickerWidth,
},
texture: {
current: () => textureEditor.currentAssetId,
/**
* Where an image pixel of the shown sheet sits on screen. Frames are dragged in image
* coordinates, so without this a check could only drag somewhere and hope – it could not
* aim at a known part of the picture and then assert which part came out.
*/
pickerWidth: () => textureEditor.pickerWidth,
/** Where an image pixel of the shown sheet sits on screen (frames are dragged in image coordinates). */
pointAt: (x: number, y: number) => textureEditor.imageToScreen(x, y),
},
world: () => editor.worldSnapshot(),
@@ -135,12 +141,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
hierarchyChevron: (id: string) => hierarchy.chevronPosition(id),
hierarchyExpandAll: () => hierarchy.expandAll(),
hierarchyCollapseAll: () => hierarchy.collapseAll(),
/**
* The colour actually on screen at a point, as `#rrggbb`. Everything else here reports what
* the model *says*; this is the only way a check can assert what was really drawn – which is
* the whole question for a texture, since a textured and an untextured sprite are identical
* in the model apart from one property.
*/
/** The colour actually on screen at a point, as `#rrggbb` – what was drawn, not what the model says. */
pixelAt: (x: number, y: number) => {
const { pixels } = app.renderer.extract.pixels({
target: app.stage,