Split the debug hook into one file per topic (#215)

debugHook.ts had grown to 1097 lines of unrelated test hooks. They
now live in seven files under src/editor/debug/, grouped by topic.
debugHook.ts only assembles them and is down to 86 lines.

- The shape of window.__gm is unchanged for tools/. Measured in the
  browser before and after: 120 top-level keys and 313 leaves with
  their arity, byte-identical JSON.
- The Text import is explicit in the helpers. Without it the name
  fell back to the DOM type and instanceof would never have matched.

Findings:
- The comment for vectorArt sat above pixelArt. It is back in place.
- An orphaned duplicate comment on assetRow is gone.

Tests:
- ./check.sh: 145 files, 2192 tests, CHECK EXIT 0 (unchanged)
- e2e 1-16,37,39-43,47,48: ALL PASSED, 734 checks
- The shape probe turns red against a deliberately misnested hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-23 10:20:06 +02:00
1 parent b830f9c747
commit ed29b2f3dc
9 files changed
+1114 -1031

No files matched your search

+197
View File
@@ -0,0 +1,197 @@
import { findAssetOf, findObjectHolder } from '../../core/model/Project';
import { resolveObjects } from '../../core/model/instance';
import { findLayer, layerZ } from '../../core/model/tilemap';
import { currentTilemapWidgets, paintedLayerId, paintedTilemap } from '../panels/TilemapPanel';
import type { TilemapTool } from '../state';
import { rectOf } from '../textProbe';
import { centreOf, type HookContext } from './hookHelpers';
/** The Scene and Game tabs, the tilemap painted in the scene view, and the Container tab. */
export function sceneHooks({ state, editor }: HookContext) {
const { viewport, gameView, containerEditor } = editor;
/**
* A holder's objects the way the editor sees them: the content of placed containers is resolved
* from the template, so it is nowhere in `holder.objects` (see `model/instance.ts`).
*/
const objectsOf = (holderId: string) => {
const holder = findObjectHolder(state.project, holderId);
return holder ? resolveObjects(state.project, holder) : [];
};
return {
sceneObjects: () => (state.activeScene ? objectsOf(state.activeScene.id) : []),
viewportCenter: () => centreOf(viewport),
/** Pans the Scene tab to a world point – a step's own lane, away from what earlier steps left. */
lookAt: (x: number, y: number) => viewport.lookAt(x, y),
/** Its drawn box – a step that wants an empty spot must stay inside what the console left over. */
viewportRect: () => {
const p = viewport.getGlobalPosition();
return { x: p.x, y: p.y, w: viewport.w, h: viewport.h };
},
/**
* A point in the viewport that no object covers – steps click there to deselect. A fixed
* offset from the centre lands on an object as soon as the viewport changes size.
*/
emptySpot: () => {
const p = viewport.getGlobalPosition();
const scene = state.activeScene;
const taken = scene
? objectsOf(scene.id)
.map((object) => viewport.objectRect(object.id))
.filter((rect): rect is NonNullable<typeof rect> => rect !== null)
: [];
const clear = (x: number, y: number) =>
!taken.some((r) => x >= r.x - 12 && x <= r.x + r.w + 12 && y >= r.y - 12 && y <= r.y + r.h + 12);
for (let y = p.y + viewport.h - 24; y > p.y + 24; y -= 24) {
for (let x = p.x + 24; x < p.x + viewport.w - 24; x += 24) if (clear(x, y)) return { x, y };
}
return null;
},
viewportInfo: () => viewport.debugInfo(),
/** A full redraw of the Scene tab – what a pan, which only moves the camera, is compared with. */
viewportRedraw: () => viewport.redraw(),
/** Where a dragged asset would land in the Scene tab, and why not when it may not. */
dropPreview: () => viewport.dropInfo(),
gameInfo: () => gameView.debugInfo(),
/** Selection outlines in the Scene tab: a switched-off object is invisible but still framed. */
gizmos: () => viewport.gizmoInfo(),
/** The corner handles of the polygon collider being edited, in global pixels (#151). */
polygonCorners: () => viewport.cornerInfo(),
/** The rays the editor run cast in the last moment, as the Scene tab draws them, in world units (#153). */
rays: () => viewport.rayInfo(),
layerOf: (id: string) => viewport.layerOf(id),
/** The icon a particle source wears in the Scene tab, with its name and its owner link. */
sceneIcon: (id: string) => viewport.iconInfo(id),
drawOrder: () => viewport.drawOrder(),
objectScreen: (id: string) => viewport.objectScreen(id),
objectRect: (id: string) => viewport.objectRect(id),
/** What a click at a screen point picks from in the Scene tab, front to back (#203). */
sceneStack: (x: number, y: number) => viewport.stackInfo(x, y),
/** The same for the Game tab – through the camera, so what the player sees. */
gameScreen: (id: string) => gameView.objectScreen(id),
gameRect: (id: string) => gameView.objectRect(id),
gameCenter: () => centreOf(gameView),
/** Which typeface a text ended up drawn in, and how wide it made it. */
textInfo: (id: string) => viewport.textInfo(id),
gameTextInfo: (id: string) => gameView.textInfo(id),
tilemap: {
/** What the scene view painted a map with, and on which layer (#128). */
paint: () => ({ ...state.tilemapPaint }),
/** The cells of one layer as the model holds them: `"x,y"` to tile id. */
cells: (assetId: string, layerId: string) => {
const asset = findAssetOf(state.project, assetId, 'tilemap');
const layer = asset && findLayer(asset, layerId);
return layer ? { ...layer.cells } : null;
},
layers: (assetId: string) =>
findAssetOf(state.project, assetId, 'tilemap')?.layers.map((layer) => ({
id: layer.id,
name: layer.name,
hidden: layer.hidden === true,
z: layerZ(layer),
cells: Object.keys(layer.cells).length,
})) ?? null,
/** What the scene view drew: meshes (one draw call each), cells and visible layers. */
info: (objectId: string) => viewport.tilemapInfo(objectId),
gameInfo: (objectId: string) => gameView.tilemapInfo(objectId),
/** Where the scene view outlines the map as blocking: the merged rectangles, in cells (#132). */
solids: (objectId: string) => viewport.solidInfo(objectId),
/**
* The painting panel beside the scene view (#142): whether it is on screen, its box, the
* scene view's box next to it and its splitter – null boxes while it is hidden.
*/
panel: () => {
// The editor hides the panel itself on every other tab, so its own flag is the whole answer.
const shown = editor.tilemapPanel.visible;
return {
shown,
title: editor.tilemapPanel.title,
panel: shown ? rectOf(editor.tilemapPanel) : null,
splitter: shown ? centreOf(editor.tilemapSplitter) : null,
viewport: rectOf(viewport),
};
},
/** Where the widgets of the painting panel sit: their texts are on screen elsewhere too. */
toolAt: (tool: string) => {
const button = currentTilemapWidgets()?.tools.get(tool as TilemapTool);
return button ? centreOf(button) : null;
},
tileButtonAt: (assetId: string) =>
currentTilemapWidgets()?.palette?.swatchGlobal(assetId)?.centre ?? null,
/** A tile's box in the palette: its centre, where its picture sits and the palette's columns (#139). */
tileSwatch: (assetId: string) => {
const palette = currentTilemapWidgets()?.palette;
const swatch = palette?.swatchGlobal(assetId);
return palette && swatch ? { ...swatch, columns: palette.columns } : null;
},
/** The plus in the layer list's header. */
addLayerAt: () => currentTilemapWidgets()?.layers.addGlobal() ?? null,
/**
* Where the painted layer's name stands in its row – a double-click there edits it (#138) –
* with the name the row shows and the field it is being edited in, while it is.
*/
layerNameAt: () => {
const widgets = currentTilemapWidgets();
const target = paintedTilemap(state);
if (!widgets || !target) return null;
const layerId = paintedLayerId(state, target.asset);
const at = widgets.layers.nameGlobal(layerId);
const editing = widgets.layers.renameField;
return at
? {
...at,
value: findLayer(target.asset, layerId)?.name ?? '',
editing: editing ? { ...centreOf(editing), value: editing.value } : null,
}
: null;
},
/** The Z field in the painted layer's row, with what it holds (#174). */
layerZAt: () => {
const widgets = currentTilemapWidgets();
const target = paintedTilemap(state);
const field = widgets && target && widgets.layers.valueField(paintedLayerId(state, target.asset));
return field ? { ...centreOf(field), value: field.value } : null;
},
/** The duplicate mark on the painted layer's row. */
duplicateLayerAt: () => {
const widgets = currentTilemapWidgets();
const target = paintedTilemap(state);
return widgets && target ? widgets.layers.duplicateGlobal(paintedLayerId(state, target.asset)) : null;
},
focusAt: () => {
const button = currentTilemapWidgets()?.focus;
return button ? { ...centreOf(button), label: button.text } : null;
},
/** One row of the layer list, top-down as drawn: the layer, where to press it, and its eye. */
layerRowAt: (index: number) => {
const list = currentTilemapWidgets()?.layers;
const id = list?.shownIds[index];
// On the name: the middle of the row may be the Z field, which a press would scrub.
const name = id ? list.nameGlobal(id) : null;
if (!list || !id || !name) return null;
// `y` is the row's own line, so a check can tell whether it is inside the panel.
return { id, y: name.y, name, visible: list.eyeGlobal(id) };
},
/** The cross on the painted layer's row; `enabled` is false where the row has none (the last layer). */
removeLayerAt: () => {
const widgets = currentTilemapWidgets();
const target = paintedTilemap(state);
if (!widgets || !target) return null;
const layerId = paintedLayerId(state, target.asset);
const at = widgets.layers.removeGlobal(layerId);
const row = widgets.layers.rowGlobal(layerId);
return at ? { ...at, enabled: true } : row ? { x: row.x, y: row.y, enabled: false } : null;
},
/** Where a cell sits on screen – for a click that paints it or a pixel that proves it. */
cellAt: (objectId: string, x: number, y: number) => viewport.cellScreen(objectId, x, y),
gameCellAt: (objectId: string, x: number, y: number) => gameView.cellScreen(objectId, x, y),
},
container: {
current: () => containerEditor.currentAssetId,
center: () => centreOf(containerEditor),
rect: () => containerEditor.canvasRect(),
/** Where a template object is drawn, so a check can click or sample it. */
objectScreen: (id: string) => containerEditor.objectScreen(id),
dropPreview: () => containerEditor.dropInfo(),
},
};
}