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:
1 parent
b830f9c747
commit
ed29b2f3dc
9 files changed
+1114
-1031
No files matched your search
@@ -0,0 +1,159 @@
|
||||
import { type FrameRect, resolvePicture } from '../../core/model/texture';
|
||||
import { effectiveScale, gameClock } from '../../core/time';
|
||||
import { afterFrames, isRendering, renderCount, requestRender } from '../../ui';
|
||||
import { particleLibrary } from '../../viewport/ParticleLibrary';
|
||||
import { shaderLibrary } from '../../viewport/ShaderLibrary';
|
||||
import { textureLibrary } from '../../viewport/TextureLibrary';
|
||||
import { type Container, Mesh, Rectangle } from 'pixi.js';
|
||||
import type { HookContext } from './hookHelpers';
|
||||
|
||||
/** What was really drawn, and the clocks behind it: pixels, textures, frames, shaders and particles. */
|
||||
export function renderHooks({ app, state, editor }: HookContext) {
|
||||
const { viewport, gameView } = editor;
|
||||
/** The colour actually on screen at a point, as `#rrggbb` – what was drawn, not what the model says. */
|
||||
const pixelAt = (x: number, y: number): string => {
|
||||
const { pixels } = app.renderer.extract.pixels({
|
||||
target: app.stage,
|
||||
frame: new Rectangle(x, y, 1, 1),
|
||||
});
|
||||
const hex = (value: number | undefined) => (value ?? 0).toString(16).padStart(2, '0');
|
||||
return `#${hex(pixels[0])}${hex(pixels[1])}${hex(pixels[2])}`;
|
||||
};
|
||||
return {
|
||||
pixelAt,
|
||||
/**
|
||||
* A horizontal run of pixels in one read. A check that has to tell a repeated pattern from a
|
||||
* stretched one needs dozens of samples, and dozens of round trips would cost more than the
|
||||
* step it belongs to.
|
||||
*/
|
||||
pixelRow: (x: number, y: number, count: number) => {
|
||||
const width = Math.max(1, Math.min(1024, Math.round(count)));
|
||||
const row: string[] = [];
|
||||
// One extract per pixel, not one strip: a wider frame comes back resampled, and a row that
|
||||
// blends its neighbours cannot tell a hard border from a soft one – which is what it is for.
|
||||
for (let i = 0; i < width; i++) row.push(pixelAt(x + i, y));
|
||||
return row;
|
||||
},
|
||||
/** The filter and wrap the GPU sampler actually got, next to the model's – only the driver knows the former. */
|
||||
textureFilter: (file: string, rect: FrameRect | null = null) => {
|
||||
const texture = textureLibrary.peek(file, rect);
|
||||
if (!texture) return null;
|
||||
const source = texture.source;
|
||||
const renderer = app.renderer as unknown as {
|
||||
gl?: WebGL2RenderingContext;
|
||||
uid: number;
|
||||
texture: { bindSource(source: unknown, location: number): void };
|
||||
};
|
||||
const gl = renderer.gl;
|
||||
const uploaded = Boolean(
|
||||
(source as unknown as { _gpuData: Record<number, unknown> })._gpuData[renderer.uid],
|
||||
);
|
||||
let magFilter = 0;
|
||||
let wrapT = 0;
|
||||
if (gl) {
|
||||
// Through Pixi rather than a raw `bindTexture`, so its idea of what is bound stays true.
|
||||
renderer.texture.bindSource(source, 0);
|
||||
magFilter = gl.getTexParameter(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER) as number;
|
||||
wrapT = gl.getTexParameter(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T) as number;
|
||||
}
|
||||
return {
|
||||
scaleMode: source.scaleMode,
|
||||
gpu: magFilter === gl?.NEAREST ? 'nearest' : magFilter === gl?.LINEAR ? 'linear' : 'none',
|
||||
wrap:
|
||||
wrapT === gl?.CLAMP_TO_EDGE
|
||||
? 'clamp'
|
||||
: wrapT === gl?.REPEAT
|
||||
? 'repeat'
|
||||
: wrapT === gl?.MIRRORED_REPEAT
|
||||
? 'mirror'
|
||||
: 'none',
|
||||
uploaded,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* A picture an asset draws itself (#129, #185): the key it is kept under, the canvas the model
|
||||
* says it is, and the size it actually lies on the GPU in – which is the only place the
|
||||
* supersampling of the rasteriser is visible. `kept` false means the key is not in memory,
|
||||
* which is what a check reads to see an old revision let go.
|
||||
*/
|
||||
drawnPicture: (assetId: string) => {
|
||||
const ref = resolvePicture(state.project, assetId);
|
||||
if (!ref) return null;
|
||||
const texture = textureLibrary.peek(ref.file);
|
||||
return {
|
||||
file: ref.file,
|
||||
width: ref.sourceWidth,
|
||||
height: ref.sourceHeight,
|
||||
pixelWidth: texture?.source.pixelWidth ?? 0,
|
||||
pixelHeight: texture?.source.pixelHeight ?? 0,
|
||||
kept: texture !== null,
|
||||
};
|
||||
},
|
||||
fps: () => editor.fps,
|
||||
/**
|
||||
* What the render loop did: frames actually drawn, and whether it is currently awake. The
|
||||
* only way to see a missing repaint from a check script – `pixelAt` re-renders on demand and
|
||||
* would answer correctly even if the screen had been frozen for a minute.
|
||||
*/
|
||||
renderStats: () => ({ drawn: renderCount(), awake: isRendering() }),
|
||||
/** Draw again, whatever happens next – what a screenshot needs before it is taken. */
|
||||
wake: () => requestRender(),
|
||||
/**
|
||||
* Resolves once the editor has drawn `count` more frames. The checks await this instead of
|
||||
* sleeping a guessed number of milliseconds: a frame is the editor saying it has caught up,
|
||||
* where a deadline is only the harness hoping it has.
|
||||
*/
|
||||
afterFrames: (count = 2, timeoutMs = 2000) => afterFrames(count, timeoutMs),
|
||||
/** Visible nodes carrying a filter – one extra render pass each; since #77 a shader is none. */
|
||||
filterCount: () => {
|
||||
let n = 0;
|
||||
const walk = (node: Container): void => {
|
||||
if (!node.visible) return;
|
||||
if (((node.filters as readonly unknown[] | undefined)?.length ?? 0) > 0) n += 1;
|
||||
for (const child of node.children) walk(child);
|
||||
};
|
||||
walk(app.stage);
|
||||
return n;
|
||||
},
|
||||
/** Visible sprite meshes drawing with a shader of their own – one draw call each, no pass (#77). */
|
||||
shaderCount: () => {
|
||||
let n = 0;
|
||||
const walk = (node: Container): void => {
|
||||
if (!node.visible) return;
|
||||
if (node instanceof Mesh && node.shader) n += 1;
|
||||
for (const child of node.children) walk(child);
|
||||
};
|
||||
walk(app.stage);
|
||||
return n;
|
||||
},
|
||||
/** The global clock: the second it stands at, the scale set, and the one a pause – either kind – makes of it. */
|
||||
globalTime: () => ({
|
||||
now: gameClock.now,
|
||||
scale: gameClock.scale,
|
||||
effective: effectiveScale(gameClock.scale, state.paused || editor.playInfo().gamePaused),
|
||||
}),
|
||||
/** Sets the global time scale – there is no slider for it in the GUI yet. */
|
||||
setTimeScale: (scale: number) => {
|
||||
gameClock.scale = scale;
|
||||
},
|
||||
/** Holds the shader clock at `at` seconds, so a Time shader's pixels can be measured; null releases it. */
|
||||
shaderTime: (at: number | null) => shaderLibrary.freezeTime(at),
|
||||
/** The same for particle time, kept apart so one can be held without the other. */
|
||||
particleTime: (at: number | null) => particleLibrary.freezeTime(at),
|
||||
/** Every source's clock back to zero on the current (usually frozen) second. */
|
||||
particlesRestart: () => {
|
||||
viewport.restartParticles(particleLibrary.now());
|
||||
gameView.restartParticles(particleLibrary.now());
|
||||
},
|
||||
/** What a source is drawing in the scene tab, or in the game view while playing. */
|
||||
particlesOf: (objectId: string, where: 'scene' | 'game' = 'scene', limit = 8) =>
|
||||
(where === 'game' ? gameView : viewport).particleInfo(objectId, limit),
|
||||
/** Graphs compiled so far; a moved node must not add one (#65). */
|
||||
shaderCompiles: () => shaderLibrary.compiles,
|
||||
/** Which views redraw every tick because a Time shader is on screen. */
|
||||
animates: () => ({ scene: viewport.animates, game: gameView.animates }),
|
||||
lowLatency: () =>
|
||||
(app.renderer as unknown as { gl?: WebGL2RenderingContext }).gl?.getContextAttributes()
|
||||
?.desynchronized === true,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user