import type { Application } from 'pixi.js'; import { physicsStatus } from '../runtime/physics/engine'; import type { SchedulerOptions } from '../core/music/Scheduler'; import type { Sequence } from '../core/music/sequence'; import type { FxEnvelope, FxStage } from '../core/soundfx/nodes'; import { Music } from '../viewport/music'; import { bothAtCenter, envelopeDriver, followCurve, releaseEnvelope, soundLibrary, stageNodes, } from '../viewport/SoundLibrary'; import { curveHooks } from './debug/curveHooks'; import { patternHooks } from './debug/patternHooks'; import { songHooks } from './debug/songHooks'; import { documentHooks } from './debug/documentHooks'; import { graphHooks } from './debug/graphHooks'; import { centreOf, type HookContext } from './debug/hookHelpers'; import { pixelArtHooks } from './debug/pixelArtHooks'; import { renderHooks } from './debug/renderHooks'; import { sceneHooks } from './debug/sceneHooks'; import { uiHooks } from './debug/uiHooks'; import { vectorArtHooks } from './debug/vectorArtHooks'; import type { DocumentId, Editor, TabId } from './Editor'; import { NodeBoard } from './plugins/NodeBoard'; import type { EditorState } from './state'; import { rectOf } from './textProbe'; /** * Installs `window.__gm`, which the check scripts in tools/ use to find widgets by their * visible text and to read editor state. The hooks of each area live in `debug/`. */ export function installDebugHook(app: Application, state: EditorState, editor: Editor): void { const context: HookContext = { app, state, editor }; const music = new Music(() => state.project); (window as unknown as { __gm: unknown }).__gm = { state, /** The Pixi application, for probes that need the renderer or the stage. */ app, ...uiHooks(context), ...sceneHooks(context), ...graphHooks(context), ...documentHooks(context), ...pixelArtHooks(context), ...vectorArtHooks(context), ...curveHooks(context), ...patternHooks(context), ...songHooks(context), ...renderHooks(context), /** * Voices started, stopped and in flight. CI runs the browser with `--mute-audio`, so nothing * is ever heard there: these counters are the only way a check can see that a graph played * anything at all, the way `renderStats` is for a repaint that did not happen. */ sound: { stats: () => soundLibrary.stats(), duration: (file: string) => soundLibrary.duration(file), /** Where the knobs of each voice of that file are now, oldest first, a pitch as its speed (#315). */ levels: (file: string) => soundLibrary.levels(file), pitchWorkletUrl: () => soundLibrary.pitchWorkletUrl(), /** Seconds on the audio clock: the time a voice really runs on. */ clock: () => soundLibrary.clock(), /** Decodes ahead of a check, so a play can be asserted without racing the network. */ load: (file: string) => soundLibrary.load(file).then((buffer) => buffer?.duration ?? null), stopAll: () => soundLibrary.stopAll(), /** What Stop sound asks for: every voice of that file, its echoes cut or left to ring (#324). */ stop: (file: string, letRing: boolean) => soundLibrary.stop(file, letRing), /** The patterns a game plays, with the notes last planned of each. */ patterns: () => soundLibrary.patterns(), /** The nodes a stage of a voice becomes, in any context – an offline one renders what it does. */ stageNodes: (context: BaseAudioContext, stage: FxStage) => stageNodes(context, stage), /** What drives the two shares of a Mix "Both at center", in any context (unstarted). */ bothAtCenter: (context: BaseAudioContext, mix: number, a: GainNode, b: GainNode) => bothAtCenter(context, mix, a, b), /** The curve of an Envelope from `at` (unstarted), its release, and how it moves a number (#335). */ envelopeDriver: (context: BaseAudioContext, envelope: FxEnvelope, at: number) => envelopeDriver(context, envelope, at), releaseEnvelope: (level: AudioParam, envelope: FxEnvelope, startedAt: number, at: number) => releaseEnvelope(level, envelope, startedAt, at), stageWith: (context: BaseAudioContext, stage: FxStage, driver: ConstantSourceNode) => stageNodes(context, stage, (mod, param, ratio, map) => followCurve(context, driver, mod, param, ratio, map), ), }, /** * The scheduler (#332), which nothing in the editor plays yet: a check hands it a sequence of * notes and reads back what was planned for which second of the audio clock. */ music: { play: (sequence: Sequence, options?: SchedulerOptions & { from?: number }) => music.play(sequence, options), edit: (sequence: Sequence) => music.edit(sequence), stop: () => music.stop(), position: () => music.current?.position() ?? null, playing: () => music.current?.playing ?? false, planned: () => [...music.planned], latency: () => soundLibrary.latency(), }, world: () => editor.worldSnapshot(), playInfo: () => editor.playInfo(), /** Which scene Play would start, and under which of the two rules (#183: set in the settings). */ playFrom: () => editor.playFromInfo(), /** The two run buttons of the top bar: the picture each wears, and whether it takes a click. */ runButtons: () => editor.runButtons(), /** Where to click one of the icon buttons: menu, undo, redo, play, pause. */ toolbarAt: (name: keyof Editor['toolbarControls']) => centreOf(editor.toolbarControls[name]), /** Behaviors running without Play (`Run in editor`), and how long they have been at it. */ editorRun: () => editor.editorRunInfo(), physics: () => physicsStatus(), tab: () => editor.tab, showTab: (tab: TabId) => editor.showTab(tab), /** The open documents in tab order and the one showing (#171); a document is `asset:` or `list`. */ documents: () => editor.openDocuments(), openDocument: (doc: DocumentId) => editor.openDocument(doc), closeDocument: (doc: DocumentId) => editor.closeDocument(doc), plugins: { /** Every file in `data/plugins/` and what became of it. */ records: () => [...(editor.plugins?.records ?? [])], /** The plugins in the user management tab (#179): one row per file, with its switch. */ pane: () => ({ ...editor.pluginsPane.debugInfo(), visible: editor.tab === 'management' }), /** Where to click the switch of one plugin; null when it has no row. */ switchAt: (file: string) => { const box = editor.pluginsPane.switchOf(file); return box ? centreOf(box) : null; }, /** A plugin tab's node board as drawn, with its canvas and Fit button; null without one. */ board: (tabId: string) => { const pane = editor.pluginPane(tabId); if (!(pane instanceof NodeBoard)) return null; return { ...pane.debugInfo(), visible: pane.visible, rect: rectOf(pane), fit: centreOf(pane.fitButton), }; }, }, }; }